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: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
This commit is contained in:
Dmitrii Makhno
2026-01-31 16:34:13 +03:00
committed by GitHub
parent 61e1153157
commit 5b849d51a9
13 changed files with 1073 additions and 208 deletions

View File

@@ -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", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))
req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000))))
hc := s.httpClient hc := s.httpClient

24
common/utils/padding.go Normal file
View File

@@ -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)
}

View File

@@ -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),

View File

@@ -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
} }

View File

@@ -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 // ctx, url, sessionId, body, uploadOnly
OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error) OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
// ctx, url, body, contentLength // ctx, url, sessionId, seqStr, body, contentLength
PostPacket(context.Context, string, io.Reader, int64) error 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) req.Header = c.transportConfig.GetRequestHeader()
if method == "POST" && !c.transportConfig.NoGRPCHeader { 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 { func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body) 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)

View File

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

View File

@@ -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 { func (c *Config) GetNormalizedUplinkHTTPMethod() string {
if c.XPaddingBytes == nil || c.XPaddingBytes.To == 0 { if c.UplinkHTTPMethod == "" {
return RangeConfig{ return "POST"
From: 100,
To: 1000,
}
} }
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
}

View File

@@ -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-gen-go v1.36.11
// protoc v5.28.2 // 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 state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache 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
sizeCache protoimpl.SizeCache
From int32 `protobuf:"varint,1,opt,name=from,proto3" json:"from,omitempty"`
To int32 `protobuf:"varint,2,opt,name=to,proto3" json:"to,omitempty"`
} }
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 state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache MaxConcurrency *RangeConfig `protobuf:"bytes,1,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"`
unknownFields protoimpl.UnknownFields MaxConnections *RangeConfig `protobuf:"bytes,2,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"`
CMaxReuseTimes *RangeConfig `protobuf:"bytes,3,opt,name=cMaxReuseTimes,proto3" json:"cMaxReuseTimes,omitempty"`
MaxConcurrency *RangeConfig `protobuf:"bytes,1,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"` HMaxRequestTimes *RangeConfig `protobuf:"bytes,4,opt,name=hMaxRequestTimes,proto3" json:"hMaxRequestTimes,omitempty"`
MaxConnections *RangeConfig `protobuf:"bytes,2,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"` HMaxReusableSecs *RangeConfig `protobuf:"bytes,5,opt,name=hMaxReusableSecs,proto3" json:"hMaxReusableSecs,omitempty"`
CMaxReuseTimes *RangeConfig `protobuf:"bytes,3,opt,name=cMaxReuseTimes,proto3" json:"cMaxReuseTimes,omitempty"` HKeepAlivePeriod int64 `protobuf:"varint,6,opt,name=hKeepAlivePeriod,proto3" json:"hKeepAlivePeriod,omitempty"`
HMaxRequestTimes *RangeConfig `protobuf:"bytes,4,opt,name=hMaxRequestTimes,proto3" json:"hMaxRequestTimes,omitempty"` unknownFields protoimpl.UnknownFields
HMaxReusableSecs *RangeConfig `protobuf:"bytes,5,opt,name=hMaxReusableSecs,proto3" json:"hMaxReusableSecs,omitempty"` sizeCache protoimpl.SizeCache
HKeepAlivePeriod int64 `protobuf:"varint,6,opt,name=hKeepAlivePeriod,proto3" json:"hKeepAlivePeriod,omitempty"`
} }
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 state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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
} }
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 ""
}
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 var File_transport_internet_splithttp_config_proto protoreflect.FileDescriptor
var file_transport_internet_splithttp_config_proto_rawDesc = []byte{ const file_transport_internet_splithttp_config_proto_rawDesc = "" +
0x0a, 0x29, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, "\n" +
0x72, 0x6e, 0x65, 0x74, 0x2f, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x63, ")transport/internet/splithttp/config.proto\x12!xray.transport.internet.splithttp\x1a\x1ftransport/internet/config.proto\"1\n" +
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, 0x78, 0x72, 0x61, "\vRangeConfig\x12\x12\n" +
0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, "\x04from\x18\x01 \x01(\x05R\x04from\x12\x0e\n" +
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x1a, 0x1f, "\x02to\x18\x02 \x01(\x05R\x02to\"\xf8\x03\n" +
0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, "\n" +
0x65, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, "XmuxConfig\x12V\n" +
0x31, 0x0a, 0x0b, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, "\x0emaxConcurrency\x18\x01 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConcurrency\x12V\n" +
0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, "\x0emaxConnections\x18\x02 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0emaxConnections\x12V\n" +
0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, "\x0ecMaxReuseTimes\x18\x03 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x0ecMaxReuseTimes\x12Z\n" +
0x74, 0x6f, 0x22, 0xf8, 0x03, 0x0a, 0x0a, 0x58, 0x6d, 0x75, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, "\x10hMaxRequestTimes\x18\x04 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxRequestTimes\x12Z\n" +
0x67, 0x12, 0x56, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, "\x10hMaxReusableSecs\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x10hMaxReusableSecs\x12*\n" +
0x6e, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, "\x10hKeepAlivePeriod\x18\x06 \x01(\x03R\x10hKeepAlivePeriod\"\xde\n" +
0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, "\n" +
0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, "\x06Config\x12\x12\n" +
0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" +
0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x56, 0x0a, 0x0e, 0x6d, 0x61, 0x78, "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, "\x04mode\x18\x03 \x01(\tR\x04mode\x12P\n" +
0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, "\aheaders\x18\x04 \x03(\v26.xray.transport.internet.splithttp.Config.HeadersEntryR\aheaders\x12T\n" +
0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, "\rxPaddingBytes\x18\x05 \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\rxPaddingBytes\x12\"\n" +
0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, "\fnoGRPCHeader\x18\x06 \x01(\bR\fnoGRPCHeader\x12 \n" +
0x67, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, "\vnoSSEHeader\x18\a \x01(\bR\vnoSSEHeader\x12^\n" +
0x73, 0x12, 0x56, 0x0a, 0x0e, 0x63, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, 0x73, 0x65, 0x54, 0x69, "\x12scMaxEachPostBytes\x18\b \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x12scMaxEachPostBytes\x12b\n" +
0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, "\x14scMinPostsIntervalMs\x18\t \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scMinPostsIntervalMs\x12.\n" +
0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, "\x12scMaxBufferedPosts\x18\n" +
0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, " \x01(\x03R\x12scMaxBufferedPosts\x12b\n" +
0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x63, 0x4d, 0x61, 0x78, 0x52, "\x14scStreamUpServerSecs\x18\v \x01(\v2..xray.transport.internet.splithttp.RangeConfigR\x14scStreamUpServerSecs\x12A\n" +
0x65, 0x75, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x10, 0x68, 0x4d, 0x61, "\x04xmux\x18\f \x01(\v2-.xray.transport.internet.splithttp.XmuxConfigR\x04xmux\x12Q\n" +
0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, "\x10downloadSettings\x18\r \x01(\v2%.xray.transport.internet.StreamConfigR\x10downloadSettings\x12*\n" +
0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, "\x10xPaddingObfsMode\x18\x0e \x01(\bR\x10xPaddingObfsMode\x12 \n" +
0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, "\vxPaddingKey\x18\x0f \x01(\tR\vxPaddingKey\x12&\n" +
0x6c, 0x69, 0x74, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, "\x0exPaddingHeader\x18\x10 \x01(\tR\x0exPaddingHeader\x12,\n" +
0x66, 0x69, 0x67, 0x52, 0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, "\x11xPaddingPlacement\x18\x11 \x01(\tR\x11xPaddingPlacement\x12&\n" +
0x54, 0x69, 0x6d, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, "\x0exPaddingMethod\x18\x12 \x01(\tR\x0exPaddingMethod\x12*\n" +
0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x63, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, "\x10uplinkHTTPMethod\x18\x13 \x01(\tR\x10uplinkHTTPMethod\x12*\n" +
0x2e, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, "\x10sessionPlacement\x18\x14 \x01(\tR\x10sessionPlacement\x12\x1e\n" +
0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x68, "\n" +
0x74, 0x74, 0x70, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, "sessionKey\x18\x15 \x01(\tR\n" +
0x10, 0x68, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x63, "sessionKey\x12\"\n" +
0x73, 0x12, 0x2a, 0x0a, 0x10, 0x68, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x50, "\fseqPlacement\x18\x16 \x01(\tR\fseqPlacement\x12\x16\n" +
0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x68, 0x4b, 0x65, "\x06seqKey\x18\x17 \x01(\tR\x06seqKey\x120\n" +
0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x22, 0xdc, 0x06, "\x13uplinkDataPlacement\x18\x18 \x01(\tR\x13uplinkDataPlacement\x12$\n" +
0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, "\ruplinkDataKey\x18\x19 \x01(\tR\ruplinkDataKey\x12(\n" +
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, "\x0fuplinkChunkSize\x18\x1a \x01(\rR\x0fuplinkChunkSize\x1a:\n" +
0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, "\fHeadersEntry\x12\x10\n" +
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
0x6d, 0x6f, 0x64, 0x65, 0x12, 0x50, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x85\x01\n" +
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, "%com.xray.transport.internet.splithttpP\x01Z6github.com/xtls/xray-core/transport/internet/splithttp\xaa\x02!Xray.Transport.Internet.SplitHttpb\x06proto3"
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,
}
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
} }

View File

@@ -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;
} }

View File

@@ -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() sessionId := ""
requestURL.Path = transportConfiguration.GetNormalizedPath() + sessionIdUuid.String() 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()),
) )

View File

@@ -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 !h.config.IsPaddingValid(paddingValue, validRange.From, validRange.To, PaddingMethod(h.config.XPaddingMethod)) {
if referrer != "" { errors.LogInfo(context.Background(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue)))
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))
writer.WriteHeader(http.StatusBadRequest) writer.WriteHeader(http.StatusBadRequest)
return return
} }
sessionId := "" sessionId, seqStr := h.config.ExtractMetaFromRequest(request, h.path)
subpath := strings.Split(request.URL.Path[len(h.path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
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 request.Method == "POST" && sessionId != "" { // stream-up, packet-up if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
seq := "" isUplinkRequest = true
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 {

View File

@@ -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
}
}

View File

@@ -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