Просмотр исходного кода

XHTTP transport: New options for bypassing CDN's detection (#5414)

Usage: https://github.com/XTLS/Xray-core/pull/5414#issuecomment-3770071786

Closes https://github.com/XTLS/Xray-core/issues/4346

---------

Co-authored-by: 风扇滑翔翼 <[email protected]>
Dmitrii Makhno 4 месяцев назад
Родитель
Сommit
5b849d51a9

+ 2 - 3
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
 

+ 24 - 0
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)
+}

+ 127 - 0
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),

+ 50 - 4
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
 	}

+ 105 - 11
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)

+ 10 - 0
transport/internet/splithttp/common.go

@@ -0,0 +1,10 @@
+package splithttp
+
+const (
+	PlacementQueryInHeader = "queryInHeader"
+	PlacementCookie        = "cookie"
+	PlacementHeader        = "header"
+	PlacementQuery         = "query"
+	PlacementPath          = "path"
+	PlacementBody          = "body"
+)

+ 141 - 21
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
+}

+ 176 - 133
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
 }

+ 13 - 0
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;
 }

+ 13 - 8
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()),
 				)

+ 103 - 27
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 {

+ 307 - 0
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
+	}
+}

+ 2 - 1
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