Fix XHTTP regression: drop download-reject guard, widen reorder buffer
The single-download guard added earlier rejected a second stream-down GET for a session with HTTP 400. On a client reconnect (network hiccup / H2 retry) while the previous download handler is still blocked on a dead socket, that 400 makes the client tear the whole session down and every retry keeps failing -- the tunnel stops passing data entirely. xray-core never rejects a re-GET, so remove the guard and match it. Also raise the default XHTTP packet-up reorder buffer from 30 to 512. xray-core uses 30 because its own client sends POSTs near-in-order, but other clients fan out many concurrent POSTs that arrive well out of order; 30 tripped the reassembly-too-large teardown and stalled traffic. The per-inbound scMaxBufferedPosts still overrides this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,11 +34,14 @@ const (
|
|||||||
fixedNativeMuxUDPWriteBuffer = 256 * 1024 // mux UDP socket write buffer
|
fixedNativeMuxUDPWriteBuffer = 256 * 1024 // mux UDP socket write buffer
|
||||||
|
|
||||||
// XHTTP: max tracked sessions (DoS guard) and the packet-up reorder buffer.
|
// XHTTP: max tracked sessions (DoS guard) and the packet-up reorder buffer.
|
||||||
// defaultNativeXHTTPBufferedPosts matches xray-core's scMaxBufferedPosts
|
// The per-inbound scMaxBufferedPosts from the config still overrides this.
|
||||||
// default; the per-inbound scMaxBufferedPosts from the config still overrides
|
// xray-core's own default is 30 (its client sends POSTs near-in-order), but
|
||||||
// it, exactly like upstream.
|
// other clients (v2rayNG/nekobox/etc.) fan out many concurrent POSTs that can
|
||||||
|
// arrive well out of order; a small buffer then trips the reassembly-too-large
|
||||||
|
// teardown and stalls traffic. Keep a generous default so reordering is
|
||||||
|
// absorbed rather than fatal.
|
||||||
defaultNativeXHTTPMaxSessions = 16384
|
defaultNativeXHTTPMaxSessions = 16384
|
||||||
defaultNativeXHTTPBufferedPosts = 30
|
defaultNativeXHTTPBufferedPosts = 512
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+11
-19
@@ -659,13 +659,12 @@ func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Requ
|
|||||||
sess.touch()
|
sess.touch()
|
||||||
defer xrayRecover(fmt.Sprintf("native xray XHTTP download inbound=%q session=%q remote=%s", ib.tag, sessionID, r.RemoteAddr))
|
defer xrayRecover(fmt.Sprintf("native xray XHTTP download inbound=%q session=%q remote=%s", ib.tag, sessionID, r.RemoteAddr))
|
||||||
xrayTracef("native xray: xhttp stream-down inbound=%q session=%q proto=%s remote=%s", ib.tag, sessionID, r.Proto, r.RemoteAddr)
|
xrayTracef("native xray: xhttp stream-down inbound=%q session=%q proto=%s remote=%s", ib.tag, sessionID, r.Proto, r.RemoteAddr)
|
||||||
if !sess.markConnected() {
|
// Do NOT reject a second download GET for the same session. xray-core does not,
|
||||||
// Another download is already streaming this session. Reject the duplicate
|
// and rejecting one permanently breaks reconnects: when the client re-opens the
|
||||||
// GET without touching the live session so the first download keeps flowing.
|
// downlink after a network hiccup (or H2 retries it) while the previous handler
|
||||||
xrayTracef("native xray: xhttp duplicate download rejected inbound=%q session=%q remote=%s", ib.tag, sessionID, r.RemoteAddr)
|
// is still blocked on a now-dead socket, a 400 makes the client tear the whole
|
||||||
xhttpBadRequest(w)
|
// session down and every retry keeps failing.
|
||||||
return
|
sess.markConnected()
|
||||||
}
|
|
||||||
defer ib.deleteXHTTPSession(sessionID, sess)
|
defer ib.deleteXHTTPSession(sessionID, sess)
|
||||||
|
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
@@ -743,21 +742,14 @@ func (s *nativeXHTTPSession) touch() {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// markConnected attaches the single download (stream-down) reader to the
|
// markConnected records that the download (stream-down) GET has attached, which
|
||||||
// session. It returns false if a download is already attached: XHTTP has exactly
|
// stops the unconnected-session reaper from expiring it. It does not reject a
|
||||||
// one downlink per session, and letting a second GET dispatch a second
|
// second attach (xray-core does not either): rejecting breaks client reconnects.
|
||||||
// VLESS/VMess reader over the same upload queue splits the decoded stream across
|
func (s *nativeXHTTPSession) markConnected() {
|
||||||
// two HTTP responses and corrupts the tunnel (seen as the proxy "stopping
|
|
||||||
// passing data" after a mobile-network reconnect).
|
|
||||||
func (s *nativeXHTTPSession) markConnected() bool {
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
|
||||||
if s.connected {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s.connected = true
|
s.connected = true
|
||||||
s.lastSeen = time.Now()
|
s.lastSeen = time.Now()
|
||||||
return true
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *nativeXHTTPSession) close() {
|
func (s *nativeXHTTPSession) close() {
|
||||||
|
|||||||
Reference in New Issue
Block a user