Skip to content

Commit d4d87e7

Browse files
authored
Move x_padding to Referer header
1 parent 30cb22a commit d4d87e7

File tree

6 files changed

+153
-48
lines changed

6 files changed

+153
-48
lines changed

transport/internet/browser_dialer/dialer.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
_ "embed"
77
"encoding/base64"
8+
"encoding/json"
89
"net/http"
910
"time"
1011

@@ -17,6 +18,12 @@ import (
1718
//go:embed dialer.html
1819
var webpage []byte
1920

21+
type task struct {
22+
Method string `json:"method"`
23+
URL string `json:"url"`
24+
Extra any `json:"extra,omitempty"`
25+
}
26+
2027
var conns chan *websocket.Conn
2128

2229
var upgrader = &websocket.Upgrader{
@@ -55,23 +62,69 @@ func HasBrowserDialer() bool {
5562
return conns != nil
5663
}
5764

65+
type webSocketExtra struct {
66+
Protocol string `json:"protocol,omitempty"`
67+
}
68+
5869
func DialWS(uri string, ed []byte) (*websocket.Conn, error) {
59-
data := []byte("WS " + uri)
70+
task := task{
71+
Method: "WS",
72+
URL: uri,
73+
}
74+
6075
if ed != nil {
61-
data = append(data, " "+base64.RawURLEncoding.EncodeToString(ed)...)
76+
task.Extra = webSocketExtra{
77+
Protocol: base64.RawURLEncoding.EncodeToString(ed),
78+
}
6279
}
6380

64-
return dialRaw(data)
81+
return dialTask(task)
6582
}
6683

67-
func DialGet(uri string) (*websocket.Conn, error) {
68-
data := []byte("GET " + uri)
69-
return dialRaw(data)
84+
type httpExtra struct {
85+
Referrer string `json:"referrer,omitempty"`
86+
Headers map[string]string `json:"headers,omitempty"`
7087
}
7188

72-
func DialPost(uri string, payload []byte) error {
73-
data := []byte("POST " + uri)
74-
conn, err := dialRaw(data)
89+
func httpExtraFromHeaders(headers http.Header) *httpExtra {
90+
if len(headers) == 0 {
91+
return nil
92+
}
93+
94+
extra := httpExtra{}
95+
if referrer := headers.Get("Referer"); referrer != "" {
96+
extra.Referrer = referrer
97+
headers.Del("Referer")
98+
}
99+
100+
if len(headers) > 0 {
101+
extra.Headers = make(map[string]string)
102+
for header := range headers {
103+
extra.Headers[header] = headers.Get(header)
104+
}
105+
}
106+
107+
return &extra
108+
}
109+
110+
func DialGet(uri string, headers http.Header) (*websocket.Conn, error) {
111+
task := task{
112+
Method: "GET",
113+
URL: uri,
114+
Extra: httpExtraFromHeaders(headers),
115+
}
116+
117+
return dialTask(task)
118+
}
119+
120+
func DialPost(uri string, headers http.Header, payload []byte) error {
121+
task := task{
122+
Method: "POST",
123+
URL: uri,
124+
Extra: httpExtraFromHeaders(headers),
125+
}
126+
127+
conn, err := dialTask(task)
75128
if err != nil {
76129
return err
77130
}
@@ -90,7 +143,12 @@ func DialPost(uri string, payload []byte) error {
90143
return nil
91144
}
92145

93-
func dialRaw(data []byte) (*websocket.Conn, error) {
146+
func dialTask(task task) (*websocket.Conn, error) {
147+
data, err := json.Marshal(task)
148+
if err != nil {
149+
return nil, err
150+
}
151+
94152
var conn *websocket.Conn
95153
for {
96154
conn = <-conns
@@ -100,7 +158,7 @@ func dialRaw(data []byte) (*websocket.Conn, error) {
100158
break
101159
}
102160
}
103-
err := CheckOK(conn)
161+
err = CheckOK(conn)
104162
if err != nil {
105163
return nil, err
106164
}

transport/internet/browser_dialer/dialer.html

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,28 @@
1414
let upstreamGetCount = 0;
1515
let upstreamWsCount = 0;
1616
let upstreamPostCount = 0;
17+
18+
function prepareRequestInit(extra) {
19+
const requestInit = {};
20+
if (extra.referrer) {
21+
// note: we have to strip the protocol and host part.
22+
// Browsers disallow that, and will reset the value to current page if attempted.
23+
const referrer = URL.parse(extra.referrer);
24+
requestInit.referrer = referrer.pathname + referrer.search + referrer.hash;
25+
requestInit.referrerPolicy = "unsafe-url";
26+
}
27+
28+
if (extra.headers) {
29+
requestInit.headers = extra.headers;
30+
}
31+
32+
return requestInit;
33+
}
34+
1735
let check = function () {
1836
if (clientIdleCount > 0) {
1937
return;
20-
};
38+
}
2139
clientIdleCount += 1;
2240
console.log("Prepare", url);
2341
let ws = new WebSocket(url);
@@ -29,12 +47,12 @@
2947
// double-checking that this continues to work
3048
ws.onmessage = function (event) {
3149
clientIdleCount -= 1;
32-
let [method, url, protocol] = event.data.split(" ");
33-
switch (method) {
50+
let task = JSON.parse(event.data);
51+
switch (task.method) {
3452
case "WS": {
3553
upstreamWsCount += 1;
36-
console.log("Dial WS", url, protocol);
37-
const wss = new WebSocket(url, protocol);
54+
console.log("Dial WS", task.url, task.extra.protocol);
55+
const wss = new WebSocket(task.url, task.extra.protocol);
3856
wss.binaryType = "arraybuffer";
3957
let opened = false;
4058
ws.onmessage = function (event) {
@@ -60,10 +78,12 @@
6078
wss.close()
6179
};
6280
break;
63-
};
81+
}
6482
case "GET": {
6583
(async () => {
66-
console.log("Dial GET", url);
84+
const requestInit = prepareRequestInit(task.extra);
85+
86+
console.log("Dial GET", task.url);
6787
ws.send("ok");
6888
const controller = new AbortController();
6989

@@ -83,16 +103,18 @@
83103
ws.onclose = (event) => {
84104
try {
85105
reader && reader.cancel();
86-
} catch(e) {};
106+
} catch(e) {}
87107

88108
try {
89109
controller.abort();
90-
} catch(e) {};
110+
} catch(e) {}
91111
};
92112

93113
try {
94114
upstreamGetCount += 1;
95-
const response = await fetch(url, {signal: controller.signal});
115+
116+
requestInit.signal = controller.signal;
117+
const response = await fetch(task.url, requestInit);
96118

97119
const body = await response.body;
98120
reader = body.getReader();
@@ -101,40 +123,42 @@
101123
const { done, value } = await reader.read();
102124
ws.send(value);
103125
if (done) break;
104-
};
126+
}
105127
} finally {
106128
upstreamGetCount -= 1;
107129
console.log("Dial GET DONE, remaining: ", upstreamGetCount);
108130
ws.close();
109-
};
131+
}
110132
})();
111133
break;
112-
};
134+
}
113135
case "POST": {
114136
upstreamPostCount += 1;
115-
console.log("Dial POST", url);
137+
138+
const requestInit = prepareRequestInit(task.extra);
139+
requestInit.method = "POST";
140+
141+
console.log("Dial POST", task.url);
116142
ws.send("ok");
117143
ws.onmessage = async (event) => {
118144
try {
119-
const response = await fetch(
120-
url,
121-
{method: "POST", body: event.data}
122-
);
145+
requestInit.body = event.data;
146+
const response = await fetch(task.url, requestInit);
123147
if (response.ok) {
124148
ws.send("ok");
125149
} else {
126150
console.error("bad status code");
127151
ws.send("fail");
128-
};
152+
}
129153
} finally {
130154
upstreamPostCount -= 1;
131155
console.log("Dial POST DONE, remaining: ", upstreamPostCount);
132156
ws.close();
133-
};
157+
}
134158
};
135159
break;
136-
};
137-
};
160+
}
161+
}
138162

139163
check();
140164
};

transport/internet/splithttp/browser_client.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import (
99
"github.com/xtls/xray-core/transport/internet/websocket"
1010
)
1111

12-
// implements splithttp.DialerClient in terms of browser dialer
13-
// has no fields because everything is global state :O)
14-
type BrowserDialerClient struct{}
12+
// BrowserDialerClient implements splithttp.DialerClient in terms of browser dialer
13+
type BrowserDialerClient struct {
14+
transportConfig *Config
15+
}
1516

1617
func (c *BrowserDialerClient) IsClosed() bool {
1718
panic("not implemented yet")
@@ -22,7 +23,7 @@ func (c *BrowserDialerClient) OpenStream(ctx context.Context, url string, body i
2223
panic("not implemented yet")
2324
}
2425

25-
conn, err := browser_dialer.DialGet(url)
26+
conn, err := browser_dialer.DialGet(url, c.transportConfig.GetRequestHeader())
2627
dummyAddr := &gonet.IPAddr{}
2728
if err != nil {
2829
return nil, dummyAddr, dummyAddr, err
@@ -37,7 +38,7 @@ func (c *BrowserDialerClient) PostPacket(ctx context.Context, url string, body i
3738
return err
3839
}
3940

40-
err = browser_dialer.DialPost(url, bytes)
41+
err = browser_dialer.DialPost(url, c.transportConfig.GetRequestHeader(), bytes)
4142
if err != nil {
4243
return err
4344
}

transport/internet/splithttp/config.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/xtls/xray-core/transport/internet"
1212
)
1313

14+
const referrerHeaderPaddingPrefix = "https://padding.xray.internal/?x_padding="
15+
1416
func (c *Config) GetNormalizedPath() string {
1517
pathAndQuery := strings.SplitN(c.Path, "?", 2)
1618
path := pathAndQuery[0]
@@ -39,11 +41,6 @@ func (c *Config) GetNormalizedQuery() string {
3941
}
4042
query += "x_version=" + core.Version()
4143

42-
paddingLen := c.GetNormalizedXPaddingBytes().rand()
43-
if paddingLen > 0 {
44-
query += "&x_padding=" + strings.Repeat("0", int(paddingLen))
45-
}
46-
4744
return query
4845
}
4946

@@ -53,6 +50,15 @@ func (c *Config) GetRequestHeader() http.Header {
5350
header.Add(k, v)
5451
}
5552

53+
paddingLen := c.GetNormalizedXPaddingBytes().rand()
54+
if paddingLen > 0 {
55+
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
56+
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
57+
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
58+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
59+
// h3's similar QPACK feature uses the same huffman table.
60+
header.Set("Referer", referrerHeaderPaddingPrefix+strings.Repeat("X", int(paddingLen)))
61+
}
5662
return header
5763
}
5864

@@ -63,7 +69,7 @@ func (c *Config) WriteResponseHeader(writer http.ResponseWriter) {
6369
writer.Header().Set("X-Version", core.Version())
6470
paddingLen := c.GetNormalizedXPaddingBytes().rand()
6571
if paddingLen > 0 {
66-
writer.Header().Set("X-Padding", strings.Repeat("0", int(paddingLen)))
72+
writer.Header().Set("X-Padding", strings.Repeat("X", int(paddingLen)))
6773
}
6874
}
6975

transport/internet/splithttp/dialer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func getHTTPClient(ctx context.Context, dest net.Destination, streamSettings *in
5454
realityConfig := reality.ConfigFromStreamSettings(streamSettings)
5555

5656
if browser_dialer.HasBrowserDialer() && realityConfig != nil {
57-
return &BrowserDialerClient{}, nil
57+
return &BrowserDialerClient{transportConfig: streamSettings.ProtocolSettings.(*Config)}, nil
5858
}
5959

6060
globalDialerAccess.Lock()

transport/internet/splithttp/hub.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
gonet "net"
99
"net/http"
10+
"net/url"
1011
"strconv"
1112
"strings"
1213
"sync"
@@ -110,9 +111,24 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
110111
}
111112

112113
validRange := h.config.GetNormalizedXPaddingBytes()
113-
x_padding := int32(len(request.URL.Query().Get("x_padding")))
114-
if validRange.To > 0 && (x_padding < validRange.From || x_padding > validRange.To) {
115-
errors.LogInfo(context.Background(), "invalid x_padding length:", x_padding)
114+
paddingLength := -1
115+
116+
const paddingQuery = "x_padding"
117+
if referrerPadding := request.Header.Get("Referer"); referrerPadding != "" {
118+
// Browser dialer cannot control the host part of referrer header, so not checking it
119+
if referrerURL, err := url.Parse(referrerPadding); err == nil {
120+
if query := referrerURL.Query(); query.Has(paddingQuery) {
121+
paddingLength = len(query.Get(paddingQuery))
122+
}
123+
}
124+
}
125+
126+
if paddingLength == -1 {
127+
paddingLength = len(request.URL.Query().Get(paddingQuery))
128+
}
129+
130+
if validRange.To > 0 && (int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To) {
131+
errors.LogInfo(context.Background(), "invalid x_padding length:", int32(paddingLength))
116132
writer.WriteHeader(http.StatusBadRequest)
117133
return
118134
}

0 commit comments

Comments
 (0)