diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index 9557c3252519..c0c1af265058 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "strings" "time" utls "github.com/refraction-networking/utls" @@ -20,6 +19,7 @@ import ( "github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/session" + "github.com/xtls/xray-core/common/utils" dns_feature "github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/transport/internet" @@ -214,8 +214,7 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, req.Header.Add("Accept", "application/dns-message") req.Header.Add("Content-Type", "application/dns-message") - - req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000)))) + req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000))) hc := s.httpClient diff --git a/common/utils/padding.go b/common/utils/padding.go new file mode 100644 index 000000000000..fe95ba9a1353 --- /dev/null +++ b/common/utils/padding.go @@ -0,0 +1,24 @@ +package utils + +import ( + "math/rand/v2" +) + +var ( + // 8 รท (397/62) + h2packCorrectionFactor = 1.2493702770780857 + base62TotalCharsNum = 62 + base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + +// H2Base62Pad generates a base62 padding string for HTTP/2 header +// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding +func H2Base62Pad[T int32 | int64 | int](expectedLen T) string { + actualLenFloat := float64(expectedLen) * h2packCorrectionFactor + actualLen := int(actualLenFloat) + result := make([]byte, actualLen) + for i := range actualLen { + result[i] = base62Chars[rand.N(base62TotalCharsNum)] + } + return string(result) +} diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index c23ed8dca9f7..b7a18afa91e1 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -228,6 +228,19 @@ type SplitHTTPConfig struct { Mode string `json:"mode"` Headers map[string]string `json:"headers"` XPaddingBytes Int32Range `json:"xPaddingBytes"` + XPaddingObfsMode bool `json:"xPaddingObfsMode"` + XPaddingKey string `json:"xPaddingKey"` + XPaddingHeader string `json:"xPaddingHeader"` + XPaddingPlacement string `json:"xPaddingPlacement"` + XPaddingMethod string `json:"xPaddingMethod"` + UplinkHTTPMethod string `json:"uplinkHTTPMethod"` + SessionPlacement string `json:"sessionPlacement"` + SessionKey string `json:"sessionKey"` + SeqPlacement string `json:"seqPlacement"` + SeqKey string `json:"seqKey"` + UplinkDataPlacement string `json:"uplinkDataPlacement"` + UplinkDataKey string `json:"uplinkDataKey"` + UplinkChunkSize uint32 `json:"uplinkChunkSize"` NoGRPCHeader bool `json:"noGRPCHeader"` NoSSEHeader bool `json:"noSSEHeader"` ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` @@ -287,6 +300,107 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { return nil, errors.New("xPaddingBytes cannot be disabled") } + if c.XPaddingKey == "" { + c.XPaddingKey = "x_padding" + } + + if c.XPaddingHeader == "" { + c.XPaddingHeader = "X-Padding" + } + + switch c.XPaddingPlacement { + case "": + c.XPaddingPlacement = "queryInHeader" + case "cookie", "header", "query", "queryInHeader": + default: + return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement) + } + + switch c.XPaddingMethod { + case "": + c.XPaddingMethod = "repeat-x" + case "repeat-x", "tokenish": + default: + return nil, errors.New("unsupported padding method: " + c.XPaddingMethod) + } + + switch c.UplinkDataPlacement { + case "": + c.UplinkDataPlacement = "body" + case "cookie", "header": + if c.Mode != "packet-up" { + return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode") + } + default: + return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement) + } + + if c.UplinkHTTPMethod == "" { + c.UplinkHTTPMethod = "POST" + } + c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod) + + if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" { + return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode") + } + + switch c.SessionPlacement { + case "": + c.SessionPlacement = "path" + case "cookie", "header", "query": + default: + return nil, errors.New("unsupported session placement: " + c.SessionPlacement) + } + + switch c.SeqPlacement { + case "": + c.SeqPlacement = "path" + case "cookie", "header", "query": + if c.SessionPlacement == "path" { + return nil, errors.New("SeqPlacement must be path when SessionPlacement is path") + } + default: + return nil, errors.New("unsupported seq placement: " + c.SeqPlacement) + } + + if c.SessionPlacement != "path" && c.SessionKey == "" { + switch c.SessionPlacement { + case "cookie", "query": + c.SessionKey = "x_session" + case "header": + c.SessionKey = "X-Session" + } + } + + if c.SeqPlacement != "path" && c.SeqKey == "" { + switch c.SeqPlacement { + case "cookie", "query": + c.SeqKey = "x_seq" + case "header": + c.SeqKey = "X-Seq" + } + } + + if c.UplinkDataPlacement != "body" && c.UplinkDataKey == "" { + switch c.UplinkDataPlacement { + case "cookie": + c.UplinkDataKey = "x_data" + case "header": + c.UplinkDataKey = "X-Data" + } + } + + if c.UplinkChunkSize == 0 { + switch c.UplinkDataPlacement { + case "cookie": + c.UplinkChunkSize = 3 * 1024 // 3KB + case "header": + c.UplinkChunkSize = 4 * 1024 // 4KB + } + } else if c.UplinkChunkSize < 64 { + c.UplinkChunkSize = 64 + } + if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") } @@ -305,6 +419,19 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) { Mode: c.Mode, Headers: c.Headers, XPaddingBytes: newRangeConfig(c.XPaddingBytes), + XPaddingObfsMode: c.XPaddingObfsMode, + XPaddingKey: c.XPaddingKey, + XPaddingHeader: c.XPaddingHeader, + XPaddingPlacement: c.XPaddingPlacement, + XPaddingMethod: c.XPaddingMethod, + UplinkHTTPMethod: c.UplinkHTTPMethod, + SessionPlacement: c.SessionPlacement, + SeqPlacement: c.SeqPlacement, + SessionKey: c.SessionKey, + SeqKey: c.SeqKey, + UplinkDataPlacement: c.UplinkDataPlacement, + UplinkDataKey: c.UplinkDataKey, + UplinkChunkSize: c.UplinkChunkSize, NoGRPCHeader: c.NoGRPCHeader, NoSSEHeader: c.NoSSEHeader, ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), diff --git a/transport/internet/splithttp/browser_client.go b/transport/internet/splithttp/browser_client.go index 6e0a5302a57b..f39f94ba680c 100644 --- a/transport/internet/splithttp/browser_client.go +++ b/transport/internet/splithttp/browser_client.go @@ -19,12 +19,35 @@ func (c *BrowserDialerClient) IsClosed() bool { panic("not implemented yet") } -func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) { +func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, _ string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) { if body != nil { return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet") } - conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader(url)) + header := c.transportConfig.GetRequestHeader() + length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.transportConfig.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.transportConfig.XPaddingPlacement, + Key: c.transportConfig.XPaddingKey, + Header: c.transportConfig.XPaddingHeader, + RawURL: url, + } + config.Method = PaddingMethod(c.transportConfig.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: url, + } + } + + c.transportConfig.ApplyXPaddingToHeader(header, config) + + conn, err := browser_dialer.DialGet(url, header) dummyAddr := &net.IPAddr{} if err != nil { return nil, dummyAddr, dummyAddr, err @@ -33,13 +56,36 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i return websocket.NewConnection(conn, dummyAddr, nil, 0), conn.RemoteAddr(), conn.LocalAddr(), nil } -func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error { +func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, _ string, _ string, body io.Reader, contentLength int64) error { bytes, err := io.ReadAll(body) if err != nil { return err } - err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(url), bytes) + header := c.transportConfig.GetRequestHeader() + length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.transportConfig.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.transportConfig.XPaddingPlacement, + Key: c.transportConfig.XPaddingKey, + Header: c.transportConfig.XPaddingHeader, + RawURL: url, + } + config.Method = PaddingMethod(c.transportConfig.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: url, + } + } + + c.transportConfig.ApplyXPaddingToHeader(header, config) + + err = browser_dialer.DialPost(url, header, bytes) if err != nil { return err } diff --git a/transport/internet/splithttp/client.go b/transport/internet/splithttp/client.go index 3eeab6eb4399..e26aea730a9b 100644 --- a/transport/internet/splithttp/client.go +++ b/transport/internet/splithttp/client.go @@ -3,6 +3,7 @@ package splithttp import ( "bytes" "context" + "encoding/base64" "fmt" "io" "net/http" @@ -19,11 +20,11 @@ import ( type DialerClient interface { IsClosed() bool - // ctx, url, body, uploadOnly - OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) + // ctx, url, sessionId, body, uploadOnly + OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) - // ctx, url, body, contentLength - PostPacket(context.Context, string, io.Reader, int64) error + // ctx, url, sessionId, seqStr, body, contentLength + PostPacket(context.Context, string, string, string, io.Reader, int64) error } // implements splithttp.DialerClient in terms of direct network connections @@ -41,7 +42,7 @@ func (c *DefaultDialerClient) IsClosed() bool { return c.closed } -func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { +func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) { // this is done when the TCP/UDP connection to the server was established, // and we can unblock the Dial function and print correct net addresses in // logs @@ -56,11 +57,34 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i method := "GET" // stream-down if body != nil { - method = "POST" // stream-up/one + method = c.transportConfig.GetNormalizedUplinkHTTPMethod() // stream-up/one } req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) - req.Header = c.transportConfig.GetRequestHeader(url) - if method == "POST" && !c.transportConfig.NoGRPCHeader { + req.Header = c.transportConfig.GetRequestHeader() + length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.transportConfig.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.transportConfig.XPaddingPlacement, + Key: c.transportConfig.XPaddingKey, + Header: c.transportConfig.XPaddingHeader, + RawURL: url, + } + config.Method = PaddingMethod(c.transportConfig.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: url, + } + } + + c.transportConfig.ApplyXPaddingToRequest(req, config) + c.transportConfig.ApplyMetaToRequest(req, sessionId, "") + + if method == c.transportConfig.GetNormalizedUplinkHTTPMethod() && !c.transportConfig.NoGRPCHeader { req.Header.Set("Content-Type", "application/grpc") } @@ -92,13 +116,83 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i return } -func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error { - req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body) +func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error { + var encodedData string + dataPlacement := c.transportConfig.GetNormalizedUplinkDataPlacement() + + if dataPlacement != PlacementBody { + data, err := io.ReadAll(body) + if err != nil { + return err + } + encodedData = base64.RawURLEncoding.EncodeToString(data) + body = nil + contentLength = 0 + } + + method := c.transportConfig.GetNormalizedUplinkHTTPMethod() + req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) if err != nil { return err } req.ContentLength = contentLength - req.Header = c.transportConfig.GetRequestHeader(url) + req.Header = c.transportConfig.GetRequestHeader() + + if dataPlacement != PlacementBody { + key := c.transportConfig.UplinkDataKey + chunkSize := int(c.transportConfig.UplinkChunkSize) + + switch dataPlacement { + case PlacementHeader: + for i := 0; i < len(encodedData); i += chunkSize { + end := i + chunkSize + if end > len(encodedData) { + end = len(encodedData) + } + chunk := encodedData[i:end] + headerKey := fmt.Sprintf("%s-%d", key, i/chunkSize) + req.Header.Set(headerKey, chunk) + } + + req.Header.Set(key+"-Length", fmt.Sprintf("%d", len(encodedData))) + req.Header.Set(key+"-Upstream", "1") + case PlacementCookie: + for i := 0; i < len(encodedData); i += chunkSize { + end := i + chunkSize + if end > len(encodedData) { + end = len(encodedData) + } + chunk := encodedData[i:end] + cookieName := fmt.Sprintf("%s_%d", key, i/chunkSize) + req.AddCookie(&http.Cookie{Name: cookieName, Value: chunk}) + } + + req.AddCookie(&http.Cookie{Name: key + "_upstream", Value: "1"}) + } + } + + length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if c.transportConfig.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: c.transportConfig.XPaddingPlacement, + Key: c.transportConfig.XPaddingKey, + Header: c.transportConfig.XPaddingHeader, + RawURL: url, + } + config.Method = PaddingMethod(c.transportConfig.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementQueryInHeader, + Key: "x_padding", + Header: "Referer", + RawURL: url, + } + } + + c.transportConfig.ApplyXPaddingToRequest(req, config) + c.transportConfig.ApplyMetaToRequest(req, sessionId, seqStr) if c.httpVersion != "1.1" { resp, err := c.client.Do(req) diff --git a/transport/internet/splithttp/common.go b/transport/internet/splithttp/common.go new file mode 100644 index 000000000000..20596180f551 --- /dev/null +++ b/transport/internet/splithttp/common.go @@ -0,0 +1,10 @@ +package splithttp + +const ( + PlacementQueryInHeader = "queryInHeader" + PlacementCookie = "cookie" + PlacementHeader = "header" + PlacementQuery = "query" + PlacementPath = "path" + PlacementBody = "body" +) diff --git a/transport/internet/splithttp/config.go b/transport/internet/splithttp/config.go index c1481ddad54e..9ecd5a436503 100644 --- a/transport/internet/splithttp/config.go +++ b/transport/internet/splithttp/config.go @@ -2,7 +2,6 @@ package splithttp import ( "net/http" - "net/url" "strings" "github.com/xtls/xray-core/common" @@ -43,41 +42,27 @@ func (c *Config) GetNormalizedQuery() string { return query } -func (c *Config) GetRequestHeader(rawURL string) http.Header { +func (c *Config) GetRequestHeader() http.Header { header := http.Header{} for k, v := range c.Headers { header.Add(k, v) } - - u, _ := url.Parse(rawURL) - // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B - // h2's HPACK Header Compression feature employs a huffman encoding using a static table. - // 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. - // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 - // h3's similar QPACK feature uses the same huffman table. - u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand())) - header.Set("Referer", u.String()) - return header } func (c *Config) WriteResponseHeader(writer http.ResponseWriter) { // CORS headers for the browser dialer writer.Header().Set("Access-Control-Allow-Origin", "*") - writer.Header().Set("Access-Control-Allow-Methods", "GET, POST") + writer.Header().Set("Access-Control-Allow-Methods", "*") // writer.Header().Set("X-Version", core.Version()) - writer.Header().Set("X-Padding", strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().rand()))) } -func (c *Config) GetNormalizedXPaddingBytes() RangeConfig { - if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { - return RangeConfig{ - From: 100, - To: 1000, - } +func (c *Config) GetNormalizedUplinkHTTPMethod() string { + if c.UplinkHTTPMethod == "" { + return "POST" } - return *c.XPaddingBytes + return c.UplinkHTTPMethod } func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig { @@ -121,6 +106,134 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig { return *c.ScStreamUpServerSecs } +func (c *Config) GetNormalizedSessionPlacement() string { + if c.SessionPlacement == "" { + return PlacementPath + } + return c.SessionPlacement +} + +func (c *Config) GetNormalizedSeqPlacement() string { + if c.SeqPlacement == "" { + return PlacementPath + } + return c.SeqPlacement +} + +func (c *Config) GetNormalizedUplinkDataPlacement() string { + if c.UplinkDataPlacement == "" { + return PlacementBody + } + return c.UplinkDataPlacement +} + +func (c *Config) GetNormalizedSessionKey() string { + if c.SessionKey != "" { + return c.SessionKey + } + switch c.GetNormalizedSessionPlacement() { + case PlacementHeader: + return "X-Session" + case PlacementCookie, PlacementQuery: + return "x_session" + default: + return "" + } +} + +func (c *Config) GetNormalizedSeqKey() string { + if c.SeqKey != "" { + return c.SeqKey + } + switch c.GetNormalizedSeqPlacement() { + case PlacementHeader: + return "X-Seq" + case PlacementCookie, PlacementQuery: + return "x_seq" + default: + return "" + } +} + +func (c *Config) ApplyMetaToRequest(req *http.Request, sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + if sessionId != "" { + switch sessionPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, sessionId) + case PlacementQuery: + q := req.URL.Query() + q.Set(sessionKey, sessionId) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(sessionKey, sessionId) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId}) + } + } + + if seqStr != "" { + switch seqPlacement { + case PlacementPath: + req.URL.Path = appendToPath(req.URL.Path, seqStr) + case PlacementQuery: + q := req.URL.Query() + q.Set(seqKey, seqStr) + req.URL.RawQuery = q.Encode() + case PlacementHeader: + req.Header.Set(seqKey, seqStr) + case PlacementCookie: + req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr}) + } + } +} + +func (c *Config) ExtractMetaFromRequest(req *http.Request, path string) (sessionId string, seqStr string) { + sessionPlacement := c.GetNormalizedSessionPlacement() + seqPlacement := c.GetNormalizedSeqPlacement() + sessionKey := c.GetNormalizedSessionKey() + seqKey := c.GetNormalizedSeqKey() + + if sessionPlacement == PlacementPath && seqPlacement == PlacementPath { + subpath := strings.Split(req.URL.Path[len(path):], "/") + if len(subpath) > 0 { + sessionId = subpath[0] + } + if len(subpath) > 1 { + seqStr = subpath[1] + } + return sessionId, seqStr + } + + switch sessionPlacement { + case PlacementQuery: + sessionId = req.URL.Query().Get(sessionKey) + case PlacementHeader: + sessionId = req.Header.Get(sessionKey) + case PlacementCookie: + if cookie, e := req.Cookie(sessionKey); e == nil { + sessionId = cookie.Value + } + } + + switch seqPlacement { + case PlacementQuery: + seqStr = req.URL.Query().Get(seqKey) + case PlacementHeader: + seqStr = req.Header.Get(seqKey) + case PlacementCookie: + if cookie, e := req.Cookie(seqKey); e == nil { + seqStr = cookie.Value + } + } + + return sessionId, seqStr +} + func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig { if m.MaxConcurrency == nil { return RangeConfig{ @@ -185,3 +298,10 @@ func init() { func (c RangeConfig) rand() int32 { return int32(crypto.RandBetween(int64(c.From), int64(c.To))) } + +func appendToPath(path, value string) string { + if strings.HasSuffix(path, "/") { + return path + value + } + return path + "/" + value +} diff --git a/transport/internet/splithttp/config.pb.go b/transport/internet/splithttp/config.pb.go index 9837772234c4..974563fc9afc 100644 --- a/transport/internet/splithttp/config.pb.go +++ b/transport/internet/splithttp/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.2 +// protoc-gen-go v1.36.11 +// protoc v3.21.12 // source: transport/internet/splithttp/config.proto package splithttp @@ -12,6 +12,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,12 +23,11 @@ const ( ) type RangeConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + From int32 `protobuf:"varint,1,opt,name=from,proto3" json:"from,omitempty"` + To int32 `protobuf:"varint,2,opt,name=to,proto3" json:"to,omitempty"` unknownFields protoimpl.UnknownFields - - From int32 `protobuf:"varint,1,opt,name=from,proto3" json:"from,omitempty"` - To int32 `protobuf:"varint,2,opt,name=to,proto3" json:"to,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RangeConfig) Reset() { @@ -75,16 +75,15 @@ func (x *RangeConfig) GetTo() int32 { } type XmuxConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - MaxConcurrency *RangeConfig `protobuf:"bytes,1,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"` - MaxConnections *RangeConfig `protobuf:"bytes,2,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"` - CMaxReuseTimes *RangeConfig `protobuf:"bytes,3,opt,name=cMaxReuseTimes,proto3" json:"cMaxReuseTimes,omitempty"` - HMaxRequestTimes *RangeConfig `protobuf:"bytes,4,opt,name=hMaxRequestTimes,proto3" json:"hMaxRequestTimes,omitempty"` - HMaxReusableSecs *RangeConfig `protobuf:"bytes,5,opt,name=hMaxReusableSecs,proto3" json:"hMaxReusableSecs,omitempty"` - HKeepAlivePeriod int64 `protobuf:"varint,6,opt,name=hKeepAlivePeriod,proto3" json:"hKeepAlivePeriod,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + MaxConcurrency *RangeConfig `protobuf:"bytes,1,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"` + MaxConnections *RangeConfig `protobuf:"bytes,2,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"` + CMaxReuseTimes *RangeConfig `protobuf:"bytes,3,opt,name=cMaxReuseTimes,proto3" json:"cMaxReuseTimes,omitempty"` + HMaxRequestTimes *RangeConfig `protobuf:"bytes,4,opt,name=hMaxRequestTimes,proto3" json:"hMaxRequestTimes,omitempty"` + HMaxReusableSecs *RangeConfig `protobuf:"bytes,5,opt,name=hMaxReusableSecs,proto3" json:"hMaxReusableSecs,omitempty"` + HKeepAlivePeriod int64 `protobuf:"varint,6,opt,name=hKeepAlivePeriod,proto3" json:"hKeepAlivePeriod,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *XmuxConfig) Reset() { @@ -160,14 +159,11 @@ func (x *XmuxConfig) GetHKeepAlivePeriod() int64 { } type Config struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` - Headers map[string]string `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Headers map[string]string `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` XPaddingBytes *RangeConfig `protobuf:"bytes,5,opt,name=xPaddingBytes,proto3" json:"xPaddingBytes,omitempty"` NoGRPCHeader bool `protobuf:"varint,6,opt,name=noGRPCHeader,proto3" json:"noGRPCHeader,omitempty"` NoSSEHeader bool `protobuf:"varint,7,opt,name=noSSEHeader,proto3" json:"noSSEHeader,omitempty"` @@ -177,6 +173,21 @@ type Config struct { ScStreamUpServerSecs *RangeConfig `protobuf:"bytes,11,opt,name=scStreamUpServerSecs,proto3" json:"scStreamUpServerSecs,omitempty"` Xmux *XmuxConfig `protobuf:"bytes,12,opt,name=xmux,proto3" json:"xmux,omitempty"` DownloadSettings *internet.StreamConfig `protobuf:"bytes,13,opt,name=downloadSettings,proto3" json:"downloadSettings,omitempty"` + XPaddingObfsMode bool `protobuf:"varint,14,opt,name=xPaddingObfsMode,proto3" json:"xPaddingObfsMode,omitempty"` + XPaddingKey string `protobuf:"bytes,15,opt,name=xPaddingKey,proto3" json:"xPaddingKey,omitempty"` + XPaddingHeader string `protobuf:"bytes,16,opt,name=xPaddingHeader,proto3" json:"xPaddingHeader,omitempty"` + 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"` + 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 uint32 `protobuf:"varint,26,opt,name=uplinkChunkSize,proto3" json:"uplinkChunkSize,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Config) Reset() { @@ -300,124 +311,157 @@ func (x *Config) GetDownloadSettings() *internet.StreamConfig { return nil } -var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor +func (x *Config) GetXPaddingObfsMode() bool { + if x != nil { + return x.XPaddingObfsMode + } + return false +} + +func (x *Config) GetXPaddingKey() string { + if x != nil { + return x.XPaddingKey + } + return "" +} -var file_transport_internet_splithttp_config_proto_rawDesc = []byte{ - 0x0a, 0x29, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, 0x78, 0x72, 0x61, - 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x1a, 0x1f, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x31, 0x0a, 0x0b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, - 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, - 0x74, 0x6f, 0x22, 0xf8, 0x03, 0x0a, 0x0a, 0x58, 0x6d, 0x75, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x56, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, - 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, - 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x56, 0x0a, 0x0e, 0x6d, 0x61, 0x78, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, - 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, - 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x56, 0x0a, 0x0e, 0x63, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, 0x73, 0x65, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, - 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, - 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x63, 0x4d, 0x61, 0x78, 0x52, - 0x65, 0x75, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x10, 0x68, 0x4d, 0x61, - 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, - 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x63, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, - 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x63, - 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x68, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x50, - 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x68, 0x4b, 0x65, - 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x22, 0xdc, 0x06, - 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x50, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, - 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, - 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x54, 0x0a, 0x0d, 0x78, 0x50, 0x61, 0x64, 0x64, 0x69, - 0x6e, 0x67, 0x42, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, - 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, - 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x78, - 0x50, 0x61, 0x64, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, - 0x6e, 0x6f, 0x47, 0x52, 0x50, 0x43, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0c, 0x6e, 0x6f, 0x47, 0x52, 0x50, 0x43, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x53, 0x53, 0x45, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x53, 0x53, 0x45, 0x48, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x12, 0x73, 0x63, 0x4d, 0x61, 0x78, 0x45, 0x61, 0x63, 0x68, 0x50, - 0x6f, 0x73, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, - 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, - 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x12, - 0x73, 0x63, 0x4d, 0x61, 0x78, 0x45, 0x61, 0x63, 0x68, 0x50, 0x6f, 0x73, 0x74, 0x42, 0x79, 0x74, - 0x65, 0x73, 0x12, 0x62, 0x0a, 0x14, 0x73, 0x63, 0x4d, 0x69, 0x6e, 0x50, 0x6f, 0x73, 0x74, 0x73, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x4d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, - 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x14, 0x73, 0x63, 0x4d, 0x69, 0x6e, 0x50, 0x6f, 0x73, 0x74, 0x73, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x4d, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x73, 0x63, 0x4d, 0x61, 0x78, 0x42, - 0x75, 0x66, 0x66, 0x65, 0x72, 0x65, 0x64, 0x50, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x12, 0x73, 0x63, 0x4d, 0x61, 0x78, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x65, - 0x64, 0x50, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x62, 0x0a, 0x14, 0x73, 0x63, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x55, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x63, 0x73, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, - 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x14, 0x73, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x55, 0x70, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x63, 0x73, 0x12, 0x41, 0x0a, 0x04, 0x78, 0x6d, - 0x75, 0x78, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x58, 0x6d, 0x75, - 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x78, 0x6d, 0x75, 0x78, 0x12, 0x51, 0x0a, - 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, - 0x74, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x10, - 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x1a, 0x3a, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x85, 0x01, 0x0a, - 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, - 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, - 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x50, 0x01, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, - 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, - 0xaa, 0x02, 0x21, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, - 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x53, 0x70, 0x6c, 0x69, 0x74, - 0x48, 0x74, 0x74, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +func (x *Config) GetXPaddingHeader() string { + if x != nil { + return x.XPaddingHeader + } + return "" +} + +func (x *Config) GetXPaddingPlacement() string { + if x != nil { + return x.XPaddingPlacement + } + return "" } +func (x *Config) GetXPaddingMethod() string { + if x != nil { + return x.XPaddingMethod + } + return "" +} + +func (x *Config) GetUplinkHTTPMethod() string { + if x != nil { + return x.UplinkHTTPMethod + } + return "" +} + +func (x *Config) GetSessionPlacement() string { + if x != nil { + return x.SessionPlacement + } + return "" +} + +func (x *Config) GetSessionKey() string { + if x != nil { + return x.SessionKey + } + return "" +} + +func (x *Config) GetSeqPlacement() string { + if x != nil { + return x.SeqPlacement + } + return "" +} + +func (x *Config) GetSeqKey() string { + if x != nil { + return x.SeqKey + } + return "" +} + +func (x *Config) GetUplinkDataPlacement() string { + if x != nil { + return x.UplinkDataPlacement + } + return "" +} + +func (x *Config) GetUplinkDataKey() string { + if x != nil { + return x.UplinkDataKey + } + return "" +} + +func (x *Config) GetUplinkChunkSize() uint32 { + if x != nil { + return x.UplinkChunkSize + } + return 0 +} + +var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor + +const file_transport_internet_splithttp_config_proto_rawDesc = "" + + "\n" + + ")transport/internet/splithttp/config.proto\x12!xray.transport.internet.splithttp\x1a\x1ftransport/internet/config.proto\"1\n" + + "\vRangeConfig\x12\x12\n" + + "\x04from\x18\x01 \x01(\x05R\x04from\x12\x0e\n" + + "\x02to\x18\x02 \x01(\x05R\x02to\"\xf8\x03\n" + + "\n" + + "XmuxConfig\x12V\n" + + "\x0emaxConcurrency\x18\x01 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConcurrency\x12V\n" + + "\x0emaxConnections\x18\x02 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConnections\x12V\n" + + "\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\"\xde\n" + + "\n" + + "\x06Config\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\x12P\n" + + "\aheaders\x18\x04 \x03(\v26.xray.transport.internet.splithttp.Config.HeadersEntryR\aheaders\x12T\n" + + "\rxPaddingBytes\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\rxPaddingBytes\x12\"\n" + + "\fnoGRPCHeader\x18\x06 \x01(\bR\fnoGRPCHeader\x12 \n" + + "\vnoSSEHeader\x18\a \x01(\bR\vnoSSEHeader\x12^\n" + + "\x12scMaxEachPostBytes\x18\b \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x12scMaxEachPostBytes\x12b\n" + + "\x14scMinPostsIntervalMs\x18\t \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scMinPostsIntervalMs\x12.\n" + + "\x12scMaxBufferedPosts\x18\n" + + " \x01(\x03R\x12scMaxBufferedPosts\x12b\n" + + "\x14scStreamUpServerSecs\x18\v \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scStreamUpServerSecs\x12A\n" + + "\x04xmux\x18\f \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmux\x12Q\n" + + "\x10downloadSettings\x18\r \x01(\v2%.xray.transport.internet.StreamConfigR\x10downloadSettings\x12*\n" + + "\x10xPaddingObfsMode\x18\x0e \x01(\bR\x10xPaddingObfsMode\x12 \n" + + "\vxPaddingKey\x18\x0f \x01(\tR\vxPaddingKey\x12&\n" + + "\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" + + "\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\x12(\n" + + "\x0fuplinkChunkSize\x18\x1a \x01(\rR\x0fuplinkChunkSize\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" + + "%com.xray.transport.internet.splithttpP\x01Z6github.com/xtls/xray-core/transport/internet/splithttp\xaa\x02!Xray.Transport.Internet.SplitHttpb\x06proto3" + var ( file_transport_internet_splithttp_config_proto_rawDescOnce sync.Once - file_transport_internet_splithttp_config_proto_rawDescData = file_transport_internet_splithttp_config_proto_rawDesc + file_transport_internet_splithttp_config_proto_rawDescData []byte ) func file_transport_internet_splithttp_config_proto_rawDescGZIP() []byte { file_transport_internet_splithttp_config_proto_rawDescOnce.Do(func() { - file_transport_internet_splithttp_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_splithttp_config_proto_rawDescData) + file_transport_internet_splithttp_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_splithttp_config_proto_rawDesc), len(file_transport_internet_splithttp_config_proto_rawDesc))) }) return file_transport_internet_splithttp_config_proto_rawDescData } @@ -459,7 +503,7 @@ func file_transport_internet_splithttp_config_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_transport_internet_splithttp_config_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_splithttp_config_proto_rawDesc), len(file_transport_internet_splithttp_config_proto_rawDesc)), NumEnums: 0, NumMessages: 4, NumExtensions: 0, @@ -470,7 +514,6 @@ func file_transport_internet_splithttp_config_proto_init() { MessageInfos: file_transport_internet_splithttp_config_proto_msgTypes, }.Build() File_transport_internet_splithttp_config_proto = out.File - file_transport_internet_splithttp_config_proto_rawDesc = nil file_transport_internet_splithttp_config_proto_goTypes = nil file_transport_internet_splithttp_config_proto_depIdxs = nil } diff --git a/transport/internet/splithttp/config.proto b/transport/internet/splithttp/config.proto index beb1d00421c4..684717713aa1 100644 --- a/transport/internet/splithttp/config.proto +++ b/transport/internet/splithttp/config.proto @@ -36,4 +36,17 @@ message Config { RangeConfig scStreamUpServerSecs = 11; XmuxConfig xmux = 12; xray.transport.internet.StreamConfig downloadSettings = 13; + bool xPaddingObfsMode = 14; + string xPaddingKey = 15; + string xPaddingHeader = 16; + string xPaddingPlacement = 17; + string xPaddingMethod = 18; + string uplinkHTTPMethod = 19; + string sessionPlacement = 20; + string sessionKey = 21; + string seqPlacement = 22; + string seqKey = 23; + string uplinkDataPlacement = 24; + string uplinkDataKey = 25; + uint32 uplinkChunkSize = 26; } diff --git a/transport/internet/splithttp/dialer.go b/transport/internet/splithttp/dialer.go index 45b770e7c6bd..d85198180b86 100644 --- a/transport/internet/splithttp/dialer.go +++ b/transport/internet/splithttp/dialer.go @@ -272,8 +272,12 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me requestURL.Host = dest.Address.String() } - sessionIdUuid := uuid.New() - requestURL.Path = transportConfiguration.GetNormalizedPath() + sessionIdUuid.String() + sessionId := "" + if transportConfiguration.Mode != "stream-one" { + sessionIdUuid := uuid.New() + sessionId = sessionIdUuid.String() + } + requestURL.Path = transportConfiguration.GetNormalizedPath() requestURL.RawQuery = transportConfiguration.GetNormalizedQuery() httpClient, xmuxClient := getHTTPClient(ctx, dest, streamSettings) @@ -327,7 +331,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me if requestURL2.Host == "" { requestURL2.Host = dest2.Address.String() } - requestURL2.Path = config2.GetNormalizedPath() + sessionIdUuid.String() + requestURL2.Path = config2.GetNormalizedPath() requestURL2.RawQuery = config2.GetNormalizedQuery() httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2) errors.LogInfo(ctx, fmt.Sprintf("XHTTP is downloading from %s, mode %s, HTTP version %s, host %s", dest2, "stream-down", httpVersion2, requestURL2.Host)) @@ -363,7 +367,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me if xmuxClient != nil { xmuxClient.LeftRequests.Add(-1) } - conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), reader, false) + conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, false) if err != nil { // browser dialer only return nil, err } @@ -372,7 +376,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me if xmuxClient2 != nil { xmuxClient2.LeftRequests.Add(-1) } - conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), nil, false) + conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false) if err != nil { // browser dialer only return nil, err } @@ -381,7 +385,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me if xmuxClient != nil { xmuxClient.LeftRequests.Add(-1) } - _, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), reader, true) + _, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, true) if err != nil { // browser dialer only return nil, err } @@ -423,8 +427,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me // this intentionally makes a shallow-copy of the struct so we // can reassign Path (potentially concurrently) url := requestURL - url.Path += "/" + strconv.FormatInt(seq, 10) - + seqStr := strconv.FormatInt(seq, 10) seq += 1 if scMinPostsIntervalMs.From > 0 { @@ -450,6 +453,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me err := httpClient.PostPacket( ctx, url.String(), + sessionId, + seqStr, &buf.MultiBufferContainer{MultiBuffer: chunk}, int64(chunk.Len()), ) diff --git a/transport/internet/splithttp/hub.go b/transport/internet/splithttp/hub.go index 442f196f7c4e..7e15724646c5 100644 --- a/transport/internet/splithttp/hub.go +++ b/transport/internet/splithttp/hub.go @@ -4,9 +4,10 @@ import ( "bytes" "context" gotls "crypto/tls" + "encoding/base64" + "fmt" "io" "net/http" - "net/url" "strconv" "strings" "sync" @@ -100,6 +101,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req } h.config.WriteResponseHeader(writer) + length := int(h.config.GetNormalizedXPaddingBytes().rand()) + config := XPaddingConfig{Length: length} + + if h.config.XPaddingObfsMode { + config.Placement = XPaddingPlacement{ + Placement: h.config.XPaddingPlacement, + Key: h.config.XPaddingKey, + Header: h.config.XPaddingHeader, + } + config.Method = PaddingMethod(h.config.XPaddingMethod) + } else { + config.Placement = XPaddingPlacement{ + Placement: PlacementHeader, + Header: "X-Padding", + } + } + + h.config.ApplyXPaddingToHeader(writer.Header(), config) /* clientVer := []int{0, 0, 0} @@ -110,29 +129,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req */ validRange := h.config.GetNormalizedXPaddingBytes() - paddingLength := 0 + paddingValue, paddingPlacement := h.config.ExtractXPaddingFromRequest(request, h.config.XPaddingObfsMode) - referrer := request.Header.Get("Referer") - if referrer != "" { - if referrerURL, err := url.Parse(referrer); err == nil { - // Browser dialer cannot control the host part of referrer header, so only check the query - paddingLength = len(referrerURL.Query().Get("x_padding")) - } - } else { - paddingLength = len(request.URL.Query().Get("x_padding")) - } - - if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To { - errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength)) + if !h.config.IsPaddingValid(paddingValue, validRange.From, validRange.To, PaddingMethod(h.config.XPaddingMethod)) { + errors.LogInfo(context.Background(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue))) writer.WriteHeader(http.StatusBadRequest) return } - sessionId := "" - subpath := strings.Split(request.URL.Path[len(h.path):], "/") - if len(subpath) > 0 { - sessionId = subpath[0] - } + sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path) if sessionId == "" && h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-one" && h.config.Mode != "stream-up" { errors.LogInfo(context.Background(), "stream-one mode is not allowed") @@ -178,14 +183,29 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req currentSession = h.upsertSession(sessionId) } scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To) + uplinkHTTPMethod := h.config.GetNormalizedUplinkHTTPMethod() + isUplinkRequest := false + + if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod { + isUplinkRequest = true + } - if request.Method == "POST" && sessionId != "" { // stream-up, packet-up - seq := "" - if len(subpath) > 1 { - seq = subpath[1] + uplinkDataPlacement := h.config.GetNormalizedUplinkDataPlacement() + uplinkDataKey := h.config.UplinkDataKey + + switch uplinkDataPlacement { + case PlacementHeader: + if request.Header.Get(uplinkDataKey+"-Upstream") == "1" { + isUplinkRequest = true + } + case PlacementCookie: + if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" { + isUplinkRequest = true } + } - if seq == "" { + if isUplinkRequest && sessionId != "" { // stream-up, packet-up + if seqStr == "" { if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" { errors.LogInfo(context.Background(), "stream-up mode is not allowed") writer.WriteHeader(http.StatusBadRequest) @@ -207,6 +227,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(http.StatusOK) scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs() + referrer := request.Header.Get("Referer") if referrer != "" && scStreamUpServerSecs.To > 0 { go func() { for { @@ -233,7 +254,62 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1)) + var payload []byte + + if uplinkDataPlacement != PlacementBody { + var encodedStr string + switch uplinkDataPlacement { + case PlacementHeader: + dataLenStr := request.Header.Get(uplinkDataKey + "-Length") + + if dataLenStr != "" { + dataLen, _ := strconv.Atoi(dataLenStr) + var chunks []string + i := 0 + + for { + chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i)) + if chunk == "" { + break + } + chunks = append(chunks, chunk) + i++ + } + + encodedStr = strings.Join(chunks, "") + if len(encodedStr) != dataLen { + encodedStr = "" + } + } + case PlacementCookie: + var chunks []string + i := 0 + + for { + cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i) + if c, _ := request.Cookie(cookieName); c != nil { + chunks = append(chunks, c.Value) + i++ + } else { + break + } + } + + if len(chunks) > 0 { + encodedStr = strings.Join(chunks, "") + } + } + + if encodedStr != "" { + payload, err = base64.RawURLEncoding.DecodeString(encodedStr) + } else { + errors.LogInfoInner(context.Background(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement) + writer.WriteHeader(http.StatusInternalServerError) + return + } + } else { + payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1)) + } if len(payload) > scMaxEachPostBytes { errors.LogInfo(context.Background(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.") @@ -247,7 +323,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req return } - seqInt, err := strconv.ParseUint(seq, 10, 64) + seq, err := strconv.ParseUint(seqStr, 10, 64) if err != nil { errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)") writer.WriteHeader(http.StatusInternalServerError) @@ -256,7 +332,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req err = currentSession.uploadQueue.Push(Packet{ Payload: payload, - Seq: seqInt, + Seq: seq, }) if err != nil { diff --git a/transport/internet/splithttp/xpadding.go b/transport/internet/splithttp/xpadding.go new file mode 100644 index 000000000000..ce22436969f2 --- /dev/null +++ b/transport/internet/splithttp/xpadding.go @@ -0,0 +1,307 @@ +package splithttp + +import ( + "crypto/rand" + "math" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/http2/hpack" +) + +type PaddingMethod string + +const ( + PaddingMethodRepeatX PaddingMethod = "repeat-x" + PaddingMethodTokenish PaddingMethod = "tokenish" +) + +const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// Huffman encoding gives ~20% size reduction for base62 sequences +const avgHuffmanBytesPerCharBase62 = 0.8 + +const validationTolerance = 2 + +type XPaddingPlacement struct { + Placement string + Key string + Header string + RawURL string +} + +type XPaddingConfig struct { + Length int + Placement XPaddingPlacement + Method PaddingMethod +} + +func randStringFromCharset(n int, charset string) (string, bool) { + if n <= 0 || len(charset) == 0 { + return "", false + } + + m := len(charset) + limit := byte(256 - (256 % m)) + + result := make([]byte, n) + i := 0 + + buf := make([]byte, 256) + for i < n { + if _, err := rand.Read(buf); err != nil { + return "", false + } + for _, rb := range buf { + if rb >= limit { + continue + } + result[i] = charset[int(rb)%m] + i++ + if i == n { + break + } + } + } + + return string(result), true +} + +func absInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string { + n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62)) + if n < 1 { + n = 1 + } + + randBase62Str, ok := randStringFromCharset(n, charsetBase62) + if !ok { + return "" + } + + const maxIter = 150 + adjustChar := byte('X') + + // Adjust until close enough + for iter := 0; iter < maxIter; iter++ { + currentLength := int(hpack.HuffmanEncodeLength(randBase62Str)) + diff := currentLength - targetHuffmanBytes + + if absInt(diff) <= validationTolerance { + return randBase62Str + } + + if diff < 0 { + // Too small -> append padding char(s) + randBase62Str += string(adjustChar) + + // Avoid a long run of identical chars + if adjustChar == 'X' { + adjustChar = 'Z' + } else { + adjustChar = 'X' + } + } else { + // Too big -> remove from the end + if len(randBase62Str) <= 1 { + return randBase62Str + } + randBase62Str = randBase62Str[:len(randBase62Str)-1] + } + } + + return randBase62Str +} + +func GeneratePadding(method PaddingMethod, length int) string { + if length <= 0 { + return "" + } + + // https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B + // h2's HPACK Header Compression feature employs a huffman encoding using a static table. + // 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire. + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2 + // h3's similar QPACK feature uses the same huffman table. + + switch method { + case PaddingMethodRepeatX: + return strings.Repeat("X", length) + case PaddingMethodTokenish: + paddingValue := GenerateTokenishPaddingBase62(length) + if paddingValue == "" { + return strings.Repeat("X", length) + } + return paddingValue + default: + return strings.Repeat("X", length) + } +} + +func ApplyPaddingToCookie(req *http.Request, name, value string) { + if req == nil || name == "" || value == "" { + return + } + req.AddCookie(&http.Cookie{ + Name: name, + Value: value, + Path: "/", + }) +} + +func ApplyPaddingToQuery(u *url.URL, key, value string) { + if u == nil || key == "" || value == "" { + return + } + q := u.Query() + q.Set(key, value) + u.RawQuery = q.Encode() +} + +func (c *Config) GetNormalizedXPaddingBytes() RangeConfig { + if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { + return RangeConfig{ + From: 100, + To: 1000, + } + } + + return *c.XPaddingBytes +} + +func (c *Config) ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) { + if h == nil { + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch p := config.Placement; p.Placement { + case PlacementHeader: + h.Set(p.Header, paddingValue) + case PlacementQueryInHeader: + u, err := url.Parse(p.RawURL) + if err != nil || u == nil { + return + } + u.RawQuery = p.Key + "=" + paddingValue + h.Set(p.Header, u.String()) + } +} + +func (c *Config) ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) { + if req == nil { + return + } + if req.Header == nil { + req.Header = make(http.Header) + } + + placement := config.Placement.Placement + + if placement == PlacementHeader || placement == PlacementQueryInHeader { + c.ApplyXPaddingToHeader(req.Header, config) + return + } + + paddingValue := GeneratePadding(config.Method, config.Length) + + switch placement { + case PlacementCookie: + ApplyPaddingToCookie(req, config.Placement.Key, paddingValue) + case PlacementQuery: + ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue) + } +} + +func (c *Config) ExtractXPaddingFromRequest(req *http.Request, obfsMode bool) (string, string) { + if req == nil { + return "", "" + } + + if !obfsMode { + referrer := req.Header.Get("Referer") + + if referrer != "" { + if referrerURL, err := url.Parse(referrer); err == nil { + paddingValue := referrerURL.Query().Get("x_padding") + paddingPlacement := PlacementQueryInHeader + "=Referer, key=x_padding" + return paddingValue, paddingPlacement + } + } else { + paddingValue := req.URL.Query().Get("x_padding") + return paddingValue, PlacementQuery + ", key=x_padding" + } + } + + key := c.XPaddingKey + header := c.XPaddingHeader + + if cookie, err := req.Cookie(key); err == nil { + if cookie != nil && cookie.Value != "" { + paddingValue := cookie.Value + paddingPlacement := PlacementCookie + ", key=" + key + return paddingValue, paddingPlacement + } + } + + headerValue := req.Header.Get(header) + + if headerValue != "" { + if c.XPaddingPlacement == PlacementHeader { + paddingPlacement := PlacementHeader + "=" + header + return headerValue, paddingPlacement + } + + if parsedURL, err := url.Parse(headerValue); err == nil { + paddingPlacement := PlacementQueryInHeader + "=" + header + ", key=" + key + + return parsedURL.Query().Get(key), paddingPlacement + } + } + + queryValue := req.URL.Query().Get(key) + + if queryValue != "" { + paddingPlacement := PlacementQuery + ", key=" + key + return queryValue, paddingPlacement + } + + return "", "" +} + +func (c *Config) IsPaddingValid(paddingValue string, from, to int32, method PaddingMethod) bool { + if paddingValue == "" { + return false + } + if to <= 0 { + r := c.GetNormalizedXPaddingBytes() + from, to = r.From, r.To + } + + switch method { + case PaddingMethodRepeatX: + n := int32(len(paddingValue)) + return n >= from && n <= to + case PaddingMethodTokenish: + const tolerance = int32(validationTolerance) + + n := int32(hpack.HuffmanEncodeLength(paddingValue)) + f := from - tolerance + t := to + tolerance + if f < 0 { + f = 0 + } + return n >= f && n <= t + default: + n := int32(len(paddingValue)) + return n >= from && n <= to + } +} diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index c1c55effef75..389d0699adfd 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -257,7 +257,8 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b } req.Header.Set("Accept", "application/dns-message") req.Header.Set("Content-Type", "application/dns-message") - req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000)))) + req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000))) + resp, err := client.Do(req) if err != nil { return nil, 0, err