From 666ff070a7ea77b0b18b684cfb89dca4e052746f Mon Sep 17 00:00:00 2001 From: Ying Huang Date: Tue, 11 Aug 2026 23:24:26 -0700 Subject: [PATCH] reality: propagate the dest's close to the client in fallback When REALITY falls back to relaying an unauthenticated connection to the dest, it forwards the dest's bytes faithfully but never forwards the close. Teardown sits behind waitGroup.Wait(), which waits on the upload io.Copy, and that copy only returns once the client closes. A client that never closes therefore keeps the connection open indefinitely, long after the real dest has dropped it. That is remotely observable and distinguishes a REALITY server from the site it imitates. Measured against sing-box 1.13.14 (metacubex/utls v1.8.4) with dest www.intel.com: send a 5-byte record header plus 32 random bytes; both the REALITY server and the real dest return the same alert 1503030002020a, but the real dest closes immediately while the REALITY server is still open after 12 seconds. One connection, 37 bytes. Propagate the close with a half-close once the download copy returns, so the client observes the same shutdown the dest performed. Guarded by a type assertion, so it is a no-op where the underlying conn cannot half-close. XTLS/REALITY already does this; see the underlying.CloseWrite() call after the download io.Copy in its tls.go, where underlying is asserted to CloseWriteConn. --- reality.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/reality.go b/reality.go index a684b86985..068c90d65e 100644 --- a/reality.go +++ b/reality.go @@ -570,6 +570,17 @@ func RealityServer(ctx context.Context, conn net.Conn, config *RealityConfig) (* } conn.Write(s2cSaved) io.Copy(underlying, newRateLimitedConn(target, &config.LimitFallbackDownload)) + // client ---underlying--- server ---target--- dest + // The dest has closed its side. Propagate that to the client with a + // half-close instead of leaving the connection open: the teardown + // below waits on the upload io.Copy, which only returns once the + // client closes, so a client that simply never closes keeps the + // connection alive long after the real dest would have dropped it. + // That difference is remotely observable and distinguishes this + // server from the dest it is imitating. + if cw, ok := underlying.(interface{ CloseWrite() error }); ok { + cw.CloseWrite() + } } waitGroup.Done() }()