Repository navigation
XHTTP: xmux.maxConnections is ineffective in Go 1.27 builds (v26.9.8+, main) — one TCP connection per session while TLS handshakes hang, leading to OOM #6797
Description
Integrity requirements
- I have read all the comments in the issue template and ensured that this issue meet the requirements.
- I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
- I provided the complete config and logs, rather than just providing the truncated parts based on my own judgment.
- I searched issues and did not find any similar issues.
- The problem can be successfully reproduced in the latest Release
Description
简述:v26.9.8 起使用 Go 1.27 构建,x/net http2.Transport 改为包装 net/http.Transport。当 TLS 握手被阻断而挂起时,每个会话都会新建一条 TCP 连接,XMUX 的 maxConnections 实际失效,连接数和内存随会话数线性增长。使用
-tags http2legacy构建可恢复正常。与 #6644 不同,这里的额外连接与 MaxConcurrentStreams 无关。
Since v26.9.8 Xray is built with Go 1.27 (fd2ca748, "go.mod: Specify Go 1.27.x"); the v26.9.9 release binary reports go1.27.1, and the release workflow builds without extra tags. With Go 1.27, golang.org/x/net (v0.58.0 in v26.9.9, v0.59.0 on main) compiles http2/transport_wrap.go:
//go:build go1.27 && !http2legacy
// Transport wrapping a net/http.Transport.In this mode http2.Transport.RoundTrip is delegated to an internal net/http.Transport instead of x/net's own clientConnPool. The old pool deduplicated in-flight dials per address (p.dialing[addr]), so an XMUX client whose handshake hangs held exactly one socket and all further requests waited on it. The new path does not: while no HTTP/2 connection to the host is established yet, every waiting request dials its own TCP connection.
Normally this is invisible — the first handshake completes in one RTT and everything is multiplexed afterwards. But when handshakes hang (e.g. a middlebox blackholes the flow after the ClientHello — the TSPU scenario from #6376), every proxied session gets its own TCP connection. xmux.maxConnections still limits the number of XMUX clients (the debug log below shows two XMUX: creating xmuxClient lines and no removals), but not the dials underneath them.
Consequences:
maxConnections: 2/3, introduced as the anti-TSPU default (18e28390, XHTTP的默认maxConcurrency字段造成问题 #6376), no longer keeps the client under the ~4-connection limit — it opens hundreds or thousands of connections and keeps re-triggering the ban;- memory grows linearly with hung sessions; in a memory-limited container the process is OOM-killed, taking every other (healthy) outbound down with it.
Affected: v26.9.8, v26.9.9 and main (d562d894) — all Go 1.27 + x/net ≥ v0.58.0. v26.7.28 and earlier are built with Go 1.26 and use the legacy transport. Tested: XHTTP stream-one over TLS (h2). I did not test packet-up/stream-up or h3.
Field observation. Linux client in an LXC container with a memory limit, VLESS + XHTTP stream-one + TLS, maxConnections: 2 (effective config verified with xray run -dump, no extra). During a TSPU episode against one server (pcap on the router WAN):
- up to 2348 simultaneously open client sockets to that server; bursts of 26 SYNs within 90 ms;
- 1258 of 1261 new connections: SYN/SYN-ACK OK, the ClientHello is never ACKed, retransmitted for minutes;
- the process was OOM-killed twice, 193 s apart (
Memory cgroup out of memory: Killed process ... (xray) ... anon-rss:218772kB); the healthy HTTP/2 connections of the other outbounds were reset along with it.
Another outbound (different server) in the same process was unaffected — its handshakes complete, so the first HTTP/2 connection is established and everything multiplexes.
Related
- splithttp: bound physical H2 carriers for maxConnections #6644 (closed, not merged) proposed bounding physical H2 connections for
maxConnections. It was closed after the observation that a transport only opens extra connections when the peer'sMAX_CONCURRENT_STREAMSis exceeded, which a server-side setting fixes. That analysis was done on v26.7.28 (Go 1.26) and is correct for the legacy pool. The connections here are unrelated to stream limits: the peer never completes TLS, so no SETTINGS frame is ever received and no stream limit is known. Raisinghttp2_max_concurrent_streamscannot help; the behavior appears only in Go 1.27 builds and disappears withhttp2legacy. - XHTTP: a stalled dial parks its goroutine with no timeout #6590 (closed, not planned) — a stalled dial parks its goroutine forever; considered a malicious-peer scenario. Here the stall comes from an on-path censor, not the peer (the XHTTP的默认maxConcurrency字段造成问题 #6376 scenario), and the impact is new: with the legacy pool it was one parked dial per XMUX client; with Go 1.27 it is one per session, unbounded, ending in OOM.
- Upstream,
transport_wrap.gohas already changed behavior in other ways, e.g. x/net/http2: ConfigureTransport no longer enables HTTP/2 under go1.27 (transport_wrap.go) golang/go#79778 and x/net/http2: ConfigureServer no longer registers h2 ALPN under go1.27 (server_wrap.go) golang/go#79642.
Workaround: build with -tags http2legacy:
CGO_ENABLED=0 go build -tags http2legacy -trimpath -buildvcs=false -ldflags "-s -w -buildid=" -o xray ./mainPossible fixes
- Build release binaries with
-tags http2legacy(verified below; smallest change). - Or bound concurrent dials per XMUX client in the XHTTP dialer itself, independently of http2 package internals. The XDRIVE transport already uses
net/http.TransportwithMaxConnsPerHost— a similar bound may work here, but I have not tested it for this path.
Reproduction Method
No Xray server is needed. A TCP server that accepts connections, reads, and never answers stands in for a server whose handshake is blackholed after the ClientHello (see Server config).
Load generator load.py — N concurrent SOCKS5 sessions spread over dur seconds:
import socket, struct, threading, time, sys
N, dur = int(sys.argv[1]), float(sys.argv[2])
def one():
try:
s = socket.create_connection(('127.0.0.1', 11080), timeout=dur)
s.sendall(b'\x05\x01\x00'); s.recv(2)
h = b'example.com'
s.sendall(b'\x05\x01\x00\x03' + bytes([len(h)]) + h + struct.pack('>H', 80))
s.recv(10); s.sendall(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); s.recv(100)
except Exception: pass
for _ in range(N):
threading.Thread(target=one, daemon=True).start(); time.sleep(dur / N / 2)
time.sleep(dur)Steps:
python3 blackhole.py &
./xray run -c client.json &
python3 load.py 1500 25 &
sleep 28; ss -tn state established '( dport = :18443 )' | tail -n +2 | wc -lResults, 1500 sessions over 25 s, each run in a clean environment (ports verified free beforehand, the Xray process verified alive at measurement time):
| build | TCP connections | XMUX: creating |
RSS |
|---|---|---|---|
official v26.9.9 release (Xray-linux-64.zip, go1.27.1) |
1500 | 2 | 195 MB |
| v26.9.9 built from the tag, no tags (go1.27.0) | 1500 | 2–3 | 176–196 MB |
main d562d894, no tags |
1500 | 2 | 207 MB |
v26.9.9, -tags http2legacy |
2 | 2 | 84 MB |
v26.9.9, -tags http2legacy, static, on Alpine 3.24 |
3 | 3 | 86 MB |
With 20 sessions (the client log below): 20 connections, 2 XMUX clients.
Expected: with maxConnections: 2 the number of TCP connections stays at about 2 regardless of how many sessions are waiting, as it does with http2legacy and with Go 1.26 builds.
Client config
Details
{
"log": {"loglevel": "debug", "dnsLog": true},
"inbounds": [{"listen": "127.0.0.1", "port": 11080, "protocol": "socks", "settings": {"udp": false}}],
"outbounds": [{
"protocol": "vless",
"settings": {"vnext": [{"address": "127.0.0.1", "port": 18443,
"users": [{"id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"}]}]},
"streamSettings": {
"network": "xhttp", "security": "tls",
"tlsSettings": {"serverName": "example.com", "fingerprint": "chrome", "alpn": ["h2"]},
"xhttpSettings": {"host": "example.com", "path": "/x", "mode": "stream-one",
"xmux": {"maxConnections": 2, "maxConcurrency": 0, "cMaxReuseTimes": 0,
"hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000", "hKeepAlivePeriod": 0}}
}
}]
}
Server config
Details
No Xray server. blackhole.py stands in for a server whose handshake is blackholed after the ClientHello:
import asyncio, sys
n=0
async def h(r,w):
global n; n+=1
try:
while await r.read(65536): pass # читаем и молчим: ClientHello принят, ServerHello не будет
except Exception: pass
async def main():
s=await asyncio.start_server(h,'127.0.0.1',18443,backlog=4096)
async def rep():
while True:
await asyncio.sleep(5); print('accepted total', n, flush=True)
asyncio.create_task(rep())
async with s: await s.serve_forever()
asyncio.run(main())
Client log
Details
Xray 26.9.9 (Xray, Penetrates Everything.) 52a412d (go1.27.1 linux/amd64)
A unified platform for anti-censorship.
2026/09/21 14:41:57.856572 [Info] infra/conf/serial: Reading config: &{Name:client-issue.json Format:json}
2026/09/21 14:41:57.857476 [Debug] app/log: Logger started
2026/09/21 14:41:57.857483 [Debug] app/proxyman/inbound: creating stream worker on 127.0.0.1:11080
2026/09/21 14:41:57.857716 [Info] transport/internet/tcp: listening TCP on 127.0.0.1:11080
2026/09/21 14:41:57.857723 [Warning] core: Xray 26.9.9 started
2026/09/21 14:41:59.860388 [Info] [3075695142] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:41:59.860404 [Info] [3075695142] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:41:59.860409 [Debug] [3075695142] transport/internet/splithttp: XMUX: creating xmuxClient because xmuxClients is empty
2026/09/21 14:41:59.860542 from tcp:127.0.0.1:40744 accepted tcp:example.com:80
2026/09/21 14:41:59.870731 [Info] [3075695142] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:41:59.870891 [Debug] [3075695142] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.008651 [Info] [1825787830] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.008666 [Info] [1825787830] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.008676 [Debug] [1825787830] transport/internet/splithttp: XMUX: creating xmuxClient because maxConnections was not hit, xmuxClients = 1
2026/09/21 14:42:00.008736 from tcp:127.0.0.1:40758 accepted tcp:example.com:80
2026/09/21 14:42:00.008753 [Info] [1825787830] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.008914 [Debug] [1825787830] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.159780 [Info] [3907544594] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.159802 [Info] [3907544594] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.159811 [Info] [3907544594] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.159901 from tcp:127.0.0.1:40768 accepted tcp:example.com:80
2026/09/21 14:42:00.160020 [Debug] [3907544594] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.309895 [Info] [653236920] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.309915 [Info] [653236920] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.309932 from tcp:127.0.0.1:40780 accepted tcp:example.com:80
2026/09/21 14:42:00.309981 [Info] [653236920] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.310165 [Debug] [653236920] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.460614 [Info] [3966413926] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.460629 [Info] [3966413926] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.460634 [Info] [3966413926] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.460712 from tcp:127.0.0.1:40796 accepted tcp:example.com:80
2026/09/21 14:42:00.460835 [Debug] [3966413926] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.610977 [Info] [1636768170] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.610997 [Info] [1636768170] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.611053 from tcp:127.0.0.1:40802 accepted tcp:example.com:80
2026/09/21 14:42:00.611074 [Info] [1636768170] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.611245 [Debug] [1636768170] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.761607 [Info] [2976687724] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.761625 [Info] [2976687724] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.761667 from tcp:127.0.0.1:45336 accepted tcp:example.com:80
2026/09/21 14:42:00.761698 [Info] [2976687724] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.761893 [Debug] [2976687724] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:00.912788 [Info] [2286947645] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:00.912824 [Info] [2286947645] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:00.912837 [Info] [2286947645] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:00.912857 from tcp:127.0.0.1:45342 accepted tcp:example.com:80
2026/09/21 14:42:00.913034 [Debug] [2286947645] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.063377 [Info] [239961799] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.063412 [Info] [239961799] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.063491 from tcp:127.0.0.1:45352 accepted tcp:example.com:80
2026/09/21 14:42:01.063499 [Info] [239961799] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.063645 [Debug] [239961799] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.214120 [Info] [1773947494] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.214151 [Info] [1773947494] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.214159 [Info] [1773947494] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.214170 from tcp:127.0.0.1:45358 accepted tcp:example.com:80
2026/09/21 14:42:01.214349 [Debug] [1773947494] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.364941 [Info] [862654821] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.365001 [Info] [862654821] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.365038 [Info] [862654821] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.365053 from tcp:127.0.0.1:45372 accepted tcp:example.com:80
2026/09/21 14:42:01.365271 [Debug] [862654821] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.516020 [Info] [1731810567] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.516054 [Info] [1731810567] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.516063 [Info] [1731810567] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.516095 from tcp:127.0.0.1:45376 accepted tcp:example.com:80
2026/09/21 14:42:01.516175 [Debug] [1731810567] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.666187 [Info] [3127971750] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.666207 [Info] [3127971750] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.666226 [Info] [3127971750] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.666239 from tcp:127.0.0.1:45384 accepted tcp:example.com:80
2026/09/21 14:42:01.666347 [Debug] [3127971750] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.816883 [Info] [1004484339] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.816899 [Info] [1004484339] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.816910 [Info] [1004484339] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.816969 from tcp:127.0.0.1:45400 accepted tcp:example.com:80
2026/09/21 14:42:01.817226 [Debug] [1004484339] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:01.967189 [Info] [2108210990] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:01.967202 [Info] [2108210990] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:01.967206 [Info] [2108210990] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:01.967274 from tcp:127.0.0.1:45404 accepted tcp:example.com:80
2026/09/21 14:42:01.967541 [Debug] [2108210990] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:02.117916 [Info] [1690246924] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:02.117933 [Info] [1690246924] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:02.117939 [Info] [1690246924] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:02.118066 from tcp:127.0.0.1:45418 accepted tcp:example.com:80
2026/09/21 14:42:02.118234 [Debug] [1690246924] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:02.268300 [Info] [489797624] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:02.268315 [Info] [489797624] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:02.268320 [Info] [489797624] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:02.268328 from tcp:127.0.0.1:45420 accepted tcp:example.com:80
2026/09/21 14:42:02.268507 [Debug] [489797624] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:02.418702 [Info] [3232071992] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:02.418718 [Info] [3232071992] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:02.418789 from tcp:127.0.0.1:45432 accepted tcp:example.com:80
2026/09/21 14:42:02.418801 [Info] [3232071992] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:02.418985 [Debug] [3232071992] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:02.569755 [Info] [2004325015] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:02.569807 [Info] [2004325015] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:02.569841 [Info] [2004325015] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:02.569923 from tcp:127.0.0.1:45442 accepted tcp:example.com:80
2026/09/21 14:42:02.570644 [Debug] [2004325015] transport/internet: dialing to tcp:127.0.0.1:18443
2026/09/21 14:42:02.720934 [Info] [2282207934] proxy/socks: TCP Connect request to tcp:example.com:80
2026/09/21 14:42:02.720950 [Info] [2282207934] app/dispatcher: default route for tcp:example.com:80
2026/09/21 14:42:02.720956 [Info] [2282207934] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:18443, mode stream-one, HTTP version 2, host example.com
2026/09/21 14:42:02.720989 from tcp:127.0.0.1:45444 accepted tcp:example.com:80
2026/09/21 14:42:02.721151 [Debug] [2282207934] transport/internet: dialing to tcp:127.0.0.1:18443
Server log
Details
None: there is no Xray server in this reproduction, and blackhole.py produces no output.
Activity
Fangliding commentedon Sep 21, 2026
这哪怕对于正常的http2都是相当大的问题 多个请求理应被阻塞住等待握手返回 也许应该反馈给 golang/go ?
如果问题存在又release blocker了
ivulit commentedon Sep 21, 2026
Reproduced without Xray: 100 concurrent requests through one Transport to a server whose TLS handshake never completes, counting dials (x/net v0.59.0):
x/net/http2.Transportx/net/http2.Transport-tags http2legacyx/net/http2.Transportnet/http.Transport, HTTP/2 onlyI don't think this is worth reporting upstream — the change is intentional:
x/net/http2.Transportwrapsnet/http.Transport;-tags http2legacyreverts it "for now".RoundTripitself is delegated in golang/net@0a81d5a.net/httphas always dialed per waiting request — by design. Innet/http/transport.go,queueForDialstarts a dial for every request that finds no idle connection unlessMaxConnsPerHostis set, andtryPutIdleConndescribes it as late binding: "They may be actively dialing, but this conn is ready first. Chrome calls this socket late binding." The code is identical in 1.26.8 and 1.27.1. The old per-address dial grouping (clientConnPool, "A dial is already in-flight. Don't start another.") existed only in standalone x/net and is now built only underhttp2legacy.// Deprecated: Use [http.Transport] instead.On the Xray side:
http2.Transportis deprecated.With the workaround from the issue, v26.9.9 built with
-tags http2legacyworks correctly on the affected client.Minimal reproducer
Fangliding commentedon Sep 22, 2026
我向上游反馈了这个问题 golang/go#81646
x-net: always build the legacy HTTP/2 transport under Go 1.27.
Core BUILD 65: the core links x/net's own HTTP/2 transport again.
RPRX commentedon Sep 27, 2026
这下又成 release-blocker 了,不过说不定这两天先再发个 pre-release 测测最近的 commits 有没有 bugFangliding commentedon Sep 27, 2026
这个issu其实夸大其词了 撑死也就是多回退到http.client行为 会多开几条连接多吃点资源但是绝对算不上内存泄露
Tynianoff commentedon Sep 28, 2026
Reproduced on a healthy localhost server, no blackholing involved — the extra dials appear on any cold pool. VLESS + XHTTP
packet-up, TLS/h2, 8 concurrent sessions through the client's socks inbound, counting TCP accepts in front of the server: v26.7.28 → 3 connections (= defaultmaxConnections), v26.9.8 and v26.9.9 → 8 (30 sessions → 30);maxConnections: 1still gives 8. Same v26.9.9 source built with-tags http2legacy→ 3. Second burst on the warm pool: +0 on every build.Why this matters beyond the OOM-in-a-container case: on iOS the tunnel runs inside a Network Extension with a ~50 MB hard memory limit. Every network change (Wi-Fi → cellular) restarts the apps' sessions against a cold pool, so a phone with a few dozen sessions (Instagram etc.) does a few dozen concurrent TLS handshakes at once — the extension exceeds the limit and iOS kills the tunnel, on an otherwise healthy network.
maxConnectionswas exactly the knob that prevented this before 26.9.8.ivulit commentedon Sep 28, 2026
Agreed, "memory leak" isn't the right word for it.
But the main problem isn't resources. A burst of concurrent connections makes censors block the address outright, so every connection to that server dies — and since the client keeps opening more, the block is re-triggered instead of expiring. The connection pattern also gives the proxy away, which is what
maxConnectionswas there to avoid. The OOM came later, as a consequence of piling up attempts against an address that was already blocked.tomon-one commentedon Oct 1, 2026
Heads-up for anyone waiting on the upstream fix: as of patch set 1, CL 840665 does not fix Xray's path. XHTTP uses a standalone
x/net/http2.Transport, whose internalnet/http.Transportstill allows HTTP/1, so the CL never coalesces its dials: 100 requests against a blackholed handshake still make 100 dials (1 withhttp2legacy). It also retries for every waiter after a failed dial, which with Xray'scontext.WithoutCancelrequests turns a SYN-drop block into an unbounded queue (190 queued, 3m20s to drain, vs 0 and 10s for the legacy pool).Details, measurements and proposed changes (x/net 5 lines + net/http on top of the CL), including Xray 26.9.30 built with them (2 TCP connections and the same timings as the
http2legacybuild under a blackholed handshake): golang/go#81646 (comment)Until a fixed Go release is out, builds with
-tags http2legacyremain the safe option.The analysis, patches and tests were done with an AI assistant (Claude) and run on my machine.
RPRX commentedon Oct 5, 2026
@Fangliding 看看上面说的,
XHTTP 代码是不是最好改一下Fangliding commentedon Oct 5, 2026
上游还没弄完呢
RPRX commentedon Oct 5, 2026
@Fangliding 至少 "golang.org/x/net/http2" 得扔掉吧,搜了下 core 里有 16 files
Fangliding commentedon Oct 5, 2026
又不是弃用了 这个bug修的好还是可以继续用的
4 remaining items
KobeArthurScofield commentedon Oct 5, 2026
我感觉最好不要对 Go 1.27.2 抱有太大期望上游要把修复合入主线了才会开对正式版本的 backport,走流程就要不少时间以及我不知道 Go 抽什么风了,Go 1.27.x 的代码在某个提交之后就没法在 GitHub Actions 里面正常完成 bootstrap,下载下来自己 bootstrap 没问题但是不能当 release 来发,但是同时 Go 那边压了一堆 CVE 修复要等他们发版才有可能知道怎么回事,搞不好 win7 版会有一段时间吃不上 1.27.2Raymond520888 commentedon Oct 5, 2026
不用考虑win7了吧,已经是旧时代的载体KobeArthurScofield commentedon Oct 5, 2026
但是现在确实还有人用,也不知道为什么win 8/8.1 和 win server 2012 / r2 现在也要了,从 Go 1.27 开始上游改了 PE 头,低于 win10 直接炸然而 win server 2012 / r2 这个月系统更新出了才算彻底结束官方支持build: use Go 1.27.1 and build with -tags http2legacy
KobeArthurScofield commentedon Oct 9, 2026
1.27.2 发了还没见到相关的 backport
但是 net/http 的 CVE 倒是有一堆Fangliding commentedon Oct 9, 2026
最近net/http好像在乱搞啥更新 之前看commit history一刷一整页 反正就我看到的弃用整个h2包换成http wrapper有点绷
RPRX commentedon Oct 9, 2026
不知道 Go 在搞什么东西Build with http2legacy so that xmux.maxConnections holds again
pixelGQ commentedon Oct 12, 2026
Reproduction for packet-up over TLS/h2 (the mode the opening post lists as untested), plus a field observation of the impact.
Setup. Client: official
Xray-linux-64builds, v26.9.9 (go1.27.1) vs v26.3.27 (go1.26), same config files. Server side: VLESS + XHTTPpacket-upbehind an HTTP reverse-proxy front (CDN-like: terminates TLS, speaks HTTP/1.1 to the origin), clienttlsSettings.alpn: ["h2","http/1.1"], uTLSfingerprint: firefox. Second target: VLESS + XHTTPstream-oneover REALITY (direct, no proxy in between). Load: 30 parallel SOCKS sessions (curl … generate_204through the client's socks inbound), and I sampledss -tnp state established '( dst <entry> )'every 250 ms filtered by the client's PID. Numbers below are the peak count of concurrent TCP connections to the entry during the burst.xmux(core default)xmux.maxConnections: 3xmux.maxConnections: 1xmux.maxConcurrency: 4xmuxSo with the go1.27 build
maxConnectionshas no effect on packet-up either: 30 waiting requests → ~30 dials, exactly as described above. Throughput on a wired link is unaffected (85–115 Mbit/s in every row), the problem is only the number of simultaneous TLS connections.Why it matters in the field. The two mainstream mobile clients in our user base (Happ 6.0.0 iOS/Android and INCY) currently ship core 26.9.9. On Russian cellular networks (tested on T2/Tele2 with the same phone) the packet-up XHTTP entry now behaves like "loads for a moment, then the connection stalls for several seconds, then recovers", while VLESS+Vision entries on the same phone and network work normally; on home Wi-Fi the same XHTTP entry is fine. The same client configs with
maxConnections: 3/1change nothing on 26.9.9, which matches the table.go.modof v26.9.30 and v26.10.10 still saysgo 1.27, so every current release is affected.Until golang/go#81646 lands, could the release workflow build with
-tags http2legacy(or could the splithttp dialer de-duplicate in-flight dials itself)? Client app vendors take the official releases as they are, and most users cannot rebuild the core.Инструкции: панель проверена на VPS, шторм своего клиента подтверждён
ivulit commentedon Oct 18, 2026
Fixed in v26.10.17.
The fix went into x/net rather than Go: golang/net@01e3d033 — "The wrapping implementation inadvertently removed dial coalescing. Add it back." — released in x/net v0.60.0. #7133 and #7147 moved the dependency to v0.61.0, so no build tag is needed. The two net/http CLs were abandoned, so nothing has to change in a Go release. golang/go#81646 is closed as completed.
Official
Xray-linux-64builds, 500 concurrent sessions against a server that completes the TCP handshake and never answers the ClientHello, peak TCP connections to it:maxConnections: 123packet-upon v26.10.17 behaves the same (maxConnections: 3→ 3 connections), and RSS drops from ~93 MB to ~55 MB at that load.RPRX commentedon Oct 18, 2026
如果没通过这个外部库、直接 net/http 呢KobeArthurScofield commentedon Oct 20, 2026
暂时还没见到有相关的 1.27 backport,x/net 确定是上了修复,1.27.4 会不会上不知道
1.27.3 被紧急更新吃了
XHTTP client: Pin single-connection dialing for concurrent streams
Ядро диагностики xdiag: улики, правила, эталонная проба Xray-core