Match xray-core: keep connected XHTTP sessions alive; WebSocket early data
Two cross-checks against xray-core found separate CRITICAL divergences that stall real traffic: XHTTP: the native session reaper had a 5-minute idle timeout that xray-core does not have. In the default stream-up/stream-down (auto/H2) mode a long download rides inside the already-open GET/POST and generates no new HTTP requests, so the idle timer fired and tore the tunnel down mid-transfer (YouTube/large downloads stalling after a few minutes). Now mirror hub.go: give the downlink GET 30s to attach, and once connected stop reaping entirely -- the session lives for the life of the GET, cleaned up by handleXHTTPDownload's deferred delete. WebSocket: the server handshake ignored Sec-WebSocket-Protocol, silently dropping 0-RTT early data. Clients configured with ?ed=N put the VLESS/VMess request header there and send no first frame, so the server blocked forever waiting for a header that never arrived -- every ed= WS client stalled. Now decode the base64url early data, deliver it before the first frame, and echo the header back, matching transport/internet/websocket. Also match only the path (ignore the ?ed= query) when validating the WS path. Add TestVLESSOverWebSocketEarlyData. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -238,6 +238,86 @@ func TestVLESSOverWebSocket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestVLESSOverWebSocketEarlyData guards the 0-RTT path: xray clients configured
|
||||
// with ?ed=N carry the VLESS request header base64url-encoded in the
|
||||
// Sec-WebSocket-Protocol header and send no first frame. Dropping it (as the
|
||||
// handshake did before) makes the server block waiting for a header that never
|
||||
// arrives as a frame, stalling every ed= WebSocket client.
|
||||
func TestVLESSOverWebSocketEarlyData(t *testing.T) {
|
||||
echoPort, stopEcho := startEchoServer(t)
|
||||
defer stopEcho()
|
||||
_, port, id, stop := newTestInbound(t, "ws", "/vlws")
|
||||
defer stop()
|
||||
|
||||
raw, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer raw.Close()
|
||||
raw.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
// Entire VLESS header + first payload live in the early-data header; no frame.
|
||||
early := append(vlessHeader(id, echoPort), []byte("ping-ed")...)
|
||||
proto := base64.RawURLEncoding.EncodeToString(early)
|
||||
|
||||
var keyBytes [16]byte
|
||||
rand.Read(keyBytes[:])
|
||||
key := base64.StdEncoding.EncodeToString(keyBytes[:])
|
||||
req := "GET /vlws HTTP/1.1\r\n" +
|
||||
"Host: test\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
"Sec-WebSocket-Key: " + key + "\r\n" +
|
||||
"Sec-WebSocket-Protocol: " + proto + "\r\n" +
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n"
|
||||
if _, err := raw.Write([]byte(req)); err != nil {
|
||||
t.Fatalf("handshake write: %v", err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(raw)
|
||||
statusLine, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("read status: %v", err)
|
||||
}
|
||||
if !strings.Contains(statusLine, "101") {
|
||||
t.Fatalf("ws handshake not 101: %q", statusLine)
|
||||
}
|
||||
sawProto := false
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("read headers: %v", err)
|
||||
}
|
||||
if strings.Contains(line, proto) {
|
||||
sawProto = true
|
||||
}
|
||||
if line == "\r\n" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !sawProto {
|
||||
t.Fatalf("server did not echo Sec-WebSocket-Protocol")
|
||||
}
|
||||
|
||||
ws := &testWSConn{Conn: raw, r: br}
|
||||
buf := make([]byte, 0, 32)
|
||||
want := 2 + len("ping-ed")
|
||||
for len(buf) < want {
|
||||
chunk := make([]byte, 64)
|
||||
n, err := ws.Read(chunk)
|
||||
if err != nil {
|
||||
t.Fatalf("ws read (early data dropped?): %v (got %q)", err, buf)
|
||||
}
|
||||
buf = append(buf, chunk[:n]...)
|
||||
}
|
||||
if buf[0] != 0 {
|
||||
t.Fatalf("bad ws vless response: %v", buf[:2])
|
||||
}
|
||||
if string(buf[2:want]) != "ping-ed" {
|
||||
t.Fatalf("ws early-data echo mismatch: got %q", buf[2:want])
|
||||
}
|
||||
}
|
||||
|
||||
func TestVLESSOverXHTTPPacketUp(t *testing.T) {
|
||||
echoPort, stopEcho := startEchoServer(t)
|
||||
defer stopEcho()
|
||||
|
||||
Reference in New Issue
Block a user