diff --git a/xray_native.go b/xray_native.go index 6ae5768..b1d60bf 100644 --- a/xray_native.go +++ b/xray_native.go @@ -734,8 +734,6 @@ func wsServerHandshake(conn net.Conn, wantPath string) (*websocketConn, error) { if key == "" { return nil, errors.New("missing Sec-WebSocket-Key") } - // Match only the path portion; the configured path may carry an ?ed=N query - // (early-data hint) and the request path is already query-stripped by Go. if i := strings.IndexByte(wantPath, '?'); i >= 0 { wantPath = wantPath[:i] } @@ -743,12 +741,6 @@ func wsServerHandshake(conn net.Conn, wantPath string) (*websocketConn, error) { return nil, fmt.Errorf("ws path mismatch: got %q want %q", req.URL.Path, wantPath) } - // WebSocket early data (0-RTT): xray clients configured with ?ed=N carry the - // first bytes -- the VLESS/VMess request header -- base64url-encoded in the - // Sec-WebSocket-Protocol request header instead of a first frame. Decode it and - // feed it to Read before any frame, and echo the header back so the client's - // upgrade check is satisfied. Dropping this makes every ed= WS client stall, - // because the server would wait for a header that never arrives as a frame. var early []byte proto := req.Header.Get("Sec-WebSocket-Protocol") if proto != "" { @@ -778,7 +770,7 @@ func wsServerHandshake(conn net.Conn, wantPath string) (*websocketConn, error) { type websocketConn struct { net.Conn r *bufio.Reader - early []byte // 0-RTT early data delivered before the first frame + early []byte readBuf []byte // decoded payload not yet consumed by Read wmu sync.Mutex } diff --git a/xray_native_mux.go b/xray_native_mux.go index 903cbaf..6ce619f 100644 --- a/xray_native_mux.go +++ b/xray_native_mux.go @@ -48,19 +48,12 @@ type nativeMuxPacket struct { discard bool } -// nativeMuxUplinkItem is one client->backend datagram/segment handed from the -// shared mux read loop to a session's own uplink goroutine. The payload is a -// private copy because the read loop reuses its scratch buffer immediately. type nativeMuxUplinkItem struct { payload []byte host string port uint16 } -// nativeMuxUplinkQueue bounds how many un-written uplink items a single mux -// session may buffer before the shared read loop applies backpressure. This -// isolates a slow/backpressured backend to its own session instead of stalling -// every other session multiplexed on the same client connection. const nativeMuxUplinkQueue = 64 var nativeMuxFramePool = sync.Pool{ @@ -286,11 +279,6 @@ func (ib *nativeInbound) nativeVLESSMuxTunnel(stream io.ReadWriteCloser, uuid, e mu.Unlock() xrayTracef("native xray: vless/mux %s user=%s -> %s session=%d xudp=%v", nativeMuxNetworkName(meta.network), email, target, meta.sessionID, isXUDP) - // Dial and pump this session on its own goroutine so a slow-connecting - // target or a backpressured/rate-limited backend never blocks the shared - // read loop and therefore never stalls the other multiplexed sessions. - // The first payload is enqueued (a copy) before run() finishes dialing; - // the session's uplink loop writes it first once the backend is up. ib2, host2, port2 := ib, targetHost, targetPort xrayGo(fmt.Sprintf("native xray mux session=%d", s.id), func() { s.run(ib2, host2, port2) }) if len(pkt.payload) > 0 { @@ -344,9 +332,6 @@ func (ib *nativeInbound) nativeVLESSMuxTunnel(stream io.ReadWriteCloser, uuid, e } } -// newNativeMuxSession allocates a session and reserves a global slot but does -// NOT dial the backend. Dialing happens later in run() on the session's own -// goroutine, so the shared mux read loop is never blocked by a slow connect. func (ib *nativeInbound) newNativeMuxSession(id uint16, network byte, host string, port uint16, xudp bool, globalID [8]byte, client io.Writer, writeMu *sync.Mutex, uuid, email string, onClose func(*nativeMuxSession)) (*nativeMuxSession, string, error) { target := net.JoinHostPort(normalizeNativeTargetHost(host), strconv.Itoa(int(port))) if invalidNativeDestination(host, port) { @@ -378,15 +363,9 @@ func (ib *nativeInbound) newNativeMuxSession(id uint16, network byte, host strin return s, target, nil } -// run dials/opens the backend for a mux session, then pumps client->backend -// data from the session's uplink channel. It owns the backend reader goroutine. -// Because this runs off the shared read loop, a slow dial or a congested backend -// only ever affects this one session. func (s *nativeMuxSession) run(ib *nativeInbound, host string, port uint16) { defer xrayRecover(fmt.Sprintf("native xray mux run session=%d", s.id)) - // Abort early if the session was torn down (client sent End, connection - // closed, or a duplicate New replaced it) before we even dialed. select { case <-s.closed: s.failInit(false) @@ -414,7 +393,6 @@ func (s *nativeMuxSession) run(ib *nativeInbound, host string, port uint16) { s.udpTarget = udpTarget } - // If the session was closed while dialing, tear the backend down now. select { case <-s.closed: s.closeBackend() @@ -426,10 +404,6 @@ func (s *nativeMuxSession) run(ib *nativeInbound, host string, port uint16) { s.uplinkLoop() } -// failInit reports a session that never came up: optionally notify the client -// with an End(error) frame, remove it from the parent maps, and release the -// slot. It must not be used once a backend reader is running (readBackendLoop's -// deferred cleanup owns that path). func (s *nativeMuxSession) failInit(notifyClient bool) { if notifyClient { s.writeMu.Lock() @@ -442,10 +416,6 @@ func (s *nativeMuxSession) failInit(notifyClient bool) { s.closeBackend() } -// enqueueUplink hands one client->backend datagram/segment to the session's -// uplink goroutine. The payload is copied because the caller (the shared read -// loop) reuses its scratch buffer on the next iteration. A send blocks only when -// this one session's queue is full (per-session backpressure) or once closed. func (s *nativeMuxSession) enqueueUplink(payload []byte, host string, port uint16) { if len(payload) == 0 { return @@ -458,12 +428,7 @@ func (s *nativeMuxSession) enqueueUplink(payload []byte, host string, port uint1 } } -// uplinkLoop drains queued client->backend items until the session closes or a -// backend write fails. Rate limiting and the blocking socket write now happen -// here instead of in the shared read loop. func (s *nativeMuxSession) uplinkLoop() { - // upMeter is owned exclusively by this goroutine (writeBackendItem adds to it - // here), so it is flushed here too. readBackendLoop must not touch upMeter. defer s.upMeter.flush() for { select { @@ -506,10 +471,6 @@ func (ib *nativeInbound) nativeOpenMuxUDP(host string, port uint16) (net.PacketC return pc, udpNetwork, udpTarget, target, nil } -// writeBackendItem writes one uplink item to the backend. It returns false when -// the session should be torn down (rate wait cancelled or a fatal write error). -// A per-packet UDP sink/override-resolve failure is a soft skip and returns true -// so the session keeps serving other datagrams. func (s *nativeMuxSession) writeBackendItem(item nativeMuxUplinkItem) bool { payload := item.payload if len(payload) == 0 { @@ -548,6 +509,9 @@ func (s *nativeMuxSession) writeBackendItem(item nativeMuxUplinkItem) bool { } n, err = s.udp.WriteTo(payload, target) } + if s.network == nativeMuxNetworkUDP && err == nil { + _ = s.udp.SetReadDeadline(time.Now().Add(nativeMuxUDPIdleTimeout())) + } if n > 0 { s.upMeter.add(n) } @@ -562,7 +526,6 @@ func (s *nativeMuxSession) readBackendLoop() { defer xrayRecover(fmt.Sprintf("native xray mux backend loop session=%d", s.id)) sendEnd := true defer func() { - // downMeter is owned by this goroutine; upMeter is flushed by uplinkLoop. s.downMeter.flush() if sendEnd { s.writeMu.Lock() @@ -653,9 +616,6 @@ func (s *nativeMuxSession) readUDPBackendLoop() bool { } } -// closeBackend tears the session down exactly once. It is safe to call -// concurrently from the read loop, the uplink loop and the backend reader; the -// previous select/default form could double-close s.closed and panic. func (s *nativeMuxSession) closeBackend() { s.closeOnce.Do(func() { close(s.closed) diff --git a/xray_native_test.go b/xray_native_test.go index a6059c8..c111dc6 100644 --- a/xray_native_test.go +++ b/xray_native_test.go @@ -238,11 +238,6 @@ 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() @@ -256,7 +251,6 @@ func TestVLESSOverWebSocketEarlyData(t *testing.T) { 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) @@ -477,17 +471,6 @@ func TestVLESSOverXHTTPPacketUpGET(t *testing.T) { } } -// TestVLESSOverXHTTPOutOfOrderPacketUp is the regression guard for the burst -// stall that made video/large downloads unusable ("download ~10MB, stall, -// repeat"). Packet-up POSTs arrive out of order (highest sequence first) and each -// POST is fully awaited before the next is sent — exactly what happens when the -// client's concurrent-POST slots fill while a low sequence is still in flight. -// -// The old queue blocked each POST until the tunnel reader consumed that exact -// payload, so a POST carrying seq=2 could never return before seq=0/1 arrived — -// a deadlock that surfaced as periodic stalls. The xray-core-style queue acks -// every POST as soon as it is buffered and reassembles server-side, so this test -// completes quickly and the payload is delivered in order. func TestVLESSOverXHTTPOutOfOrderPacketUp(t *testing.T) { echoPort, stopEcho := startEchoServer(t) defer stopEcho() @@ -543,14 +526,10 @@ func TestVLESSOverXHTTPOutOfOrderPacketUp(t *testing.T) { t.Fatalf("xhttp GET did not open") } - // Full uplink stream = VLESS header + body, split into 3 sequenced chunks. full := append(vlessHeader(id, echoPort), []byte("reordered-payload-body")...) third := len(full) / 3 chunks := [][]byte{full[:third], full[third : 2*third], full[2*third:]} - // Send the POSTs highest-seq-first, awaiting each response before the next. - // On the old block-until-consumed queue, the very first POST (seq 2) would - // hang until the 4s client timeout because seq 0/1 have not arrived yet. for _, seq := range []int{2, 1, 0} { start := time.Now() postResp, err := client.Post(baseURL+"/"+itoa(seq), "application/octet-stream", bytes.NewReader(chunks[seq])) @@ -1060,12 +1039,6 @@ func TestVLESSMuxTCPDoesNotStall(t *testing.T) { } } -// TestVLESSMuxSlowDialDoesNotBlockOtherSessions is the regression guard for the -// head-of-line stall that made the panel proxy "hang with multiple users": a -// single mux session whose target is slow to connect must not freeze the other -// sessions multiplexed on the same client connection. Session 1 targets a -// blackhole address (a connect that hangs until the 10s dial timeout); session 2 -// targets a live echo server and must respond promptly regardless. func TestVLESSMuxSlowDialDoesNotBlockOtherSessions(t *testing.T) { tcpPort, stopTCP := startEchoServer(t) defer stopTCP() @@ -1087,21 +1060,14 @@ func TestVLESSMuxSlowDialDoesNotBlockOtherSessions(t *testing.T) { t.Fatalf("read mux response header: %v", err) } - // Session 1: blackhole target (TEST-NET-1, RFC 5737) — connect will hang for - // the full dial timeout. On the old synchronous read loop this alone blocked - // every subsequent frame for up to 10s. if _, err := conn.Write(buildMuxTCPFrame(1, "192.0.2.1", 80, []byte("slow"))); err != nil { t.Fatalf("write slow mux frame: %v", err) } - // Session 2: live echo server. Must round-trip well within the 4s deadline, - // i.e. long before session 1's 10s dial timeout could ever return. want := []byte("fast-session") if _, err := conn.Write(buildMuxTCPFrame(2, "127.0.0.1", tcpPort, want)); err != nil { t.Fatalf("write fast mux frame: %v", err) } - // The blackhole session cannot produce data, so the first frame back must be - // session 2's echo. meta, err := readNativeMuxMetadata(conn) if err != nil { t.Fatalf("read fast session response meta (head-of-line stall?): %v", err)