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

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"
 	"io"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
-	"strings"
 	"time"
 	"time"
 
 
 	utls "github.com/refraction-networking/utls"
 	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/net/cnc"
 	"github.com/xtls/xray-core/common/protocol/dns"
 	"github.com/xtls/xray-core/common/protocol/dns"
 	"github.com/xtls/xray-core/common/session"
 	"github.com/xtls/xray-core/common/session"
+	"github.com/xtls/xray-core/common/utils"
 	dns_feature "github.com/xtls/xray-core/features/dns"
 	dns_feature "github.com/xtls/xray-core/features/dns"
 	"github.com/xtls/xray-core/features/routing"
 	"github.com/xtls/xray-core/features/routing"
 	"github.com/xtls/xray-core/transport/internet"
 	"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("Accept", "application/dns-message")
 	req.Header.Add("Content-Type", "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
 	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"`
 	Mode                 string            `json:"mode"`
 	Headers              map[string]string `json:"headers"`
 	Headers              map[string]string `json:"headers"`
 	XPaddingBytes        Int32Range        `json:"xPaddingBytes"`
 	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"`
 	NoGRPCHeader         bool              `json:"noGRPCHeader"`
 	NoSSEHeader          bool              `json:"noSSEHeader"`
 	NoSSEHeader          bool              `json:"noSSEHeader"`
 	ScMaxEachPostBytes   Int32Range        `json:"scMaxEachPostBytes"`
 	ScMaxEachPostBytes   Int32Range        `json:"scMaxEachPostBytes"`
@@ -287,6 +300,107 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
 		return nil, errors.New("xPaddingBytes cannot be disabled")
 		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 {
 	if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
 		return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
 		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,
 		Mode:                 c.Mode,
 		Headers:              c.Headers,
 		Headers:              c.Headers,
 		XPaddingBytes:        newRangeConfig(c.XPaddingBytes),
 		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,
 		NoGRPCHeader:         c.NoGRPCHeader,
 		NoSSEHeader:          c.NoSSEHeader,
 		NoSSEHeader:          c.NoSSEHeader,
 		ScMaxEachPostBytes:   newRangeConfig(c.ScMaxEachPostBytes),
 		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")
 	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 {
 	if body != nil {
 		return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
 		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{}
 	dummyAddr := &net.IPAddr{}
 	if err != nil {
 	if err != nil {
 		return nil, dummyAddr, dummyAddr, err
 		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
 	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)
 	bytes, err := io.ReadAll(body)
 	if err != nil {
 	if err != nil {
 		return err
 		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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 105 - 11
transport/internet/splithttp/client.go

@@ -3,6 +3,7 @@ package splithttp
 import (
 import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
+	"encoding/base64"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
@@ -19,11 +20,11 @@ import (
 type DialerClient interface {
 type DialerClient interface {
 	IsClosed() bool
 	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
 // implements splithttp.DialerClient in terms of direct network connections
@@ -41,7 +42,7 @@ func (c *DefaultDialerClient) IsClosed() bool {
 	return c.closed
 	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,
 	// 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
 	// and we can unblock the Dial function and print correct net addresses in
 	// logs
 	// logs
@@ -56,11 +57,34 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
 
 
 	method := "GET" // stream-down
 	method := "GET" // stream-down
 	if body != nil {
 	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, _ := 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")
 		req.Header.Set("Content-Type", "application/grpc")
 	}
 	}
 
 
@@ -92,13 +116,83 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
 	return
 	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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	req.ContentLength = contentLength
 	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" {
 	if c.httpVersion != "1.1" {
 		resp, err := c.client.Do(req)
 		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 (
 import (
 	"net/http"
 	"net/http"
-	"net/url"
 	"strings"
 	"strings"
 
 
 	"github.com/xtls/xray-core/common"
 	"github.com/xtls/xray-core/common"
@@ -43,41 +42,27 @@ func (c *Config) GetNormalizedQuery() string {
 	return query
 	return query
 }
 }
 
 
-func (c *Config) GetRequestHeader(rawURL string) http.Header {
+func (c *Config) GetRequestHeader() http.Header {
 	header := http.Header{}
 	header := http.Header{}
 	for k, v := range c.Headers {
 	for k, v := range c.Headers {
 		header.Add(k, v)
 		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
 	return header
 }
 }
 
 
 func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
 func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
 	// CORS headers for the browser dialer
 	// CORS headers for the browser dialer
 	writer.Header().Set("Access-Control-Allow-Origin", "*")
 	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-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 {
 func (c *Config) GetNormalizedScMaxEachPostBytes() RangeConfig {
@@ -121,6 +106,134 @@ func (c *Config) GetNormalizedScStreamUpServerSecs() RangeConfig {
 	return *c.ScStreamUpServerSecs
 	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 {
 func (m *XmuxConfig) GetNormalizedMaxConcurrency() RangeConfig {
 	if m.MaxConcurrency == nil {
 	if m.MaxConcurrency == nil {
 		return RangeConfig{
 		return RangeConfig{
@@ -185,3 +298,10 @@ func init() {
 func (c RangeConfig) rand() int32 {
 func (c RangeConfig) rand() int32 {
 	return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
 	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.
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 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
 // source: transport/internet/splithttp/config.proto
 
 
 package splithttp
 package splithttp
@@ -12,6 +12,7 @@ import (
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 	reflect "reflect"
 	reflect "reflect"
 	sync "sync"
 	sync "sync"
+	unsafe "unsafe"
 )
 )
 
 
 const (
 const (
@@ -22,12 +23,11 @@ const (
 )
 )
 
 
 type RangeConfig struct {
 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
 	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() {
 func (x *RangeConfig) Reset() {
@@ -75,16 +75,15 @@ func (x *RangeConfig) GetTo() int32 {
 }
 }
 
 
 type XmuxConfig struct {
 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() {
 func (x *XmuxConfig) Reset() {
@@ -160,14 +159,11 @@ func (x *XmuxConfig) GetHKeepAlivePeriod() int64 {
 }
 }
 
 
 type Config struct {
 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"`
 	Host                 string                 `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
 	Path                 string                 `protobuf:"bytes,2,opt,name=path,proto3" json:"path,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"`
 	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"`
 	XPaddingBytes        *RangeConfig           `protobuf:"bytes,5,opt,name=xPaddingBytes,proto3" json:"xPaddingBytes,omitempty"`
 	NoGRPCHeader         bool                   `protobuf:"varint,6,opt,name=noGRPCHeader,proto3" json:"noGRPCHeader,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"`
 	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"`
 	ScStreamUpServerSecs *RangeConfig           `protobuf:"bytes,11,opt,name=scStreamUpServerSecs,proto3" json:"scStreamUpServerSecs,omitempty"`
 	Xmux                 *XmuxConfig            `protobuf:"bytes,12,opt,name=xmux,proto3" json:"xmux,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"`
 	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() {
 func (x *Config) Reset() {
@@ -300,124 +311,157 @@ func (x *Config) GetDownloadSettings() *internet.StreamConfig {
 	return nil
 	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 (
 var (
 	file_transport_internet_splithttp_config_proto_rawDescOnce sync.Once
 	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 {
 func file_transport_internet_splithttp_config_proto_rawDescGZIP() []byte {
 	file_transport_internet_splithttp_config_proto_rawDescOnce.Do(func() {
 	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
 	return file_transport_internet_splithttp_config_proto_rawDescData
 }
 }
@@ -459,7 +503,7 @@ func file_transport_internet_splithttp_config_proto_init() {
 	out := protoimpl.TypeBuilder{
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			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,
 			NumEnums:      0,
 			NumMessages:   4,
 			NumMessages:   4,
 			NumExtensions: 0,
 			NumExtensions: 0,
@@ -470,7 +514,6 @@ func file_transport_internet_splithttp_config_proto_init() {
 		MessageInfos:      file_transport_internet_splithttp_config_proto_msgTypes,
 		MessageInfos:      file_transport_internet_splithttp_config_proto_msgTypes,
 	}.Build()
 	}.Build()
 	File_transport_internet_splithttp_config_proto = out.File
 	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_goTypes = nil
 	file_transport_internet_splithttp_config_proto_depIdxs = 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;
   RangeConfig scStreamUpServerSecs = 11;
   XmuxConfig xmux = 12;
   XmuxConfig xmux = 12;
   xray.transport.internet.StreamConfig downloadSettings = 13;
   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()
 		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()
 	requestURL.RawQuery = transportConfiguration.GetNormalizedQuery()
 
 
 	httpClient, xmuxClient := getHTTPClient(ctx, dest, streamSettings)
 	httpClient, xmuxClient := getHTTPClient(ctx, dest, streamSettings)
@@ -327,7 +331,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 		if requestURL2.Host == "" {
 		if requestURL2.Host == "" {
 			requestURL2.Host = dest2.Address.String()
 			requestURL2.Host = dest2.Address.String()
 		}
 		}
-		requestURL2.Path = config2.GetNormalizedPath() + sessionIdUuid.String()
+		requestURL2.Path = config2.GetNormalizedPath()
 		requestURL2.RawQuery = config2.GetNormalizedQuery()
 		requestURL2.RawQuery = config2.GetNormalizedQuery()
 		httpClient2, xmuxClient2 = getHTTPClient(ctx, dest2, memory2)
 		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))
 		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 {
 		if xmuxClient != nil {
 			xmuxClient.LeftRequests.Add(-1)
 			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
 		if err != nil { // browser dialer only
 			return nil, err
 			return nil, err
 		}
 		}
@@ -372,7 +376,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 		if xmuxClient2 != nil {
 		if xmuxClient2 != nil {
 			xmuxClient2.LeftRequests.Add(-1)
 			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
 		if err != nil { // browser dialer only
 			return nil, err
 			return nil, err
 		}
 		}
@@ -381,7 +385,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 		if xmuxClient != nil {
 		if xmuxClient != nil {
 			xmuxClient.LeftRequests.Add(-1)
 			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
 		if err != nil { // browser dialer only
 			return nil, err
 			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
 			// this intentionally makes a shallow-copy of the struct so we
 			// can reassign Path (potentially concurrently)
 			// can reassign Path (potentially concurrently)
 			url := requestURL
 			url := requestURL
-			url.Path += "/" + strconv.FormatInt(seq, 10)
-
+			seqStr := strconv.FormatInt(seq, 10)
 			seq += 1
 			seq += 1
 
 
 			if scMinPostsIntervalMs.From > 0 {
 			if scMinPostsIntervalMs.From > 0 {
@@ -450,6 +453,8 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
 				err := httpClient.PostPacket(
 				err := httpClient.PostPacket(
 					ctx,
 					ctx,
 					url.String(),
 					url.String(),
+					sessionId,
+					seqStr,
 					&buf.MultiBufferContainer{MultiBuffer: chunk},
 					&buf.MultiBufferContainer{MultiBuffer: chunk},
 					int64(chunk.Len()),
 					int64(chunk.Len()),
 				)
 				)

+ 103 - 27
transport/internet/splithttp/hub.go

@@ -4,9 +4,10 @@ import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
 	gotls "crypto/tls"
 	gotls "crypto/tls"
+	"encoding/base64"
+	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
-	"net/url"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -100,6 +101,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 	}
 	}
 
 
 	h.config.WriteResponseHeader(writer)
 	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}
 		clientVer := []int{0, 0, 0}
@@ -110,29 +129,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 	*/
 	*/
 
 
 	validRange := h.config.GetNormalizedXPaddingBytes()
 	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)
 		writer.WriteHeader(http.StatusBadRequest)
 		return
 		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" {
 	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")
 		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)
 		currentSession = h.upsertSession(sessionId)
 	}
 	}
 	scMaxEachPostBytes := int(h.ln.config.GetNormalizedScMaxEachPostBytes().To)
 	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" {
 			if h.config.Mode != "" && h.config.Mode != "auto" && h.config.Mode != "stream-up" {
 				errors.LogInfo(context.Background(), "stream-up mode is not allowed")
 				errors.LogInfo(context.Background(), "stream-up mode is not allowed")
 				writer.WriteHeader(http.StatusBadRequest)
 				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.Header().Set("Cache-Control", "no-store")
 				writer.WriteHeader(http.StatusOK)
 				writer.WriteHeader(http.StatusOK)
 				scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs()
 				scStreamUpServerSecs := h.config.GetNormalizedScStreamUpServerSecs()
+				referrer := request.Header.Get("Referer")
 				if referrer != "" && scStreamUpServerSecs.To > 0 {
 				if referrer != "" && scStreamUpServerSecs.To > 0 {
 					go func() {
 					go func() {
 						for {
 						for {
@@ -233,7 +254,62 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 			return
 			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 {
 		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.")
 			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
 			return
 		}
 		}
 
 
-		seqInt, err := strconv.ParseUint(seq, 10, 64)
+		seq, err := strconv.ParseUint(seqStr, 10, 64)
 		if err != nil {
 		if err != nil {
 			errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)")
 			errors.LogInfoInner(context.Background(), err, "failed to upload (ParseUint)")
 			writer.WriteHeader(http.StatusInternalServerError)
 			writer.WriteHeader(http.StatusInternalServerError)
@@ -256,7 +332,7 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
 
 
 		err = currentSession.uploadQueue.Push(Packet{
 		err = currentSession.uploadQueue.Push(Packet{
 			Payload: payload,
 			Payload: payload,
-			Seq:     seqInt,
+			Seq:     seq,
 		})
 		})
 
 
 		if err != nil {
 		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("Accept", "application/dns-message")
 		req.Header.Set("Content-Type", "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)
 		resp, err := client.Do(req)
 		if err != nil {
 		if err != nil {
 			return nil, 0, err
 			return nil, 0, err