Skip to content

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

Open

Description

@ivulit

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

Workaround: build with -tags http2legacy:

CGO_ENABLED=0 go build -tags http2legacy -trimpath -buildvcs=false -ldflags "-s -w -buildid=" -o xray ./main

Possible 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.Transport with MaxConnsPerHost — 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 -l

Results, 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

  1. Fangliding commented on Sep 21, 2026

    @Fangliding
    Member

    这哪怕对于正常的http2都是相当大的问题 多个请求理应被阻塞住等待握手返回 也许应该反馈给 golang/go ?
    如果问题存在又release blocker了

  2. ivulit commented on Sep 21, 2026

    @ivulit
    Author

    已在不含 Xray 的最小程序中复现。这不是 Go 的意外 bug,而是有意的改动:x/net 在 Go 1.27 下改为包装 net/http(49810da),net/http 本来就为每个等待中的请求各自拨号(late binding),而且 x/net/http2.Transport 已在 v0.59.0 被弃用(f3ca034)。相关提交见下。使用 -tags http2legacy 构建的 v26.9.9 在受影响的客户端上运行正常。

    Reproduced without Xray: 100 concurrent requests through one Transport to a server whose TLS handshake never completes, counting dials (x/net v0.59.0):

    Go Transport dials
    go1.26.8 x/net/http2.Transport 1
    go1.27.1 x/net/http2.Transport 100
    go1.27.1, -tags http2legacy x/net/http2.Transport 1
    go1.26.8 / go1.27.1 net/http.Transport, HTTP/2 only 100 / 100

    I don't think this is worth reporting upstream — the change is intentional:

    • golang/net@49810da (2026-04-28, first in x/net v0.54.0) "http2: enable net/http wrapping when go >= 1.27" (x/net/http2: wrap net/http golang/go#78508): under Go 1.27 x/net/http2.Transport wraps net/http.Transport; -tags http2legacy reverts it "for now". RoundTrip itself is delegated in golang/net@0a81d5a.
    • net/http has always dialed per waiting request — by design. In net/http/transport.go, queueForDial starts a dial for every request that finds no idle connection unless MaxConnsPerHost is set, and tryPutIdleConn describes 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 under http2legacy.
    • golang/net@f3ca034 (2026-08-22, first in x/net v0.59.0) "http2: deprecate Transport and Server" (x/net/http2: deprecate Transport and Server golang/go#78064): // Deprecated: Use [http.Transport] instead.

    On the Xray side:

    • a3c054a (2026-05-22) brought in x/net v0.54.0, but v26.6.27 and v26.7.28 were built with Go 1.26, so the legacy path was still used;
    • fd2ca74 (2026-09-08, "go.mod: Specify Go 1.27.x") activated the wrapper — v26.9.8 is the first affected release;
    • 24e6f6d (2026-09-20) moved main to x/net v0.59.0, where http2.Transport is deprecated.

    With the workaround from the issue, v26.9.9 built with -tags http2legacy works correctly on the affected client.

    Minimal reproducer
    // Sends 100 concurrent requests through one Transport to a server
    // whose TLS handshake never completes, and counts dials.
    package main
    
    import (
    	"context"
    	"crypto/tls"
    	"fmt"
    	"net"
    	"net/http"
    	"runtime"
    	"sync/atomic"
    	"time"
    
    	"golang.org/x/net/http2"
    )
    
    const requests = 100
    
    // stallingServer accepts TCP connections, reads the ClientHello and never answers.
    func stallingServer() string {
    	ln, err := net.Listen("tcp", "127.0.0.1:0")
    	if err != nil {
    		panic(err)
    	}
    	go func() {
    		for {
    			c, err := ln.Accept()
    			if err != nil {
    				return
    			}
    			go func() {
    				b := make([]byte, 4096)
    				for {
    					if _, err := c.Read(b); err != nil {
    						return
    					}
    				}
    			}()
    		}
    	}()
    	return ln.Addr().String()
    }
    
    func run(name string, newTransport func(dialTLS func(context.Context) (net.Conn, error)) http.RoundTripper) {
    	addr := stallingServer()
    	var dials atomic.Int64
    	dialTLS := func(ctx context.Context) (net.Conn, error) {
    		dials.Add(1)
    		c, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
    		if err != nil {
    			return nil, err
    		}
    		tc := tls.Client(c, &tls.Config{ServerName: "example.com", NextProtos: []string{"h2"}})
    		if err := tc.HandshakeContext(ctx); err != nil {
    			c.Close()
    			return nil, err
    		}
    		return tc, nil
    	}
    	client := &http.Client{Transport: newTransport(dialTLS)}
    	for range requests {
    		go func() {
    			req, _ := http.NewRequest("GET", "https://example.com/", nil)
    			client.Do(req)
    		}()
    	}
    	time.Sleep(2 * time.Second)
    	fmt.Printf("%s  %-36s requests=%d dials=%d\n", runtime.Version(), name, requests, dials.Load())
    }
    
    func main() {
    	run("x/net/http2.Transport", func(dialTLS func(context.Context) (net.Conn, error)) http.RoundTripper {
    		return &http2.Transport{
    			DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.Config) (net.Conn, error) {
    				return dialTLS(ctx)
    			},
    		}
    	})
    	run("net/http.Transport (HTTP/2 only)", func(dialTLS func(context.Context) (net.Conn, error)) http.RoundTripper {
    		t := &http.Transport{
    			DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
    				return dialTLS(ctx)
    			},
    		}
    		t.Protocols = new(http.Protocols)
    		t.Protocols.SetHTTP2(true)
    		return t
    	})
    }
    go mod init repro && go get golang.org/x/net@v0.59.0
    GOTOOLCHAIN=go1.26.8 go run .
    GOTOOLCHAIN=go1.27.1 go run .
    GOTOOLCHAIN=go1.27.1 go run -tags http2legacy .
  3. deleted a comment from therealtunge on Sep 22, 2026
  4. Fangliding commented on Sep 22, 2026

    @Fangliding
    Member

    我向上游反馈了这个问题 golang/go#81646

  5. RPRX commented on Sep 27, 2026

    @RPRX
    Member

    这下又成 release-blocker 了,不过说不定这两天先再发个 pre-release 测测最近的 commits 有没有 bug

  6. Fangliding commented on Sep 27, 2026

    @Fangliding
    Member

    这个issu其实夸大其词了 撑死也就是多回退到http.client行为 会多开几条连接多吃点资源但是绝对算不上内存泄露

  7. Tynianoff commented on Sep 28, 2026

    @Tynianoff

    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 (= default maxConnections), v26.9.8 and v26.9.9 → 8 (30 sessions → 30); maxConnections: 1 still 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. maxConnections was exactly the knob that prevented this before 26.9.8.

  8. ivulit commented on Sep 28, 2026

    @ivulit
    Author

    同意,说内存泄露并不准确。但主要问题不在资源:并发连接一多,审查设备就会把这个地址整个封掉,而客户端不停新开连接会让封禁一直续下去;同时这种连接形态也让代理暴露。OOM 只是之后不断向已被封禁的地址重连的结果。

    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 maxConnections was there to avoid. The OOM came later, as a consequence of piling up attempts against an address that was already blocked.

  9. tomon-one commented on Oct 1, 2026

    @tomon-one

    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 internal net/http.Transport still allows HTTP/1, so the CL never coalesces its dials: 100 requests against a blackholed handshake still make 100 dials (1 with http2legacy). It also retries for every waiter after a failed dial, which with Xray's context.WithoutCancel requests 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 http2legacy build under a blackholed handshake): golang/go#81646 (comment)

    Until a fixed Go release is out, builds with -tags http2legacy remain the safe option.

    The analysis, patches and tests were done with an AI assistant (Claude) and run on my machine.

  10. RPRX commented on Oct 5, 2026

    @RPRX
    Member

    @Fangliding 看看上面说的,XHTTP 代码是不是最好改一下

  11. Fangliding commented on Oct 5, 2026

    @Fangliding
    Member

    上游还没弄完呢

  12. RPRX commented on Oct 5, 2026

    @RPRX
    Member

    @Fangliding 至少 "golang.org/x/net/http2" 得扔掉吧,搜了下 core 里有 16 files

  13. Fangliding commented on Oct 5, 2026

    @Fangliding
    Member

    又不是弃用了 这个bug修的好还是可以继续用的

  14. 4 remaining items

  15. KobeArthurScofield commented on Oct 5, 2026

    @KobeArthurScofield
    Collaborator

    我感觉最好不要对 Go 1.27.2 抱有太大期望

    上游要把修复合入主线了才会开对正式版本的 backport,走流程就要不少时间

    以及我不知道 Go 抽什么风了,Go 1.27.x 的代码在某个提交之后就没法在 GitHub Actions 里面正常完成 bootstrap,下载下来自己 bootstrap 没问题但是不能当 release 来发,但是同时 Go 那边压了一堆 CVE 修复要等他们发版才有可能知道怎么回事,搞不好 win7 版会有一段时间吃不上 1.27.2

  16. Raymond520888 commented on Oct 5, 2026

    @Raymond520888

    我感觉最好不要对 Go 1.27.2 抱有太大期望

    上游要把修复合入主线了才会开对正式版本的 backport,走流程就要不少时间

    以及我不知道 Go 抽什么风了,Go 1.27.x 的代码在某个提交之后就没法在 GitHub Actions 里面正常完成 bootstrap,下载下来自己 bootstrap 没问题但是不能当 release 来发,但是同时 Go 那边压了一堆 CVE 修复要等他们发版才有可能知道怎么回事,搞不好 win7 版会有一段时间吃不上 1.27.2

    不用考虑win7了吧,已经是旧时代的载体

  17. KobeArthurScofield commented on Oct 5, 2026

    @KobeArthurScofield
    Collaborator

    不用考虑win7了吧,已经是旧时代的载体

    但是现在确实还有人用,也不知道为什么
    win 8/8.1 和 win server 2012 / r2 现在也要了,从 Go 1.27 开始上游改了 PE 头,低于 win10 直接炸
    然而 win server 2012 / r2 这个月系统更新出了才算彻底结束官方支持

  18. KobeArthurScofield commented on Oct 9, 2026

    @KobeArthurScofield
    Collaborator

    1.27.2 发了还没见到相关的 backport

    但是 net/http 的 CVE 倒是有一堆

  19. Fangliding commented on Oct 9, 2026

    @Fangliding
    Member

    最近net/http好像在乱搞啥更新 之前看commit history一刷一整页 反正就我看到的弃用整个h2包换成http wrapper有点绷

  20. RPRX commented on Oct 9, 2026

    @RPRX
    Member

    不知道 Go 在搞什么东西

  21. pixelGQ commented on Oct 12, 2026

    @pixelGQ

    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-64 builds, v26.9.9 (go1.27.1) vs v26.3.27 (go1.26), same config files. Server side: VLESS + XHTTP packet-up behind an HTTP reverse-proxy front (CDN-like: terminates TLS, speaks HTTP/1.1 to the origin), client tlsSettings.alpn: ["h2","http/1.1"], uTLS fingerprint: firefox. Second target: VLESS + XHTTP stream-one over REALITY (direct, no proxy in between). Load: 30 parallel SOCKS sessions (curl … generate_204 through the client's socks inbound), and I sampled ss -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.

    client config (xhttpSettings.extra) v26.3.27 (go1.26) v26.9.9 (go1.27.1)
    packet-up, no xmux (core default) 30 27
    packet-up, xmux.maxConnections: 3 8 29
    packet-up, xmux.maxConnections: 1 4 28
    packet-up, xmux.maxConcurrency: 4 14 17
    stream-one over REALITY, no xmux 29 7

    So with the go1.27 build maxConnections has 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 / 1 change nothing on 26.9.9, which matches the table. go.mod of v26.9.30 and v26.10.10 still says go 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.

  22. ivulit commented on Oct 18, 2026

    @ivulit
    Author

    v26.10.17 已修复,不需要 -tags http2legacy。修复在 x/net 而不是 Go:golang/net@01e3d033 恢复了拨号合并,随 v0.60.0 发布;#7133 和 #7147 把依赖升到了 v0.61.0。

    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-64 builds, 500 concurrent sessions against a server that completes the TCP handshake and never answers the ClientHello, peak TCP connections to it:

    build maxConnections: 1 2 3
    v26.10.10 (x/net v0.59.0) 500 500 500
    v26.10.17 (x/net v0.61.0) 1 2 3

    packet-up on v26.10.17 behaves the same (maxConnections: 3 → 3 connections), and RSS drops from ~93 MB to ~55 MB at that load.

  23. RPRX commented on Oct 18, 2026

    @RPRX
    Member

    The fix went into x/net

    如果没通过这个外部库、直接 net/http 呢

  24. KobeArthurScofield commented on Oct 20, 2026

    @KobeArthurScofield
    Collaborator

    暂时还没见到有相关的 1.27 backport,x/net 确定是上了修复,1.27.4 会不会上不知道

    1.27.3 被紧急更新吃了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions