Merge pull request #1534 from apernet/feat/bbr-profiles

feat: add configurable congestion modes and BBR profiles
This commit is contained in:
Toby 2026-03-29 10:08:02 -07:00 committed by GitHub
commit 8059de329c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 475 additions and 36 deletions

View file

@ -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,
}

View file

@ -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
}

View file

@ -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
View 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)
}
}

View file

@ -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,

View file

@ -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"`)
})
}

View file

@ -43,6 +43,10 @@ quic:
maxIncomingStreams: 256
disablePathMTUDiscovery: true
congestion:
type: reno
bbrProfile: aggressive
bandwidth:
up: 500 mbps
down: 100 mbps

View file

@ -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()

View file

@ -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

View file

@ -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
}

View file

@ -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"`)
}

View file

@ -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))
}
}

View file

@ -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

View file

@ -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