XHTTP transport: Add "bbr" (default) and "force-brutal" congestion control for H3 (#5711)

https://github.com/XTLS/Xray-core/pull/5711#issuecomment-3984037632
This commit is contained in:
Жора Змейкин
2026-03-07 16:46:40 +04:00
committed by GitHub
parent 0ac13bd910
commit 5138ffcf22
6 changed files with 195 additions and 45 deletions

View File

@@ -717,6 +717,11 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
return certificate, nil
}
type QuicParamsConfig struct {
Congestion string `json:"congestion"`
Up Bandwidth `json:"up"`
}
type TLSConfig struct {
AllowInsecure bool `json:"allowInsecure"`
Certs []*TLSCertConfig `json:"certificates"`
@@ -1414,8 +1419,9 @@ func (c *Mask) Build(tcp bool) (proto.Message, error) {
}
type FinalMask struct {
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
Tcp []Mask `json:"tcp"`
Udp []Mask `json:"udp"`
QuicParams *QuicParamsConfig `json:"quicParams"`
}
type StreamConfig struct {
@@ -1588,6 +1594,28 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
}
config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u))
}
if c.FinalMask.QuicParams != nil {
up, err := c.FinalMask.QuicParams.Up.Bps()
if err != nil {
return nil, err
}
if up > 0 && up < 65536 {
return nil, errors.New("Up must be at least 65536 bytes per second")
}
switch c.FinalMask.QuicParams.Congestion {
case "", "bbr", "reno":
case "force-brutal":
if up == 0 {
return nil, errors.New("force-brutal requires up")
}
default:
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: bbr, reno, force-brutal")
}
config.QuicParams = &internet.QuicParams{
Congestion: c.FinalMask.QuicParams.Congestion,
Up: up,
}
}
}
return config, nil

View File

@@ -206,7 +206,7 @@ func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber {
// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead.
func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{4, 0}
return file_transport_internet_config_proto_rawDescGZIP(), []int{5, 0}
}
type TransportConfig struct {
@@ -276,6 +276,7 @@ type StreamConfig struct {
SecuritySettings []*serial.TypedMessage `protobuf:"bytes,4,rep,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"`
Udpmasks []*serial.TypedMessage `protobuf:"bytes,10,rep,name=udpmasks,proto3" json:"udpmasks,omitempty"`
Tcpmasks []*serial.TypedMessage `protobuf:"bytes,11,rep,name=tcpmasks,proto3" json:"tcpmasks,omitempty"`
QuicParams *QuicParams `protobuf:"bytes,12,opt,name=quic_params,json=quicParams,proto3" json:"quic_params,omitempty"`
SocketSettings *SocketConfig `protobuf:"bytes,6,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -367,6 +368,13 @@ func (x *StreamConfig) GetTcpmasks() []*serial.TypedMessage {
return nil
}
func (x *StreamConfig) GetQuicParams() *QuicParams {
if x != nil {
return x.QuicParams
}
return nil
}
func (x *StreamConfig) GetSocketSettings() *SocketConfig {
if x != nil {
return x.SocketSettings
@@ -374,6 +382,58 @@ func (x *StreamConfig) GetSocketSettings() *SocketConfig {
return nil
}
type QuicParams struct {
state protoimpl.MessageState `protogen:"open.v1"`
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
Up uint64 `protobuf:"varint,2,opt,name=up,proto3" json:"up,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *QuicParams) Reset() {
*x = QuicParams{}
mi := &file_transport_internet_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *QuicParams) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QuicParams) ProtoMessage() {}
func (x *QuicParams) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use QuicParams.ProtoReflect.Descriptor instead.
func (*QuicParams) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{2}
}
func (x *QuicParams) GetCongestion() string {
if x != nil {
return x.Congestion
}
return ""
}
func (x *QuicParams) GetUp() uint64 {
if x != nil {
return x.Up
}
return 0
}
type ProxyConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
@@ -384,7 +444,7 @@ type ProxyConfig struct {
func (x *ProxyConfig) Reset() {
*x = ProxyConfig{}
mi := &file_transport_internet_config_proto_msgTypes[2]
mi := &file_transport_internet_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -396,7 +456,7 @@ func (x *ProxyConfig) String() string {
func (*ProxyConfig) ProtoMessage() {}
func (x *ProxyConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[2]
mi := &file_transport_internet_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -409,7 +469,7 @@ func (x *ProxyConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProxyConfig.ProtoReflect.Descriptor instead.
func (*ProxyConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{2}
return file_transport_internet_config_proto_rawDescGZIP(), []int{3}
}
func (x *ProxyConfig) GetTag() string {
@@ -440,7 +500,7 @@ type CustomSockopt struct {
func (x *CustomSockopt) Reset() {
*x = CustomSockopt{}
mi := &file_transport_internet_config_proto_msgTypes[3]
mi := &file_transport_internet_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -452,7 +512,7 @@ func (x *CustomSockopt) String() string {
func (*CustomSockopt) ProtoMessage() {}
func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[3]
mi := &file_transport_internet_config_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -465,7 +525,7 @@ func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
// Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead.
func (*CustomSockopt) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{3}
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
}
func (x *CustomSockopt) GetSystem() string {
@@ -547,7 +607,7 @@ type SocketConfig struct {
func (x *SocketConfig) Reset() {
*x = SocketConfig{}
mi := &file_transport_internet_config_proto_msgTypes[4]
mi := &file_transport_internet_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -559,7 +619,7 @@ func (x *SocketConfig) String() string {
func (*SocketConfig) ProtoMessage() {}
func (x *SocketConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[4]
mi := &file_transport_internet_config_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -572,7 +632,7 @@ func (x *SocketConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead.
func (*SocketConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
}
func (x *SocketConfig) GetMark() int32 {
@@ -748,7 +808,7 @@ type HappyEyeballsConfig struct {
func (x *HappyEyeballsConfig) Reset() {
*x = HappyEyeballsConfig{}
mi := &file_transport_internet_config_proto_msgTypes[5]
mi := &file_transport_internet_config_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -760,7 +820,7 @@ func (x *HappyEyeballsConfig) String() string {
func (*HappyEyeballsConfig) ProtoMessage() {}
func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_config_proto_msgTypes[5]
mi := &file_transport_internet_config_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -773,7 +833,7 @@ func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use HappyEyeballsConfig.ProtoReflect.Descriptor instead.
func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) {
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
return file_transport_internet_config_proto_rawDescGZIP(), []int{6}
}
func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool {
@@ -811,7 +871,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
"\x1ftransport/internet/config.proto\x12\x17xray.transport.internet\x1a!common/serial/typed_message.proto\x1a\x18common/net/address.proto\"t\n" +
"\x0fTransportConfig\x12#\n" +
"\rprotocol_name\x18\x03 \x01(\tR\fprotocolName\x12<\n" +
"\bsettings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\bsettings\"\x97\x04\n" +
"\bsettings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\bsettings\"\xdd\x04\n" +
"\fStreamConfig\x125\n" +
"\aaddress\x18\b \x01(\v2\x1b.xray.common.net.IPOrDomainR\aaddress\x12\x12\n" +
"\x04port\x18\t \x01(\rR\x04port\x12#\n" +
@@ -821,8 +881,16 @@ const file_transport_internet_config_proto_rawDesc = "" +
"\x11security_settings\x18\x04 \x03(\v2 .xray.common.serial.TypedMessageR\x10securitySettings\x12<\n" +
"\budpmasks\x18\n" +
" \x03(\v2 .xray.common.serial.TypedMessageR\budpmasks\x12<\n" +
"\btcpmasks\x18\v \x03(\v2 .xray.common.serial.TypedMessageR\btcpmasks\x12N\n" +
"\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"Q\n" +
"\btcpmasks\x18\v \x03(\v2 .xray.common.serial.TypedMessageR\btcpmasks\x12D\n" +
"\vquic_params\x18\f \x01(\v2#.xray.transport.internet.QuicParamsR\n" +
"quicParams\x12N\n" +
"\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"<\n" +
"\n" +
"QuicParams\x12\x1e\n" +
"\n" +
"congestion\x18\x01 \x01(\tR\n" +
"congestion\x12\x0e\n" +
"\x02up\x18\x02 \x01(\x04R\x02up\"Q\n" +
"\vProxyConfig\x12\x10\n" +
"\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" +
"\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" +
@@ -911,38 +979,40 @@ func file_transport_internet_config_proto_rawDescGZIP() []byte {
}
var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_transport_internet_config_proto_goTypes = []any{
(DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy
(AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy
(SocketConfig_TProxyMode)(0), // 2: xray.transport.internet.SocketConfig.TProxyMode
(*TransportConfig)(nil), // 3: xray.transport.internet.TransportConfig
(*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig
(*ProxyConfig)(nil), // 5: xray.transport.internet.ProxyConfig
(*CustomSockopt)(nil), // 6: xray.transport.internet.CustomSockopt
(*SocketConfig)(nil), // 7: xray.transport.internet.SocketConfig
(*HappyEyeballsConfig)(nil), // 8: xray.transport.internet.HappyEyeballsConfig
(*serial.TypedMessage)(nil), // 9: xray.common.serial.TypedMessage
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
(*QuicParams)(nil), // 5: xray.transport.internet.QuicParams
(*ProxyConfig)(nil), // 6: xray.transport.internet.ProxyConfig
(*CustomSockopt)(nil), // 7: xray.transport.internet.CustomSockopt
(*SocketConfig)(nil), // 8: xray.transport.internet.SocketConfig
(*HappyEyeballsConfig)(nil), // 9: xray.transport.internet.HappyEyeballsConfig
(*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage
(*net.IPOrDomain)(nil), // 11: xray.common.net.IPOrDomain
}
var file_transport_internet_config_proto_depIdxs = []int32{
9, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
10, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
10, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
11, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig
9, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
9, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
9, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
7, // 6: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
2, // 7: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
0, // 8: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
6, // 9: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
1, // 10: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
8, // 11: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
10, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
10, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
10, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
5, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams
8, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
2, // 8: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
0, // 9: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
7, // 10: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
1, // 11: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
9, // 12: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_transport_internet_config_proto_init() }
@@ -956,7 +1026,7 @@ func file_transport_internet_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)),
NumEnums: 3,
NumMessages: 6,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},

View File

@@ -59,9 +59,16 @@ message StreamConfig {
repeated xray.common.serial.TypedMessage udpmasks = 10;
repeated xray.common.serial.TypedMessage tcpmasks = 11;
QuicParams quic_params = 12;
SocketConfig socket_settings = 6;
}
message QuicParams {
string congestion = 1;
uint64 up = 2;
}
message ProxyConfig {
string tag = 1;
bool transportLayerProxy = 2;

View File

@@ -14,6 +14,7 @@ type MemoryStreamConfig struct {
SecuritySettings interface{}
TcpmaskManager *finalmask.TcpmaskManager
UdpmaskManager *finalmask.UdpmaskManager
QuicParams *QuicParams
SocketSettings *SocketConfig
DownloadSettings *MemoryStreamConfig
}
@@ -62,6 +63,10 @@ func ToMemoryStreamConfig(s *StreamConfig) (*MemoryStreamConfig, error) {
mss.TcpmaskManager = finalmask.NewTcpmaskManager(masks)
}
if s != nil && s.QuicParams != nil {
mss.QuicParams = s.QuicParams
}
if s != nil && len(s.Udpmasks) > 0 {
var masks []finalmask.Udpmask
for _, msg := range s.Udpmasks {

View File

@@ -22,6 +22,7 @@ import (
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
"github.com/xtls/xray-core/transport/internet/browser_dialer"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/stat"
@@ -195,7 +196,23 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
}
}
return quic.DialEarly(ctx, udpConn, udpAddr, tlsCfg, cfg)
quicConn, err := quic.DialEarly(ctx, udpConn, udpAddr, tlsCfg, cfg)
if err != nil {
return nil, err
}
if streamSettings.QuicParams != nil {
switch streamSettings.QuicParams.Congestion {
case "force-brutal":
congestion.UseBrutal(quicConn, streamSettings.QuicParams.Up)
case "reno":
// quic-go default, do nothing
default:
congestion.UseBBR(quicConn)
}
} else {
congestion.UseBBR(quicConn)
}
return quicConn, nil
},
}
} else if httpVersion == "2" {

View File

@@ -20,6 +20,7 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
http_proto "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/transport/internet"
@@ -474,8 +475,29 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
Handler: handler,
}
go func() {
if err := l.h3server.ServeListener(l.h3listener); err != nil {
errors.LogErrorInner(ctx, err, "failed to serve HTTP/3 for XHTTP/3")
for {
conn, err := l.h3listener.Accept(context.Background())
if err != nil {
errors.LogInfoInner(ctx, err, "XHTTP/3 listener closed")
return
}
if streamSettings.QuicParams != nil {
switch streamSettings.QuicParams.Congestion {
case "force-brutal":
congestion.UseBrutal(conn, streamSettings.QuicParams.Up)
case "reno":
// quic-go default, do nothing
default:
congestion.UseBBR(conn)
}
} else {
congestion.UseBBR(conn)
}
go func() {
if err := l.h3server.ServeQUICConn(conn); err != nil {
errors.LogDebugInner(ctx, err, "XHTTP/3 connection ended")
}
}()
}
}()
} else { // tcp
@@ -539,6 +561,7 @@ func (ln *Listener) Close() error {
if err := ln.h3server.Close(); err != nil {
return err
}
return ln.h3listener.Close()
} else if ln.listener != nil {
return ln.listener.Close()
}