diff --git a/app/cmd/client.go b/app/cmd/client.go index 7fc050f..aa91b83 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -59,23 +59,24 @@ func initClientFlags() { } type clientConfig struct { - Server string `mapstructure:"server"` - Auth string `mapstructure:"auth"` - Transport clientConfigTransport `mapstructure:"transport"` - Obfs clientConfigObfs `mapstructure:"obfs"` - TLS clientConfigTLS `mapstructure:"tls"` - QUIC clientConfigQUIC `mapstructure:"quic"` - Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` - FastOpen bool `mapstructure:"fastOpen"` - Lazy bool `mapstructure:"lazy"` - SOCKS5 *socks5Config `mapstructure:"socks5"` - HTTP *httpConfig `mapstructure:"http"` - TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` - UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` - TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` - UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` - TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"` - TUN *tunConfig `mapstructure:"tun"` + Server string `mapstructure:"server"` + Auth string `mapstructure:"auth"` + Transport clientConfigTransport `mapstructure:"transport"` + 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"` + SOCKS5 *socks5Config `mapstructure:"socks5"` + HTTP *httpConfig `mapstructure:"http"` + TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` + UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` + TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` + UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` + TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"` + TUN *tunConfig `mapstructure:"tun"` } type clientConfigTransportUDP struct { @@ -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, } diff --git a/app/cmd/client_test.go b/app/cmd/client_test.go index 56d1067..db1ca4f 100644 --- a/app/cmd/client_test.go +++ b/app/cmd/client_test.go @@ -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 } diff --git a/app/cmd/client_test.yaml b/app/cmd/client_test.yaml index eda3f80..37bcf02 100644 --- a/app/cmd/client_test.yaml +++ b/app/cmd/client_test.yaml @@ -33,6 +33,10 @@ quic: fwmark: 1234 fdControlUnixSocket: test.sock +congestion: + type: bbr + bbrProfile: aggressive + bandwidth: up: 200 mbps down: 1 gbps diff --git a/app/cmd/congestion.go b/app/cmd/congestion.go new file mode 100644 index 0000000..4db6237 --- /dev/null +++ b/app/cmd/congestion.go @@ -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) + } +} diff --git a/app/cmd/server.go b/app/cmd/server.go index 79417b1..8a05947 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -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, diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index c610c7b..2be6ade 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -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"`) + }) +} diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index e04ad48..e38edf3 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -43,6 +43,10 @@ quic: maxIncomingStreams: 256 disablePathMTUDiscovery: true +congestion: + type: reno + bbrProfile: aggressive + bandwidth: up: 500 mbps down: 100 mbps diff --git a/core/client/client.go b/core/client/client.go index 5302ca7..8224420 100644 --- a/core/client/client.go +++ b/core/client/client.go @@ -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() diff --git a/core/client/config.go b/core/client/config.go index 2f864d9..5c0d4be 100644 --- a/core/client/config.go +++ b/core/client/config.go @@ -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" ) @@ -18,13 +19,14 @@ const ( ) type Config struct { - ConnFactory ConnFactory - ServerAddr net.Addr - Auth string - TLSConfig TLSConfig - QUICConfig QUICConfig - BandwidthConfig BandwidthConfig - FastOpen bool + ConnFactory ConnFactory + ServerAddr net.Addr + Auth string + TLSConfig TLSConfig + QUICConfig QUICConfig + CongestionConfig CongestionConfig + BandwidthConfig BandwidthConfig + FastOpen bool filled bool // whether the fields have been verified and filled } @@ -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 diff --git a/core/internal/congestion/bbr/bbr_sender.go b/core/internal/congestion/bbr/bbr_sender.go index dc32ee3..636dc5e 100644 --- a/core/internal/congestion/bbr/bbr_sender.go +++ b/core/internal/congestion/bbr/bbr_sender.go @@ -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 } diff --git a/core/internal/congestion/bbr/bbr_sender_test.go b/core/internal/congestion/bbr/bbr_sender_test.go index 41dad12..5aff552 100644 --- a/core/internal/congestion/bbr/bbr_sender_test.go +++ b/core/internal/congestion/bbr/bbr_sender_test.go @@ -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"`) +} diff --git a/core/internal/congestion/utils.go b/core/internal/congestion/utils.go index 5337d11..4b5673b 100644 --- a/core/internal/congestion/utils.go +++ b/core/internal/congestion/utils.go @@ -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)) + } +} diff --git a/core/server/config.go b/core/server/config.go index 9873318..1dde39f 100644 --- a/core/server/config.go +++ b/core/server/config.go @@ -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 diff --git a/core/server/server.go b/core/server/server.go index d8e6afc..4821717 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -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