diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 31701fe0aa36..2c5650b0daee 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "math" + "math/big" "net/netip" "net/url" "os" @@ -219,8 +220,10 @@ type SplitHTTPConfig struct { XPaddingPlacement string `json:"xPaddingPlacement"` XPaddingMethod string `json:"xPaddingMethod"` UplinkHTTPMethod string `json:"uplinkHTTPMethod"` - SessionPlacement string `json:"sessionPlacement"` - SessionKey string `json:"sessionKey"` + SessionIDPlacement string `json:"sessionIDPlacement"` + SessionIDKey string `json:"sessionIDKey"` + SessionIDTable string `json:"sessionIDTable"` + SessionIDLength Int32Range `json:"sessionIDLength"` SeqPlacement string `json:"seqPlacement"` SeqKey string `json:"seqKey"` UplinkDataPlacement string `json:"uplinkDataPlacement"` @@ -331,12 +334,12 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") } - switch c.SessionPlacement { + switch c.SessionIDPlacement { case "": - c.SessionPlacement = "path" + c.SessionIDPlacement = "path" case "path", "cookie", "header", "query": default: - return nil, errors.New("unsupported session placement: " + c.SessionPlacement) + return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement) } switch c.SeqPlacement { @@ -347,12 +350,31 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) } - if c.SessionPlacement != "path" && c.SessionKey == "" { - switch c.SessionPlacement { + if c.SessionIDPlacement != "path" && c.SessionIDKey == "" { + switch c.SessionIDPlacement { case "cookie", "query": - c.SessionKey = "x_session" + c.SessionIDKey = "x_session" case "header": - c.SessionKey = "X-Session" + c.SessionIDKey = "X-Session" + } + } + + if c.SessionIDTable != "" { + if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok { + c.SessionIDTable = predefined + } + room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To) + // 2.1B possiblities should be enough + if room.Cmp(big.NewInt(2<<30)) < 0 { + return nil, errors.New("sessionIDTable or sessionIDLength is too small") + } + if c.SessionIDLength.From <= 0 { + return nil, errors.New("sessionIDLength.from must be greater than 0") + } + for i := 0; i < len(c.SessionIDTable); i++ { + if c.SessionIDTable[i] >= 0x80 { + return nil, errors.New("sessionIDTable must contain only ASCII characters") + } } } @@ -402,9 +424,9 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { XPaddingPlacement: c.XPaddingPlacement, XPaddingMethod: c.XPaddingMethod, UplinkHTTPMethod: c.UplinkHTTPMethod, - SessionPlacement: c.SessionPlacement, + SessionIDPlacement: c.SessionIDPlacement, SeqPlacement: c.SeqPlacement, - SessionKey: c.SessionKey, + SessionIDKey: c.SessionIDKey, SeqKey: c.SeqKey, UplinkDataPlacement: c.UplinkDataPlacement, UplinkDataKey: c.UplinkDataKey, @@ -416,6 +438,8 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { ScMaxBufferedPosts: c.ScMaxBufferedPosts, ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs), ServerMaxHeaderBytes: c.ServerMaxHeaderBytes, + SessionIDTable: c.SessionIDTable, + SessionIDLength: newRangeConfig(c.SessionIDLength), Xmux: &splithttp.XmuxConfig{ MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConnections: newRangeConfig(c.Xmux.MaxConnections), @@ -439,6 +463,17 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return config, nil } +func roomSize(tableSize int, min, max int32) *big.Int { + base := big.NewInt(int64(tableSize)) + sum := new(big.Int) + term := new(big.Int) + for k := min; k <= max; k++ { + term.Exp(base, big.NewInt(int64(k)), nil) + sum.Add(sum, term) + } + return sum +} + const ( Byte = 1 Kilobyte = 1024 * Byte diff --git a/transport/internet/splithttp/config.go b/transport/internet/splithttp/config.go index 61f861a33c77..9acf5e4ff349 100644 --- a/transport/internet/splithttp/config.go +++ b/transport/internet/splithttp/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "io" + "math/rand/v2" "net/http" "strings" @@ -11,6 +12,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/transport/internet" ) @@ -131,26 +133,26 @@ func (c *Config) GetNormalizedUplinkHTTPMethod() string { return c.UplinkHTTPMethod } -func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig { +func (c *Config) GetNormalizedScMaxEachPostBytes() *RangeConfig { if c.ScMaxEachPostBytes == nil || c.ScMaxEachPostBytes.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 1000000, To: 1000000, } } - return *c.ScMaxEachPostBytes + return c.ScMaxEachPostBytes } -func (c *Config) GetNormalizedScMinPostsIntervalMs() RangeConfig { +func (c *Config) GetNormalizedScMinPostsIntervalMs() *RangeConfig { if c.ScMinPostsIntervalMs == nil || c.ScMinPostsIntervalMs.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 30, To: 30, } } - return *c.ScMinPostsIntervalMs + return c.ScMinPostsIntervalMs } func (c *Config) GetNormalizedScMaxBufferedPosts() int { @@ -161,27 +163,27 @@ func (c *Config) GetNormalizedScMaxBufferedPosts() int { return int(c.ScMaxBufferedPosts) } -func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig { +func (c *Config) GetNormalizedScStreamUpServerSecs() *RangeConfig { if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 20, To: 80, } } - return *c.ScStreamUpServerSecs + return c.ScStreamUpServerSecs } -func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { +func (c *Config) GetNormalizedUplinkChunkSize() *RangeConfig { if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 { switch c.UplinkDataPlacement { case PlacementCookie: - return RangeConfig{ + return &RangeConfig{ From: 2 * 1024, // 2 KiB To: 3 * 1024, // 3 KiB } case PlacementHeader: - return RangeConfig{ + return &RangeConfig{ From: 3 * 1000, // 3 KB To: 4 * 1000, // 4 KB } @@ -189,13 +191,13 @@ func (c *Config) GetNormalizedUplinkChunkSize() RangeConfig { return c.GetNormalizedScMaxEachPostBytes() } } else if c.UplinkChunkSize.From < 64 { - return RangeConfig{ + return &RangeConfig{ From: 64, To: max(64, c.UplinkChunkSize.To), } } - return *c.UplinkChunkSize + return c.UplinkChunkSize } func (c *Config) GetNormalizedServerMaxHeaderBytes() int { @@ -207,10 +209,10 @@ func (c *Config) GetNormalizedServerMaxHeaderBytes() int { } func (c *Config) GetNormalizedSessionPlacement() string { - if c.SessionPlacement == "" { + if c.SessionIDPlacement == "" { return PlacementPath } - return c.SessionPlacement + return c.SessionIDPlacement } func (c *Config) GetNormalizedSeqPlacement() string { @@ -228,8 +230,8 @@ func (c *Config) GetNormalizedUplinkDataPlacement() string { } func (c *Config) GetNormalizedSessionKey() string { - if c.SessionKey != "" { - return c.SessionKey + if c.SessionIDKey != "" { + return c.SessionIDKey } switch c.GetNormalizedSessionPlacement() { case PlacementHeader: @@ -417,59 +419,59 @@ func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (session return sessionId, seqStr } -func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { +func (m *XmuxConfig) GetNormalizedMaxConcurrency() *RangeConfig { if m.MaxConcurrency == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.MaxConcurrency + return m.MaxConcurrency } -func (m *XmuxConfig) GetNormalizedMaxConnections() RangeConfig { +func (m *XmuxConfig) GetNormalizedMaxConnections() *RangeConfig { if m.MaxConnections == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.MaxConnections + return m.MaxConnections } -func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() RangeConfig { +func (m *XmuxConfig) GetNormalizedCMaxReuseTimes() *RangeConfig { if m.CMaxReuseTimes == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.CMaxReuseTimes + return m.CMaxReuseTimes } -func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() RangeConfig { +func (m *XmuxConfig) GetNormalizedHMaxRequestTimes() *RangeConfig { if m.HMaxRequestTimes == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.HMaxRequestTimes + return m.HMaxRequestTimes } -func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() RangeConfig { +func (m *XmuxConfig) GetNormalizedHMaxReusableSecs() *RangeConfig { if m.HMaxReusableSecs == nil { - return RangeConfig{ + return &RangeConfig{ From: 0, To: 0, } } - return *m.HMaxReusableSecs + return m.HMaxReusableSecs } func init() { @@ -478,10 +480,44 @@ func init() { })) } -func (c RangeConfig) rand() int32 { +func (c *RangeConfig) rand() int32 { + if c == nil { + return 0 + } return int32(crypto.RandBetween(int64(c.From), int64(c.To))) } +// predefined +var PredefinedTable = map[string]string{ + "ALPHABET": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "BASE36": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Base62": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "HEX": "0123456789ABCDEF", + "alphabet": "abcdefghijklmnopqrstuvwxyz", + "base36": "0123456789abcdefghijklmnopqrstuvwxyz", + "hex": "0123456789abcdef", + "number": "0123456789", +} + +func (c *Config) GenerateSessionID() string { + length := c.SessionIDLength.rand() + table := c.SessionIDTable + if predefined, ok := PredefinedTable[table]; ok { + table = predefined + } + if table != "" && length > 0 { + id := make([]byte, length) + for i := range id { + id[i] = table[rand.N(len(table))] + } + return string(id) + } else { + uuid := uuid.New() + return uuid.String() + } +} + func appendToPath(path, value string) string { if strings.HasSuffix(path, "/") { return path + value diff --git a/transport/internet/splithttp/config.pb.go b/transport/internet/splithttp/config.pb.go index 4e99d8a8c42a..c7dfe4ce8ca9 100644 --- a/transport/internet/splithttp/config.pb.go +++ b/transport/internet/splithttp/config.pb.go @@ -179,14 +179,16 @@ type Config struct { XPaddingPlacement string `protobuf:"bytes,17,opt,name=xPaddingPlacement,proto3" json:"xPaddingPlacement,omitempty"` XPaddingMethod string `protobuf:"bytes,18,opt,name=xPaddingMethod,proto3" json:"xPaddingMethod,omitempty"` UplinkHTTPMethod string `protobuf:"bytes,19,opt,name=uplinkHTTPMethod,proto3" json:"uplinkHTTPMethod,omitempty"` - SessionPlacement string `protobuf:"bytes,20,opt,name=sessionPlacement,proto3" json:"sessionPlacement,omitempty"` - SessionKey string `protobuf:"bytes,21,opt,name=sessionKey,proto3" json:"sessionKey,omitempty"` + SessionIDPlacement string `protobuf:"bytes,20,opt,name=sessionIDPlacement,proto3" json:"sessionIDPlacement,omitempty"` + SessionIDKey string `protobuf:"bytes,21,opt,name=sessionIDKey,proto3" json:"sessionIDKey,omitempty"` SeqPlacement string `protobuf:"bytes,22,opt,name=seqPlacement,proto3" json:"seqPlacement,omitempty"` SeqKey string `protobuf:"bytes,23,opt,name=seqKey,proto3" json:"seqKey,omitempty"` UplinkDataPlacement string `protobuf:"bytes,24,opt,name=uplinkDataPlacement,proto3" json:"uplinkDataPlacement,omitempty"` UplinkDataKey string `protobuf:"bytes,25,opt,name=uplinkDataKey,proto3" json:"uplinkDataKey,omitempty"` UplinkChunkSize *RangeConfig `protobuf:"bytes,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"` ServerMaxHeaderBytes int32 `protobuf:"varint,27,opt,name=serverMaxHeaderBytes,proto3" json:"serverMaxHeaderBytes,omitempty"` + SessionIDTable string `protobuf:"bytes,28,opt,name=sessionIDTable,proto3" json:"sessionIDTable,omitempty"` + SessionIDLength *RangeConfig `protobuf:"bytes,29,opt,name=sessionIDLength,proto3" json:"sessionIDLength,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -354,16 +356,16 @@ func (x *Config) GetUplinkHTTPMethod() string { return "" } -func (x *Config) GetSessionPlacement() string { +func (x *Config) GetSessionIDPlacement() string { if x != nil { - return x.SessionPlacement + return x.SessionIDPlacement } return "" } -func (x *Config) GetSessionKey() string { +func (x *Config) GetSessionIDKey() string { if x != nil { - return x.SessionKey + return x.SessionIDKey } return "" } @@ -410,6 +412,20 @@ func (x *Config) GetServerMaxHeaderBytes() int32 { return 0 } +func (x *Config) GetSessionIDTable() string { + if x != nil { + return x.SessionIDTable + } + return "" +} + +func (x *Config) GetSessionIDLength() *RangeConfig { + if x != nil { + return x.SessionIDLength + } + return nil +} + var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor const file_transport_internet_splithttp_config_proto_rawDesc = "" + @@ -425,7 +441,7 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" + "\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" + "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" + - "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xc2\v\n" + + "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xcc\f\n" + "\x06Config\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + @@ -446,17 +462,17 @@ const file_transport_internet_splithttp_config_proto_rawDesc = "" + "\x0exPaddingHeader\x18\x10 \x01(\tR\x0exPaddingHeader\x12,\n" + "\x11xPaddingPlacement\x18\x11 \x01(\tR\x11xPaddingPlacement\x12&\n" + "\x0exPaddingMethod\x18\x12 \x01(\tR\x0exPaddingMethod\x12*\n" + - "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12*\n" + - "\x10sessionPlacement\x18\x14 \x01(\tR\x10sessionPlacement\x12\x1e\n" + - "\n" + - "sessionKey\x18\x15 \x01(\tR\n" + - "sessionKey\x12\"\n" + + "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12.\n" + + "\x12sessionIDPlacement\x18\x14 \x01(\tR\x12sessionIDPlacement\x12\"\n" + + "\fsessionIDKey\x18\x15 \x01(\tR\fsessionIDKey\x12\"\n" + "\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" + "\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" + "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" + "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12X\n" + "\x0fuplinkChunkSize\x18\x1a \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fuplinkChunkSize\x122\n" + - "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x1a:\n" + + "\x14serverMaxHeaderBytes\x18\x1b \x01(\x05R\x14serverMaxHeaderBytes\x12&\n" + + "\x0esessionIDTable\x18\x1c \x01(\tR\x0esessionIDTable\x12X\n" + + "\x0fsessionIDLength\x18\x1d \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0fsessionIDLength\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" + @@ -496,11 +512,12 @@ var file_transport_internet_splithttp_config_proto_depIdxs = []int32{ 1, // 10: xray.transport.internet.splithttp.Config.xmux:type_name -> xray.transport.internet.splithttp.XmuxConfig 4, // 11: xray.transport.internet.splithttp.Config.downloadSettings:type_name -> xray.transport.internet.StreamConfig 0, // 12: xray.transport.internet.splithttp.Config.uplinkChunkSize:type_name -> xray.transport.internet.splithttp.RangeConfig - 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 + 0, // 13: xray.transport.internet.splithttp.Config.sessionIDLength:type_name -> xray.transport.internet.splithttp.RangeConfig + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_transport_internet_splithttp_config_proto_init() } diff --git a/transport/internet/splithttp/config.proto b/transport/internet/splithttp/config.proto index 4c303d293222..efe6de03f28d 100644 --- a/transport/internet/splithttp/config.proto +++ b/transport/internet/splithttp/config.proto @@ -42,12 +42,14 @@ message Config { string xPaddingPlacement = 17; string xPaddingMethod = 18; string uplinkHTTPMethod = 19; - string sessionPlacement = 20; - string sessionKey = 21; + string sessionIDPlacement = 20; + string sessionIDKey = 21; string seqPlacement = 22; string seqKey = 23; string uplinkDataPlacement = 24; string uplinkDataKey = 25; RangeConfig uplinkChunkSize = 26; int32 serverMaxHeaderBytes = 27; + string sessionIDTable = 28; + RangeConfig sessionIDLength = 29; } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 1329713c5f81..5092a9dbfa36 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -24,7 +24,6 @@ import ( "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net/cnc" "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/browser_dialer" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" @@ -376,8 +375,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me sessionId := "" if mode != "stream-one" { - sessionIdUuid := uuid.New() - sessionId = sessionIdUuid.String() + sessionId = transportConfiguration.GenerateSessionID() } errors.LogInfo(ctx, fmt.Sprintf("XHTTP is dialing to %s, mode %s, HTTP version %s, host %s", dest, mode, httpVersion, requestURL.Host)) diff --git a/transport/internet/splithttp/xpadding.go b/transport/internet/splithttp/xpadding.go index 1dcbc3e2f05b..f2f99340bc98 100644 --- a/transport/internet/splithttp/xpadding.go +++ b/transport/internet/splithttp/xpadding.go @@ -176,15 +176,15 @@ func ApplyPaddingToQuery(u *url.URL, key, value string) { u.RawQuery = q.Encode() } -func (c *Config) GetNormalizedXPaddingBytes() RangeConfig { +func (c *Config) GetNormalizedXPaddingBytes() *RangeConfig { if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { - return RangeConfig{ + return &RangeConfig{ From: 100, To: 1000, } } - return *c.XPaddingBytes + return c.XPaddingBytes } func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) {