feat: add configurable congestion modes and BBR profiles
This commit is contained in:
parent
a00368f9e3
commit
b3f2193f10
14 changed files with 475 additions and 36 deletions
|
|
@ -65,6 +65,7 @@ type clientConfig struct {
|
|||
Obfs clientConfigObfs `mapstructure:"obfs"`
|
||||
TLS clientConfigTLS `mapstructure:"tls"`
|
||||
QUIC clientConfigQUIC `mapstructure:"quic"`
|
||||
Congestion clientConfigCongestion `mapstructure:"congestion"`
|
||||
Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"`
|
||||
FastOpen bool `mapstructure:"fastOpen"`
|
||||
Lazy bool `mapstructure:"lazy"`
|
||||
|
|
@ -127,6 +128,11 @@ type clientConfigBandwidth struct {
|
|||
Down string `mapstructure:"down"`
|
||||
}
|
||||
|
||||
type clientConfigCongestion struct {
|
||||
Type string `mapstructure:"type"`
|
||||
BBRProfile string `mapstructure:"bbrProfile"`
|
||||
}
|
||||
|
||||
type socks5Config struct {
|
||||
Listen string `mapstructure:"listen"`
|
||||
Username string `mapstructure:"username"`
|
||||
|
|
@ -355,6 +361,22 @@ func (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConfig) fillCongestionConfig(hyConfig *client.Config) error {
|
||||
normalizedType, err := normalizeCongestionType(c.Congestion.Type)
|
||||
if err != nil {
|
||||
return configError{Field: "congestion.type", Err: err}
|
||||
}
|
||||
hyConfig.CongestionConfig.Type = normalizedType
|
||||
if normalizedType == congestionTypeBBR {
|
||||
normalizedProfile, err := normalizeBBRProfile(c.Congestion.BBRProfile)
|
||||
if err != nil {
|
||||
return configError{Field: "congestion.bbrProfile", Err: err}
|
||||
}
|
||||
hyConfig.CongestionConfig.BBRProfile = normalizedProfile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {
|
||||
hyConfig.FastOpen = c.FastOpen
|
||||
return nil
|
||||
|
|
@ -457,6 +479,7 @@ func (c *clientConfig) Config() (*client.Config, error) {
|
|||
c.fillAuth,
|
||||
c.fillTLSConfig,
|
||||
c.fillQUICConfig,
|
||||
c.fillCongestionConfig,
|
||||
c.fillBandwidthConfig,
|
||||
c.fillFastOpen,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
|
@ -54,6 +55,10 @@ func TestClientConfig(t *testing.T) {
|
|||
FdControlUnixSocket: stringRef("test.sock"),
|
||||
},
|
||||
},
|
||||
Congestion: clientConfigCongestion{
|
||||
Type: "bbr",
|
||||
BBRProfile: "aggressive",
|
||||
},
|
||||
Bandwidth: clientConfigBandwidth{
|
||||
Up: "200 mbps",
|
||||
Down: "1 gbps",
|
||||
|
|
@ -197,6 +202,46 @@ func TestClientConfigURI(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientFillCongestionConfig(t *testing.T) {
|
||||
t.Run("defaults to bbr standard", func(t *testing.T) {
|
||||
hyConfig := &client.Config{}
|
||||
err := (&clientConfig{}).fillCongestionConfig(hyConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bbr", hyConfig.CongestionConfig.Type)
|
||||
assert.Equal(t, "standard", hyConfig.CongestionConfig.BBRProfile)
|
||||
})
|
||||
|
||||
t.Run("reno ignores bbr profile", func(t *testing.T) {
|
||||
hyConfig := &client.Config{}
|
||||
err := (&clientConfig{
|
||||
Congestion: clientConfigCongestion{
|
||||
Type: "reno",
|
||||
BBRProfile: "definitely-invalid",
|
||||
},
|
||||
}).fillCongestionConfig(hyConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "reno", hyConfig.CongestionConfig.Type)
|
||||
assert.Empty(t, hyConfig.CongestionConfig.BBRProfile)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid type", func(t *testing.T) {
|
||||
err := (&clientConfig{
|
||||
Congestion: clientConfigCongestion{Type: "cubic"},
|
||||
}).fillCongestionConfig(&client.Config{})
|
||||
assert.EqualError(t, err, `invalid config: congestion.type: unsupported congestion type "cubic"`)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid bbr profile", func(t *testing.T) {
|
||||
err := (&clientConfig{
|
||||
Congestion: clientConfigCongestion{
|
||||
Type: "bbr",
|
||||
BBRProfile: "turbo",
|
||||
},
|
||||
}).fillCongestionConfig(&client.Config{})
|
||||
assert.EqualError(t, err, `invalid config: congestion.bbrProfile: unsupported BBR profile "turbo"`)
|
||||
})
|
||||
}
|
||||
|
||||
func stringRef(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ quic:
|
|||
fwmark: 1234
|
||||
fdControlUnixSocket: test.sock
|
||||
|
||||
congestion:
|
||||
type: bbr
|
||||
bbrProfile: aggressive
|
||||
|
||||
bandwidth:
|
||||
up: 200 mbps
|
||||
down: 1 gbps
|
||||
|
|
|
|||
33
app/cmd/congestion.go
Normal file
33
app/cmd/congestion.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
congestionTypeBBR = "bbr"
|
||||
congestionTypeReno = "reno"
|
||||
)
|
||||
|
||||
func normalizeCongestionType(congestionType string) (string, error) {
|
||||
switch normalized := strings.ToLower(congestionType); normalized {
|
||||
case "", congestionTypeBBR:
|
||||
return congestionTypeBBR, nil
|
||||
case congestionTypeReno:
|
||||
return congestionTypeReno, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported congestion type %q", congestionType)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBBRProfile(profile string) (string, error) {
|
||||
switch normalized := strings.ToLower(profile); normalized {
|
||||
case "", "standard":
|
||||
return "standard", nil
|
||||
case "conservative", "aggressive":
|
||||
return normalized, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported BBR profile %q", profile)
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ type serverConfig struct {
|
|||
TLS *serverConfigTLS `mapstructure:"tls"`
|
||||
ACME *serverConfigACME `mapstructure:"acme"`
|
||||
QUIC serverConfigQUIC `mapstructure:"quic"`
|
||||
Congestion serverConfigCongestion `mapstructure:"congestion"`
|
||||
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
|
||||
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
|
||||
SpeedTest bool `mapstructure:"speedTest"`
|
||||
|
|
@ -140,6 +141,11 @@ type serverConfigBandwidth struct {
|
|||
Down string `mapstructure:"down"`
|
||||
}
|
||||
|
||||
type serverConfigCongestion struct {
|
||||
Type string `mapstructure:"type"`
|
||||
BBRProfile string `mapstructure:"bbrProfile"`
|
||||
}
|
||||
|
||||
type serverConfigAuthHTTP struct {
|
||||
URL string `mapstructure:"url"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
|
|
@ -740,6 +746,22 @@ func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConfig) fillCongestionConfig(hyConfig *server.Config) error {
|
||||
normalizedType, err := normalizeCongestionType(c.Congestion.Type)
|
||||
if err != nil {
|
||||
return configError{Field: "congestion.type", Err: err}
|
||||
}
|
||||
hyConfig.CongestionConfig.Type = normalizedType
|
||||
if normalizedType == congestionTypeBBR {
|
||||
normalizedProfile, err := normalizeBBRProfile(c.Congestion.BBRProfile)
|
||||
if err != nil {
|
||||
return configError{Field: "congestion.bbrProfile", Err: err}
|
||||
}
|
||||
hyConfig.CongestionConfig.BBRProfile = normalizedProfile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error {
|
||||
hyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth
|
||||
return nil
|
||||
|
|
@ -918,6 +940,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
|
|||
c.fillQUICConfig,
|
||||
c.fillRequestHook,
|
||||
c.fillOutboundConfig,
|
||||
c.fillCongestionConfig,
|
||||
c.fillBandwidthConfig,
|
||||
c.fillIgnoreClientBandwidth,
|
||||
c.fillDisableUDP,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/server"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
|
@ -68,6 +69,10 @@ func TestServerConfig(t *testing.T) {
|
|||
MaxIncomingStreams: 256,
|
||||
DisablePathMTUDiscovery: true,
|
||||
},
|
||||
Congestion: serverConfigCongestion{
|
||||
Type: "reno",
|
||||
BBRProfile: "aggressive",
|
||||
},
|
||||
Bandwidth: serverConfigBandwidth{
|
||||
Up: "500 mbps",
|
||||
Down: "100 mbps",
|
||||
|
|
@ -189,3 +194,43 @@ func TestServerConfig(t *testing.T) {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerFillCongestionConfig(t *testing.T) {
|
||||
t.Run("defaults to bbr standard", func(t *testing.T) {
|
||||
hyConfig := &server.Config{}
|
||||
err := (&serverConfig{}).fillCongestionConfig(hyConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bbr", hyConfig.CongestionConfig.Type)
|
||||
assert.Equal(t, "standard", hyConfig.CongestionConfig.BBRProfile)
|
||||
})
|
||||
|
||||
t.Run("reno ignores bbr profile", func(t *testing.T) {
|
||||
hyConfig := &server.Config{}
|
||||
err := (&serverConfig{
|
||||
Congestion: serverConfigCongestion{
|
||||
Type: "reno",
|
||||
BBRProfile: "invalid",
|
||||
},
|
||||
}).fillCongestionConfig(hyConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "reno", hyConfig.CongestionConfig.Type)
|
||||
assert.Empty(t, hyConfig.CongestionConfig.BBRProfile)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid type", func(t *testing.T) {
|
||||
err := (&serverConfig{
|
||||
Congestion: serverConfigCongestion{Type: "cubic"},
|
||||
}).fillCongestionConfig(&server.Config{})
|
||||
assert.EqualError(t, err, `invalid config: congestion.type: unsupported congestion type "cubic"`)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid bbr profile", func(t *testing.T) {
|
||||
err := (&serverConfig{
|
||||
Congestion: serverConfigCongestion{
|
||||
Type: "bbr",
|
||||
BBRProfile: "turbo",
|
||||
},
|
||||
}).fillCongestionConfig(&server.Config{})
|
||||
assert.EqualError(t, err, `invalid config: congestion.bbrProfile: unsupported BBR profile "turbo"`)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ quic:
|
|||
maxIncomingStreams: 256
|
||||
disablePathMTUDiscovery: true
|
||||
|
||||
congestion:
|
||||
type: reno
|
||||
bbrProfile: aggressive
|
||||
|
||||
bandwidth:
|
||||
up: 500 mbps
|
||||
down: 100 mbps
|
||||
|
|
|
|||
|
|
@ -134,8 +134,8 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
|
|||
var actualTx uint64
|
||||
if authResp.RxAuto {
|
||||
// Server asks client to use bandwidth detection,
|
||||
// ignore local bandwidth config and use BBR
|
||||
congestion.UseBBR(conn)
|
||||
// ignore local bandwidth config and use the configured congestion controller.
|
||||
congestion.UseConfigured(conn, c.config.CongestionConfig.Type, c.config.CongestionConfig.BBRProfile)
|
||||
} else {
|
||||
// actualTx = min(serverRx, clientTx)
|
||||
actualTx = authResp.Rx
|
||||
|
|
@ -146,8 +146,8 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
|
|||
if actualTx > 0 {
|
||||
congestion.UseBrutal(conn, actualTx)
|
||||
} else {
|
||||
// We don't know our own bandwidth either, use BBR
|
||||
congestion.UseBBR(conn)
|
||||
// We don't know our own bandwidth either, use the configured congestion controller.
|
||||
congestion.UseConfigured(conn, c.config.CongestionConfig.Type, c.config.CongestionConfig.BBRProfile)
|
||||
}
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/errors"
|
||||
"github.com/apernet/hysteria/core/v2/internal/congestion"
|
||||
"github.com/apernet/hysteria/core/v2/internal/pmtud"
|
||||
)
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ type Config struct {
|
|||
Auth string
|
||||
TLSConfig TLSConfig
|
||||
QUICConfig QUICConfig
|
||||
CongestionConfig CongestionConfig
|
||||
BandwidthConfig BandwidthConfig
|
||||
FastOpen bool
|
||||
|
||||
|
|
@ -72,6 +74,17 @@ func (c *Config) verifyAndFill() error {
|
|||
return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"}
|
||||
}
|
||||
c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
|
||||
var err error
|
||||
c.CongestionConfig.Type, err = congestion.NormalizeType(c.CongestionConfig.Type)
|
||||
if err != nil {
|
||||
return errors.ConfigError{Field: "CongestionConfig.Type", Reason: err.Error()}
|
||||
}
|
||||
if c.CongestionConfig.Type == congestion.TypeBBR {
|
||||
c.CongestionConfig.BBRProfile, err = congestion.NormalizeBBRProfile(c.CongestionConfig.BBRProfile)
|
||||
if err != nil {
|
||||
return errors.ConfigError{Field: "CongestionConfig.BBRProfile", Reason: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
c.filled = true
|
||||
return nil
|
||||
|
|
@ -107,6 +120,11 @@ type QUICConfig struct {
|
|||
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
|
||||
}
|
||||
|
||||
type CongestionConfig struct {
|
||||
Type string
|
||||
BBRProfile string
|
||||
}
|
||||
|
||||
// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.
|
||||
type BandwidthConfig struct {
|
||||
MaxTx uint64
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/congestion"
|
||||
|
|
@ -93,6 +94,76 @@ const (
|
|||
bbrRecoveryStateGrowth
|
||||
)
|
||||
|
||||
type Profile string
|
||||
|
||||
const (
|
||||
ProfileConservative Profile = "conservative"
|
||||
ProfileStandard Profile = "standard"
|
||||
ProfileAggressive Profile = "aggressive"
|
||||
)
|
||||
|
||||
type profileConfig struct {
|
||||
highGain float64
|
||||
highCwndGain float64
|
||||
congestionWindowGainConstant float64
|
||||
numStartupRtts int64
|
||||
drainToTarget bool
|
||||
detectOvershooting bool
|
||||
bytesLostMultiplier uint8
|
||||
enableAckAggregationStartup bool
|
||||
expireAckAggregationStartup bool
|
||||
enableOverestimateAvoidance bool
|
||||
reduceExtraAckedOnBandwidthIncrease bool
|
||||
}
|
||||
|
||||
func ParseProfile(profile string) (Profile, error) {
|
||||
switch normalized := strings.ToLower(profile); normalized {
|
||||
case "", string(ProfileStandard):
|
||||
return ProfileStandard, nil
|
||||
case string(ProfileConservative):
|
||||
return ProfileConservative, nil
|
||||
case string(ProfileAggressive):
|
||||
return ProfileAggressive, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported BBR profile %q", profile)
|
||||
}
|
||||
}
|
||||
|
||||
func configForProfile(profile Profile) profileConfig {
|
||||
switch profile {
|
||||
case ProfileConservative:
|
||||
return profileConfig{
|
||||
highGain: 2.25,
|
||||
highCwndGain: 1.75,
|
||||
congestionWindowGainConstant: 1.75,
|
||||
numStartupRtts: 2,
|
||||
drainToTarget: true,
|
||||
detectOvershooting: true,
|
||||
bytesLostMultiplier: 1,
|
||||
enableOverestimateAvoidance: true,
|
||||
reduceExtraAckedOnBandwidthIncrease: true,
|
||||
}
|
||||
case ProfileAggressive:
|
||||
return profileConfig{
|
||||
highGain: 3.0,
|
||||
highCwndGain: 2.25,
|
||||
congestionWindowGainConstant: 2.5,
|
||||
numStartupRtts: 4,
|
||||
bytesLostMultiplier: 2,
|
||||
enableAckAggregationStartup: true,
|
||||
expireAckAggregationStartup: true,
|
||||
}
|
||||
default:
|
||||
return profileConfig{
|
||||
highGain: defaultHighGain,
|
||||
highCwndGain: derivedHighCWNDGain,
|
||||
congestionWindowGainConstant: 2.0,
|
||||
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
|
||||
bytesLostMultiplier: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type bbrSender struct {
|
||||
rttStats congestion.RTTStatsProvider
|
||||
clock Clock
|
||||
|
|
@ -141,6 +212,9 @@ type bbrSender struct {
|
|||
// The smallest value the |congestion_window_| can achieve.
|
||||
minCongestionWindow congestion.ByteCount
|
||||
|
||||
// The BBR profile used by the sender.
|
||||
profile Profile
|
||||
|
||||
// The pacing gain applied during the STARTUP phase.
|
||||
highGain float64
|
||||
|
||||
|
|
@ -247,12 +321,14 @@ var _ congestion.CongestionControl = &bbrSender{}
|
|||
func NewBbrSender(
|
||||
clock Clock,
|
||||
initialMaxDatagramSize congestion.ByteCount,
|
||||
profile Profile,
|
||||
) *bbrSender {
|
||||
return newBbrSender(
|
||||
clock,
|
||||
initialMaxDatagramSize,
|
||||
initialCongestionWindowPackets*initialMaxDatagramSize,
|
||||
congestion.MaxCongestionWindowPackets*initialMaxDatagramSize,
|
||||
profile,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +337,7 @@ func newBbrSender(
|
|||
initialMaxDatagramSize,
|
||||
initialCongestionWindow,
|
||||
initialMaxCongestionWindow congestion.ByteCount,
|
||||
profile Profile,
|
||||
) *bbrSender {
|
||||
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
|
||||
b := &bbrSender{
|
||||
|
|
@ -274,8 +351,9 @@ func newBbrSender(
|
|||
initialCongestionWindow: initialCongestionWindow,
|
||||
maxCongestionWindow: initialMaxCongestionWindow,
|
||||
minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize),
|
||||
profile: ProfileStandard,
|
||||
highGain: defaultHighGain,
|
||||
highCwndGain: defaultHighGain,
|
||||
highCwndGain: derivedHighCWNDGain,
|
||||
drainGain: 1.0 / defaultHighGain,
|
||||
pacingGain: 1.0,
|
||||
congestionWindowGain: 1.0,
|
||||
|
|
@ -291,13 +369,38 @@ func newBbrSender(
|
|||
debug: debug,
|
||||
}
|
||||
b.pacer = common.NewPacer(b.bandwidthForPacer)
|
||||
b.applyProfile(profile)
|
||||
if b.debug {
|
||||
b.debugPrint("Profile: %s", b.profile)
|
||||
}
|
||||
|
||||
b.enterStartupMode(b.clock.Now())
|
||||
b.setHighCwndGain(derivedHighCWNDGain)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bbrSender) applyProfile(profile Profile) {
|
||||
if profile == "" {
|
||||
profile = ProfileStandard
|
||||
}
|
||||
cfg := configForProfile(profile)
|
||||
b.profile = profile
|
||||
b.highGain = cfg.highGain
|
||||
b.highCwndGain = cfg.highCwndGain
|
||||
b.drainGain = 1.0 / cfg.highGain
|
||||
b.congestionWindowGainConstant = cfg.congestionWindowGainConstant
|
||||
b.numStartupRtts = cfg.numStartupRtts
|
||||
b.drainToTarget = cfg.drainToTarget
|
||||
b.detectOvershooting = cfg.detectOvershooting
|
||||
b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier
|
||||
b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup
|
||||
b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup
|
||||
if cfg.enableOverestimateAvoidance {
|
||||
b.sampler.EnableOverestimateAvoidance()
|
||||
}
|
||||
b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease)
|
||||
}
|
||||
|
||||
func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount {
|
||||
return minCongestionWindowPackets * maxDatagramSize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ func TestSetMaxDatagramSizeRescalesPacketSizedWindows(t *testing.T) {
|
|||
oldMaxDatagramSize,
|
||||
initialCongestionWindowPackets*oldMaxDatagramSize,
|
||||
maxCongestionWindowPackets*oldMaxDatagramSize,
|
||||
ProfileStandard,
|
||||
)
|
||||
b.congestionWindow = b.initialCongestionWindow
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ func TestSetMaxDatagramSizeClampsCongestionWindow(t *testing.T) {
|
|||
const oldMaxDatagramSize = congestion.ByteCount(1000)
|
||||
const newMaxDatagramSize = congestion.ByteCount(1400)
|
||||
|
||||
b := NewBbrSender(DefaultClock{}, oldMaxDatagramSize)
|
||||
b := NewBbrSender(DefaultClock{}, oldMaxDatagramSize, ProfileStandard)
|
||||
b.congestionWindow = b.minCongestionWindow + oldMaxDatagramSize
|
||||
b.recoveryWindow = b.minCongestionWindow + oldMaxDatagramSize
|
||||
|
||||
|
|
@ -42,3 +43,88 @@ func TestSetMaxDatagramSizeClampsCongestionWindow(t *testing.T) {
|
|||
require.Equal(t, b.minCongestionWindow, b.congestionWindow)
|
||||
require.Equal(t, b.minCongestionWindow, b.recoveryWindow)
|
||||
}
|
||||
|
||||
func TestNewBbrSenderAppliesProfiles(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
profile Profile
|
||||
highGain float64
|
||||
highCwndGain float64
|
||||
congestionWindowGainConstant float64
|
||||
numStartupRtts int64
|
||||
drainToTarget bool
|
||||
detectOvershooting bool
|
||||
bytesLostMultiplier uint8
|
||||
enableAckAggregationDuringStartup bool
|
||||
expireAckAggregationInStartup bool
|
||||
enableOverestimateAvoidance bool
|
||||
reduceExtraAckedOnBandwidthIncrease bool
|
||||
}{
|
||||
{
|
||||
name: "standard",
|
||||
profile: ProfileStandard,
|
||||
highGain: defaultHighGain,
|
||||
highCwndGain: derivedHighCWNDGain,
|
||||
congestionWindowGainConstant: 2.0,
|
||||
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
|
||||
bytesLostMultiplier: 2,
|
||||
},
|
||||
{
|
||||
name: "conservative",
|
||||
profile: ProfileConservative,
|
||||
highGain: 2.25,
|
||||
highCwndGain: 1.75,
|
||||
congestionWindowGainConstant: 1.75,
|
||||
numStartupRtts: 2,
|
||||
drainToTarget: true,
|
||||
detectOvershooting: true,
|
||||
bytesLostMultiplier: 1,
|
||||
enableOverestimateAvoidance: true,
|
||||
reduceExtraAckedOnBandwidthIncrease: true,
|
||||
},
|
||||
{
|
||||
name: "aggressive",
|
||||
profile: ProfileAggressive,
|
||||
highGain: 3.0,
|
||||
highCwndGain: 2.25,
|
||||
congestionWindowGainConstant: 2.5,
|
||||
numStartupRtts: 4,
|
||||
bytesLostMultiplier: 2,
|
||||
enableAckAggregationDuringStartup: true,
|
||||
expireAckAggregationInStartup: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b := NewBbrSender(DefaultClock{}, congestion.InitialPacketSize, tc.profile)
|
||||
require.Equal(t, tc.profile, b.profile)
|
||||
require.Equal(t, tc.highGain, b.highGain)
|
||||
require.Equal(t, tc.highCwndGain, b.highCwndGain)
|
||||
require.Equal(t, tc.congestionWindowGainConstant, b.congestionWindowGainConstant)
|
||||
require.Equal(t, tc.numStartupRtts, b.numStartupRtts)
|
||||
require.Equal(t, tc.drainToTarget, b.drainToTarget)
|
||||
require.Equal(t, tc.detectOvershooting, b.detectOvershooting)
|
||||
require.Equal(t, tc.bytesLostMultiplier, b.bytesLostMultiplierWhileDetectingOvershooting)
|
||||
require.Equal(t, tc.enableAckAggregationDuringStartup, b.enableAckAggregationDuringStartup)
|
||||
require.Equal(t, tc.expireAckAggregationInStartup, b.expireAckAggregationInStartup)
|
||||
require.Equal(t, tc.enableOverestimateAvoidance, b.sampler.IsOverestimateAvoidanceEnabled())
|
||||
require.Equal(t, tc.reduceExtraAckedOnBandwidthIncrease, b.sampler.maxAckHeightTracker.reduceExtraAckedOnBandwidthIncrease)
|
||||
require.Equal(t, b.highGain, b.pacingGain)
|
||||
require.Equal(t, b.highCwndGain, b.congestionWindowGain)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProfile(t *testing.T) {
|
||||
profile, err := ParseProfile("")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ProfileStandard, profile)
|
||||
|
||||
profile, err = ParseProfile("Aggressive")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ProfileAggressive, profile)
|
||||
|
||||
_, err = ParseProfile("turbo")
|
||||
require.EqualError(t, err, `unsupported BBR profile "turbo"`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,55 @@
|
|||
package congestion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/internal/congestion/bbr"
|
||||
"github.com/apernet/hysteria/core/v2/internal/congestion/brutal"
|
||||
"github.com/apernet/quic-go"
|
||||
)
|
||||
|
||||
func UseBBR(conn *quic.Conn) {
|
||||
const (
|
||||
TypeBBR = "bbr"
|
||||
TypeReno = "reno"
|
||||
)
|
||||
|
||||
func NormalizeType(congestionType string) (string, error) {
|
||||
switch normalized := strings.ToLower(congestionType); normalized {
|
||||
case "", TypeBBR:
|
||||
return TypeBBR, nil
|
||||
case TypeReno:
|
||||
return TypeReno, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported congestion type %q", congestionType)
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeBBRProfile(profile string) (string, error) {
|
||||
normalized, err := bbr.ParseProfile(profile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(normalized), nil
|
||||
}
|
||||
|
||||
func UseBBR(conn *quic.Conn, profile bbr.Profile) {
|
||||
conn.SetCongestionControl(bbr.NewBbrSender(
|
||||
bbr.DefaultClock{},
|
||||
bbr.GetInitialPacketSize(conn.RemoteAddr()),
|
||||
profile,
|
||||
))
|
||||
}
|
||||
|
||||
func UseBrutal(conn *quic.Conn, tx uint64) {
|
||||
conn.SetCongestionControl(brutal.NewBrutalSender(tx))
|
||||
}
|
||||
|
||||
func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) {
|
||||
switch congestionType {
|
||||
case TypeReno:
|
||||
return
|
||||
default:
|
||||
UseBBR(conn, bbr.Profile(bbrProfile))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/apernet/hysteria/core/v2/errors"
|
||||
"github.com/apernet/hysteria/core/v2/internal/congestion"
|
||||
"github.com/apernet/hysteria/core/v2/internal/pmtud"
|
||||
"github.com/apernet/hysteria/core/v2/internal/utils"
|
||||
"github.com/apernet/quic-go"
|
||||
|
|
@ -28,6 +29,7 @@ type Config struct {
|
|||
Conn net.PacketConn
|
||||
RequestHook RequestHook
|
||||
Outbound Outbound
|
||||
CongestionConfig CongestionConfig
|
||||
BandwidthConfig BandwidthConfig
|
||||
IgnoreClientBandwidth bool
|
||||
DisableUDP bool
|
||||
|
|
@ -75,6 +77,17 @@ func (c *Config) fill() error {
|
|||
return errors.ConfigError{Field: "QUICConfig.MaxIncomingStreams", Reason: "must be at least 8"}
|
||||
}
|
||||
c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
|
||||
var err error
|
||||
c.CongestionConfig.Type, err = congestion.NormalizeType(c.CongestionConfig.Type)
|
||||
if err != nil {
|
||||
return errors.ConfigError{Field: "CongestionConfig.Type", Reason: err.Error()}
|
||||
}
|
||||
if c.CongestionConfig.Type == congestion.TypeBBR {
|
||||
c.CongestionConfig.BBRProfile, err = congestion.NormalizeBBRProfile(c.CongestionConfig.BBRProfile)
|
||||
if err != nil {
|
||||
return errors.ConfigError{Field: "CongestionConfig.BBRProfile", Reason: err.Error()}
|
||||
}
|
||||
}
|
||||
if c.Conn == nil {
|
||||
return errors.ConfigError{Field: "Conn", Reason: "must be set"}
|
||||
}
|
||||
|
|
@ -116,6 +129,11 @@ type QUICConfig struct {
|
|||
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
|
||||
}
|
||||
|
||||
type CongestionConfig struct {
|
||||
Type string
|
||||
BBRProfile string
|
||||
}
|
||||
|
||||
// RequestHook allows filtering and modifying requests before the server connects to the remote.
|
||||
// A request will only be hooked if Check returns true.
|
||||
// The returned byte slice, if not empty, will be sent to the remote before proxying - this is
|
||||
|
|
|
|||
|
|
@ -152,8 +152,8 @@ func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
h.authenticated = true
|
||||
h.authID = id
|
||||
if h.config.IgnoreClientBandwidth {
|
||||
// Ignore client bandwidth, always use BBR
|
||||
congestion.UseBBR(h.conn)
|
||||
// Ignore client bandwidth and use the configured congestion controller.
|
||||
congestion.UseConfigured(h.conn, h.config.CongestionConfig.Type, h.config.CongestionConfig.BBRProfile)
|
||||
actualTx = 0
|
||||
} else {
|
||||
// actualTx = min(serverTx, clientRx)
|
||||
|
|
@ -165,8 +165,8 @@ func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if actualTx > 0 {
|
||||
congestion.UseBrutal(h.conn, actualTx)
|
||||
} else {
|
||||
// Client doesn't know its own bandwidth, use BBR
|
||||
congestion.UseBBR(h.conn)
|
||||
// Client doesn't know its own bandwidth, use the configured congestion controller.
|
||||
congestion.UseConfigured(h.conn, h.config.CongestionConfig.Type, h.config.CongestionConfig.BBRProfile)
|
||||
}
|
||||
}
|
||||
// Auth OK, send response
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue