Skip to content
5 changes: 2 additions & 3 deletions app/dns/nameserver_doh.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"

utls "github.com/refraction-networking/utls"
Expand All @@ -20,6 +19,7 @@ import (
"github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils"
dns_feature "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet"
Expand Down Expand Up @@ -214,8 +214,7 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,

req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message")

req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000))))
req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))

hc := s.httpClient

Expand Down
24 changes: 24 additions & 0 deletions common/utils/padding.go
Original file line number Diff line number Diff line change
@@ -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 changes: 127 additions & 0 deletions infra/conf/transport_internet.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,19 @@ type SplitHTTPConfig struct {
Mode string `json:"mode"`
Headers map[string]string `json:"headers"`
XPaddingBytes Int32Range `json:"xPaddingBytes"`
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
XPaddingKey string `json:"xPaddingKey"`
XPaddingHeader string `json:"xPaddingHeader"`
XPaddingPlacement string `json:"xPaddingPlacement"`
XPaddingMethod string `json:"xPaddingMethod"`
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
SessionPlacement string `json:"sessionPlacement"`
SessionKey string `json:"sessionKey"`
SeqPlacement string `json:"seqPlacement"`
SeqKey string `json:"seqKey"`
UplinkDataPlacement string `json:"uplinkDataPlacement"`
UplinkDataKey string `json:"uplinkDataKey"`
UplinkChunkSize uint32 `json:"uplinkChunkSize"`
NoGRPCHeader bool `json:"noGRPCHeader"`
NoSSEHeader bool `json:"noSSEHeader"`
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
Expand Down Expand Up @@ -287,6 +300,107 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}

if c.XPaddingKey == "" {
c.XPaddingKey = "x_padding"
}

if c.XPaddingHeader == "" {
c.XPaddingHeader = "X-Padding"
}

switch c.XPaddingPlacement {
case "":
c.XPaddingPlacement = "queryInHeader"
case "cookie", "header", "query", "queryInHeader":
default:
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
}

switch c.XPaddingMethod {
case "":
c.XPaddingMethod = "repeat-x"
case "repeat-x", "tokenish":
default:
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
}

switch c.UplinkDataPlacement {
case "":
c.UplinkDataPlacement = "body"
case "cookie", "header":
if c.Mode != "packet-up" {
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
}
default:
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
}

if c.UplinkHTTPMethod == "" {
c.UplinkHTTPMethod = "POST"
}
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)

if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
}

switch c.SessionPlacement {
case "":
c.SessionPlacement = "path"
case "cookie", "header", "query":
default:
return nil, errors.New("unsupported session placement: " + c.SessionPlacement)
}

switch c.SeqPlacement {
case "":
c.SeqPlacement = "path"
case "cookie", "header", "query":
if c.SessionPlacement == "path" {
return nil, errors.New("SeqPlacement must be path when SessionPlacement is path")
}
default:
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
}

if c.SessionPlacement != "path" && c.SessionKey == "" {
switch c.SessionPlacement {
case "cookie", "query":
c.SessionKey = "x_session"
case "header":
c.SessionKey = "X-Session"
}
}

if c.SeqPlacement != "path" && c.SeqKey == "" {
switch c.SeqPlacement {
case "cookie", "query":
c.SeqKey = "x_seq"
case "header":
c.SeqKey = "X-Seq"
}
}

if c.UplinkDataPlacement != "body" && c.UplinkDataKey == "" {
switch c.UplinkDataPlacement {
case "cookie":
c.UplinkDataKey = "x_data"
case "header":
c.UplinkDataKey = "X-Data"
}
}

if c.UplinkChunkSize == 0 {
switch c.UplinkDataPlacement {
case "cookie":
c.UplinkChunkSize = 3 * 1024 // 3KB
case "header":
c.UplinkChunkSize = 4 * 1024 // 4KB
}
} else if c.UplinkChunkSize < 64 {
c.UplinkChunkSize = 64
}

if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
}
Expand All @@ -305,6 +419,19 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
Mode: c.Mode,
Headers: c.Headers,
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
XPaddingObfsMode: c.XPaddingObfsMode,
XPaddingKey: c.XPaddingKey,
XPaddingHeader: c.XPaddingHeader,
XPaddingPlacement: c.XPaddingPlacement,
XPaddingMethod: c.XPaddingMethod,
UplinkHTTPMethod: c.UplinkHTTPMethod,
SessionPlacement: c.SessionPlacement,
SeqPlacement: c.SeqPlacement,
SessionKey: c.SessionKey,
SeqKey: c.SeqKey,
UplinkDataPlacement: c.UplinkDataPlacement,
UplinkDataKey: c.UplinkDataKey,
UplinkChunkSize: c.UplinkChunkSize,
NoGRPCHeader: c.NoGRPCHeader,
NoSSEHeader: c.NoSSEHeader,
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
Expand Down
54 changes: 50 additions & 4 deletions transport/internet/splithttp/browser_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,35 @@ func (c *BrowserDialerClient) IsClosed() bool {
panic("not implemented yet")
}

func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) {
func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, _ string, body io.Reader, uploadOnly bool) (io.ReadCloser, net.Addr, net.Addr, error) {
if body != nil {
return nil, nil, nil, errors.New("bidirectional streaming for browser dialer not implemented yet")
}

conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader(url))
header := c.transportConfig.GetRequestHeader()
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}

if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}

c.transportConfig.ApplyXPaddingToHeader(header, config)

conn, err := browser_dialer.DialGet(url, header)
dummyAddr := &net.IPAddr{}
if err != nil {
return nil, dummyAddr, dummyAddr, err
Expand All @@ -33,13 +56,36 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
return websocket.NewConnection(conn, dummyAddr, nil, 0), conn.RemoteAddr(), conn.LocalAddr(), nil
}

func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error {
func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, _ string, _ string, body io.Reader, contentLength int64) error {
bytes, err := io.ReadAll(body)
if err != nil {
return err
}

err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(url), bytes)
header := c.transportConfig.GetRequestHeader()
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
config := XPaddingConfig{Length: length}

if c.transportConfig.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.transportConfig.XPaddingPlacement,
Key: c.transportConfig.XPaddingKey,
Header: c.transportConfig.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}

c.transportConfig.ApplyXPaddingToHeader(header, config)

err = browser_dialer.DialPost(url, header, bytes)
if err != nil {
return err
}
Expand Down
Loading