Skip to content

Commit 5b849d5

Browse files
paqxFangliding
andauthored
XHTTP transport: New options for bypassing CDN's detection (#5414)
Usage: #5414 (comment) Closes #4346 --------- Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
1 parent 61e1153 commit 5b849d5

File tree

13 files changed

+1073
-208
lines changed

13 files changed

+1073
-208
lines changed

app/dns/nameserver_doh.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"io"
99
"net/http"
1010
"net/url"
11-
"strings"
1211
"time"
1312

1413
utls "github.com/refraction-networking/utls"
@@ -20,6 +19,7 @@ import (
2019
"github.com/xtls/xray-core/common/net/cnc"
2120
"github.com/xtls/xray-core/common/protocol/dns"
2221
"github.com/xtls/xray-core/common/session"
22+
"github.com/xtls/xray-core/common/utils"
2323
dns_feature "github.com/xtls/xray-core/features/dns"
2424
"github.com/xtls/xray-core/features/routing"
2525
"github.com/xtls/xray-core/transport/internet"
@@ -214,8 +214,7 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
214214

215215
req.Header.Add("Accept", "application/dns-message")
216216
req.Header.Add("Content-Type", "application/dns-message")
217-
218-
req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000))))
217+
req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))
219218

220219
hc := s.httpClient
221220

common/utils/padding.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package utils
2+
3+
import (
4+
"math/rand/v2"
5+
)
6+
7+
var (
8+
// 8 ÷ (397/62)
9+
h2packCorrectionFactor = 1.2493702770780857
10+
base62TotalCharsNum = 62
11+
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
12+
)
13+
14+
// H2Base62Pad generates a base62 padding string for HTTP/2 header
15+
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
16+
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
17+
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
18+
actualLen := int(actualLenFloat)
19+
result := make([]byte, actualLen)
20+
for i := range actualLen {
21+
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
22+
}
23+
return string(result)
24+
}

infra/conf/transport_internet.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,19 @@ type SplitHTTPConfig struct {
228228
Mode string `json:"mode"`
229229
Headers map[string]string `json:"headers"`
230230
XPaddingBytes Int32Range `json:"xPaddingBytes"`
231+
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
232+
XPaddingKey string `json:"xPaddingKey"`
233+
XPaddingHeader string `json:"xPaddingHeader"`
234+
XPaddingPlacement string `json:"xPaddingPlacement"`
235+
XPaddingMethod string `json:"xPaddingMethod"`
236+
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
237+
SessionPlacement string `json:"sessionPlacement"`
238+
SessionKey string `json:"sessionKey"`
239+
SeqPlacement string `json:"seqPlacement"`
240+
SeqKey string `json:"seqKey"`
241+
UplinkDataPlacement string `json:"uplinkDataPlacement"`
242+
UplinkDataKey string `json:"uplinkDataKey"`
243+
UplinkChunkSize uint32 `json:"uplinkChunkSize"`
231244
NoGRPCHeader bool `json:"noGRPCHeader"`
232245
NoSSEHeader bool `json:"noSSEHeader"`
233246
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
@@ -287,6 +300,107 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
287300
return nil, errors.New("xPaddingBytes cannot be disabled")
288301
}
289302

303+
if c.XPaddingKey == "" {
304+
c.XPaddingKey = "x_padding"
305+
}
306+
307+
if c.XPaddingHeader == "" {
308+
c.XPaddingHeader = "X-Padding"
309+
}
310+
311+
switch c.XPaddingPlacement {
312+
case "":
313+
c.XPaddingPlacement = "queryInHeader"
314+
case "cookie", "header", "query", "queryInHeader":
315+
default:
316+
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
317+
}
318+
319+
switch c.XPaddingMethod {
320+
case "":
321+
c.XPaddingMethod = "repeat-x"
322+
case "repeat-x", "tokenish":
323+
default:
324+
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
325+
}
326+
327+
switch c.UplinkDataPlacement {
328+
case "":
329+
c.UplinkDataPlacement = "body"
330+
case "cookie", "header":
331+
if c.Mode != "packet-up" {
332+
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
333+
}
334+
default:
335+
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
336+
}
337+
338+
if c.UplinkHTTPMethod == "" {
339+
c.UplinkHTTPMethod = "POST"
340+
}
341+
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)
342+
343+
if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
344+
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
345+
}
346+
347+
switch c.SessionPlacement {
348+
case "":
349+
c.SessionPlacement = "path"
350+
case "cookie", "header", "query":
351+
default:
352+
return nil, errors.New("unsupported session placement: " + c.SessionPlacement)
353+
}
354+
355+
switch c.SeqPlacement {
356+
case "":
357+
c.SeqPlacement = "path"
358+
case "cookie", "header", "query":
359+
if c.SessionPlacement == "path" {
360+
return nil, errors.New("SeqPlacement must be path when SessionPlacement is path")
361+
}
362+
default:
363+
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
364+
}
365+
366+
if c.SessionPlacement != "path" && c.SessionKey == "" {
367+
switch c.SessionPlacement {
368+
case "cookie", "query":
369+
c.SessionKey = "x_session"
370+
case "header":
371+
c.SessionKey = "X-Session"
372+
}
373+
}
374+
375+
if c.SeqPlacement != "path" && c.SeqKey == "" {
376+
switch c.SeqPlacement {
377+
case "cookie", "query":
378+
c.SeqKey = "x_seq"
379+
case "header":
380+
c.SeqKey = "X-Seq"
381+
}
382+
}
383+
384+
if c.UplinkDataPlacement != "body" && c.UplinkDataKey == "" {
385+
switch c.UplinkDataPlacement {
386+
case "cookie":
387+
c.UplinkDataKey = "x_data"
388+
case "header":
389+
c.UplinkDataKey = "X-Data"
390+
}
391+
}
392+
393+
if c.UplinkChunkSize == 0 {
394+
switch c.UplinkDataPlacement {
395+
case "cookie":
396+
c.UplinkChunkSize = 3 * 1024 // 3KB
397+
case "header":
398+
c.UplinkChunkSize = 4 * 1024 // 4KB
399+
}
400+
} else if c.UplinkChunkSize < 64 {
401+
c.UplinkChunkSize = 64
402+
}
403+
290404
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
291405
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
292406
}
@@ -305,6 +419,19 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
305419
Mode: c.Mode,
306420
Headers: c.Headers,
307421
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
422+
XPaddingObfsMode: c.XPaddingObfsMode,
423+
XPaddingKey: c.XPaddingKey,
424+
XPaddingHeader: c.XPaddingHeader,
425+
XPaddingPlacement: c.XPaddingPlacement,
426+
XPaddingMethod: c.XPaddingMethod,
427+
UplinkHTTPMethod: c.UplinkHTTPMethod,
428+
SessionPlacement: c.SessionPlacement,
429+
SeqPlacement: c.SeqPlacement,
430+
SessionKey: c.SessionKey,
431+
SeqKey: c.SeqKey,
432+
UplinkDataPlacement: c.UplinkDataPlacement,
433+
UplinkDataKey: c.UplinkDataKey,
434+
UplinkChunkSize: c.UplinkChunkSize,
308435
NoGRPCHeader: c.NoGRPCHeader,
309436
NoSSEHeader: c.NoSSEHeader,
310437
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),

transport/internet/splithttp/browser_client.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,35 @@ func (c *BrowserDialerClient) IsClosed() bool {
1919
panic("not implemented yet")
2020
}
2121

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

27-
conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader(url))
27+
header := c.transportConfig.GetRequestHeader()
28+
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
29+
config := XPaddingConfig{Length: length}
30+
31+
if c.transportConfig.XPaddingObfsMode {
32+
config.Placement = XPaddingPlacement{
33+
Placement: c.transportConfig.XPaddingPlacement,
34+
Key: c.transportConfig.XPaddingKey,
35+
Header: c.transportConfig.XPaddingHeader,
36+
RawURL: url,
37+
}
38+
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
39+
} else {
40+
config.Placement = XPaddingPlacement{
41+
Placement: PlacementQueryInHeader,
42+
Key: "x_padding",
43+
Header: "Referer",
44+
RawURL: url,
45+
}
46+
}
47+
48+
c.transportConfig.ApplyXPaddingToHeader(header, config)
49+
50+
conn, err := browser_dialer.DialGet(url, header)
2851
dummyAddr := &net.IPAddr{}
2952
if err != nil {
3053
return nil, dummyAddr, dummyAddr, err
@@ -33,13 +56,36 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
3356
return websocket.NewConnection(conn, dummyAddr, nil, 0), conn.RemoteAddr(), conn.LocalAddr(), nil
3457
}
3558

36-
func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error {
59+
func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, _ string, _ string, body io.Reader, contentLength int64) error {
3760
bytes, err := io.ReadAll(body)
3861
if err != nil {
3962
return err
4063
}
4164

42-
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(url), bytes)
65+
header := c.transportConfig.GetRequestHeader()
66+
length := int(c.transportConfig.GetNormalizedXPaddingBytes().rand())
67+
config := XPaddingConfig{Length: length}
68+
69+
if c.transportConfig.XPaddingObfsMode {
70+
config.Placement = XPaddingPlacement{
71+
Placement: c.transportConfig.XPaddingPlacement,
72+
Key: c.transportConfig.XPaddingKey,
73+
Header: c.transportConfig.XPaddingHeader,
74+
RawURL: url,
75+
}
76+
config.Method = PaddingMethod(c.transportConfig.XPaddingMethod)
77+
} else {
78+
config.Placement = XPaddingPlacement{
79+
Placement: PlacementQueryInHeader,
80+
Key: "x_padding",
81+
Header: "Referer",
82+
RawURL: url,
83+
}
84+
}
85+
86+
c.transportConfig.ApplyXPaddingToHeader(header, config)
87+
88+
err = browser_dialer.DialPost(url, header, bytes)
4389
if err != nil {
4490
return err
4591
}

0 commit comments

Comments
 (0)