Align native Xray with xray-core; drop dead knobs; split admin app.js
Fix the two reliability problems in the in-process Xray emulator by matching XTLS/Xray-core's transport semantics: - XHTTP upload queue: rewrite as a faithful port of xray-core's uploadQueue (bounded channel + sequence reorder heap). Packet-up POSTs are now acked immediately on buffering instead of blocking until the tunnel reader consumes them. The old block-until-consumed behavior throttled the uplink to the reassembly rate and deadlocked against the client's concurrent-POST limit, which showed up as "download a burst, stall, repeat" on video/large downloads. - Mux: dial the backend and pump uplink on a per-session goroutine fed by a bounded channel (mirrors xray-core's per-session buffered pipe). Previously the dial and backend writes ran inline in the shared read loop, so one slow target or backpressured session stalled every other muxed session. - XHTTP download writer: flush every write (matches httpServerConn.Write) instead of batching behind a 2ms/32KB window. - XHTTP: enforce a single download (stream-down) per session to stop two GETs from splitting the decoded stream and corrupting the tunnel. - Fix a close-of-closed-channel race in the mux session teardown (sync.Once). Remove the now-inert XHTTP tuning knobs (xhttp_queue_timeout_ms, xhttp_flush_ms, xhttp_flush_bytes) from the backend struct and the admin panel UI. Split admin/assets/app.js into ordered classic-script modules under admin/assets/js/ for maintainability. The concatenation is byte-identical to the old file and load order is preserved via defer, so behavior is unchanged. Add regression tests for the mux head-of-line stall and the out-of-order packet-up burst-stall; add golang.org/x/text to go.mod so tests build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -397,6 +397,107 @@ 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()
|
||||
|
||||
var id [16]byte
|
||||
if _, err := rand.Read(id[:]); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
ib := &nativeInbound{
|
||||
tag: "test-xhttp-reorder",
|
||||
protocol: "vless",
|
||||
transport: "xhttp",
|
||||
path: normalizeXHTTPPath("/xhttp"),
|
||||
xhttpMode: "packet-up",
|
||||
xhttpMaxEachPostBytes: 1_000_000,
|
||||
xhttpMaxBufferedPosts: 30,
|
||||
xhttpSessions: make(map[string]*nativeXHTTPSession),
|
||||
clientsByID: map[[16]byte]*nativeXrayClient{id: {id: id, uuid: "test", email: "test@t"}},
|
||||
}
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("xhttp listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
go ib.serveXHTTPListener(ln)
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
client := &http.Client{Timeout: 4 * time.Second}
|
||||
session := "session-reorder"
|
||||
baseURL := "http://" + net.JoinHostPort("127.0.0.1", itoa(port)) + "/xhttp/" + session
|
||||
|
||||
respCh := make(chan *http.Response, 1)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
resp, err := client.Get(baseURL)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
respCh <- resp
|
||||
}()
|
||||
|
||||
var resp *http.Response
|
||||
select {
|
||||
case resp = <-respCh:
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("xhttp GET status: %s", resp.Status)
|
||||
}
|
||||
case err := <-errCh:
|
||||
t.Fatalf("xhttp GET: %v", err)
|
||||
case <-time.After(2 * time.Second):
|
||||
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]))
|
||||
if err != nil {
|
||||
t.Fatalf("xhttp POST seq=%d (head-of-line stall?): %v", seq, err)
|
||||
}
|
||||
postResp.Body.Close()
|
||||
if postResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("xhttp POST seq=%d status: %s", seq, postResp.Status)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 2*time.Second {
|
||||
t.Fatalf("xhttp POST seq=%d took %v; expected an immediate ack (not blocked on consumption)", seq, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
got := make([]byte, 2+len("reordered-payload-body"))
|
||||
if _, err := io.ReadFull(resp.Body, got); err != nil {
|
||||
t.Fatalf("xhttp read response: %v", err)
|
||||
}
|
||||
if got[0] != 0 {
|
||||
t.Fatalf("bad xhttp vless response: %v", got[:2])
|
||||
}
|
||||
if string(got[2:]) != "reordered-payload-body" {
|
||||
t.Fatalf("xhttp reassembled echo mismatch: got %q", got[2:])
|
||||
}
|
||||
}
|
||||
|
||||
func TestXHTTPRejectsBrowserGETWithoutSession(t *testing.T) {
|
||||
var id [16]byte
|
||||
if _, err := rand.Read(id[:]); err != nil {
|
||||
@@ -878,3 +979,61 @@ func TestVLESSMuxTCPDoesNotStall(t *testing.T) {
|
||||
t.Fatalf("mux tcp echo mismatch: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
_, port, id, stop := newTestInbound(t, "tcp", "")
|
||||
defer stop()
|
||||
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", itoa(port)))
|
||||
if err != nil {
|
||||
t.Fatalf("dial inbound: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.SetDeadline(time.Now().Add(4 * time.Second))
|
||||
|
||||
if _, err := conn.Write(vlessMuxHeader(id)); err != nil {
|
||||
t.Fatalf("write mux header: %v", err)
|
||||
}
|
||||
resp := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||
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)
|
||||
}
|
||||
if meta.sessionID != 2 || meta.status != nativeMuxStatusKeep || meta.option&nativeMuxOptionData == 0 {
|
||||
t.Fatalf("expected session 2 keep-data frame, got: %#v", meta)
|
||||
}
|
||||
got, err := readNativeMuxDataBlock(conn)
|
||||
if err != nil {
|
||||
t.Fatalf("read fast session payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("fast session echo mismatch: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user