Repository navigation
XHTTP: a stalled dial parks its goroutine with no timeout #6590
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
An XHTTP peer that completes the TCP handshake and then stops sending parks the dialing goroutine for the rest of the process lifetime.
net.Dialer.Timeout bounds the TCP phase at 16s:
Xray-core/transport/internet/system_dialer.go
Line 112 in 5ca6f4b
| Timeout: time.Second * 16, |
Nothing bounds what comes after it. OpenStream and PostPacket build their requests with context.WithoutCancel, so that an established stream survives cancellation of the caller's context:
| req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body) |
The same uncancellable context reaches the dial. reality.UClient and tls.UConn.HandshakeContext both take ctxInner:
Xray-core/transport/internet/splithttp/dialer.go
Lines 136 to 142 in 5ca6f4b
| return reality.UClient(conn, realityConfig, ctxInner, dest) | |
| } | |
| if gotlsConfig != nil { | |
| if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil { | |
| conn = tls.UClient(conn, gotlsConfig, fingerprint) | |
| if err := conn.(*tls.UConn).HandshakeContext(ctxInner); err != nil { |
http2.Transport has no dial timeout of its own, and in x/net v0.57.0 clientConnPool.getClientConn waits on <-call.done with no select on the context, so once the handshake stalls there is nothing left to wake that goroutine:
goroutine 97 [chan receive, 2 minutes]:
splithttp.(*DefaultDialerClient).OpenStream
transport/internet/splithttp/client.go:96
splithttp.Dial
transport/internet/splithttp/dialer.go:467
internet.Dial
transport/internet/dialer.go:63
proxyman/outbound.(*Handler).Dial
app/proxyman/outbound/handler.go:312
vless/outbound.(*Handler).Process.func2
proxy/vless/outbound/outbound.go:194
retry.(*retryer).On
common/retry/retry.go:27
vless/outbound.(*Handler).Process
proxy/vless/outbound/outbound.go:192
proxyman/outbound.(*Handler).Dispatch
app/proxyman/outbound/handler.go:243
dispatcher.(*DefaultDispatcher).routedDispatch
app/dispatcher/default.go:504
created by dispatcher.(*DefaultDispatcher).Dispatch
app/dispatcher/default.go:288
Anything that dials on a schedule then accumulates one goroutine per attempt. burstObservatory does it with no client traffic at all.
I measured it against a listener that accepts TCP and never writes, on the release binary for v26.7.28 and on the same tag with #6584 applied and nothing else. Each instance had its own listener, same config apart from the ports, probe interval 15s, no client traffic. I read the total goroutine count and the number parked in getClientConn from the metrics endpoint. This is the run whose logs are attached:
t, s v26.7.28 parked with #6584 parked
0 22 2 30 3
30 31 4 34 4
60 42 7 27 3
90 51 9 42 6
120 59 11 31 4
150 65 13 22 2
The release count only goes up. With the deadline in place the count moves in both directions and the parked dials come back down, because the timeout releases the ones that stalled and the client dials again. Their listeners show it from the other side: the release peer accepted 3 connections in those 150s, the patched peer accepted 10.
A longer run of the release binary, 9 minutes: it went from 22 goroutines to 161, and its parked count went 2, 5, 6, 7, 10, 12, 14, 17, 19, 21, 22, 27, 28, 31, 33, 34, 37, never once dropping. Its goroutine ages at the end sat at 2, 4, 6, 7, 8 and 9 minutes, so they had been laid down steadily over the whole lifetime.
Client traffic behaves the same way. In the attached run, three requests that the client abandons after 5s leave three parked dials, and the count is still exactly three at the end. In the 9 minute run those three were still parked 5 minutes later, then twenty more abandoned requests took the count from 3 to 23, one per request, while the listener had accepted 3 TCP connections in total, because the later requests queue on the dials that are already pending.
I opened #6584 for this before filing.
Is WithoutCancel meant to cover the establishment phase as well, or only an established stream?
Reproduction Method
- Save the listener from the server config section as
silent.goand rungo run silent.go. It accepts connections and never writes, which is what a peer looks like when it stalls inside TLS. - Start the release binary with the client config below.
- Poll the metrics endpoint:
while true; do
curl -s "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1" | head -1
curl -s "http://127.0.0.1:6060/debug/pprof/goroutine?debug=2" | grep -c getClientConn
sleep 30
done
At a 15s probe interval the total climbed by about 17 per minute and the parked count by about 4.
For the client traffic variant, drop burstObservatory from the config and send a few requests that give up early:
curl -s -o /dev/null -m 5 -x socks5h://127.0.0.1:10808 https://stall.example/
The parked count goes up by one per request and stays there after curl exits.
Client config
Details
{
"log": {
"loglevel": "debug",
"dnsLog": true
},
"metrics": {
"listen": "127.0.0.1:6060"
},
"inbounds": [
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": false
}
}
],
"outbounds": [
{
"tag": "xhttp-stall",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "127.0.0.1",
"port": 9443,
"users": [
{
"id": "cbe4e52b-2fcb-450e-9aea-aae4c4587a83",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "tls",
"tlsSettings": {
"serverName": "stall.example"
},
"xhttpSettings": {
"host": "stall.example",
"path": "/x",
"mode": "packet-up"
}
}
}
],
"burstObservatory": {
"subjectSelector": [
"xhttp-stall"
],
"pingConfig": {
"destination": "https://www.google.com/generate_204",
"interval": "15s",
"timeout": "2s",
"sampling": 3
}
}
}
Server config
There is no Xray server here. The peer is a listener that accepts TCP and never writes, which is the condition I am reporting.
Details
// Accepts TCP connections and never sends anything back. // Simulates a peer that completes the TCP handshake and then stalls during TLS. package mainimport (
"log"
"net"
)
func main() {
l, err := net.Listen("tcp", "127.0.0.1:9443")
if err != nil {
log.Fatal(err)
}
log.Println("silent listener on 127.0.0.1:9443")
var held []net.Conn
for {
c, err := l.Accept()
if err != nil {
log.Fatal(err)
}
log.Println("accepted", c.RemoteAddr())
held = append(held, c)
}
}
Client log
This is the complete log of the release instance from the run tabulated above, at loglevel: debug with dnsLog: true.
Details
Xray 26.7.28 (Xray, Penetrates Everything.) 5ca6f4b (go1.26.5 darwin/arm64)
A unified platform for anti-censorship.
2026/08/04 13:43:16.646527 [Info] infra/conf/serial: Reading config: &{Name:config.json Format:json}
2026/08/04 13:43:16.648336 [Debug] app/log: Logger started
2026/08/04 13:43:16.649314 [Debug] app/proxyman/inbound: creating stream worker on 127.0.0.1:10808
2026/08/04 13:43:16.649852 [Info] transport/internet/tcp: listening TCP on 127.0.0.1:10808
2026/08/04 13:43:16.649897 [Info] app/metrics: Metrics server listening on 127.0.0.1:6060
2026/08/04 13:43:16.650490 [Warning] core: Xray 26.7.28 started
2026/08/04 13:43:16.650846 [Info] app/observatory/burst: perform one-time health check for tags [xhttp-stall]
2026/08/04 13:43:16.651184 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:43:16.653048 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:43:16.653444 [Debug] transport/internet/splithttp: XMUX: creating xmuxClient because xmuxClients is empty
2026/08/04 13:43:16.653572 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:43:16.655415 [Debug] transport/internet: dialing to tcp:127.0.0.1:9443
2026/08/04 13:43:18.652855 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:43:27.663322 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:43:27.664144 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:43:27.664240 [Debug] transport/internet/splithttp: XMUX: creating xmuxClient because maxConnections was not hit, xmuxClients = 1
2026/08/04 13:43:27.664277 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:43:27.664299 [Debug] transport/internet: dialing to tcp:127.0.0.1:9443
2026/08/04 13:43:29.664415 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:43:53.925318 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:43:53.925619 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:43:53.925706 [Debug] transport/internet/splithttp: XMUX: creating xmuxClient because maxConnections was not hit, xmuxClients = 2
2026/08/04 13:43:53.925748 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:43:53.925903 [Debug] transport/internet: dialing to tcp:127.0.0.1:9443
2026/08/04 13:43:55.926050 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:43:57.974606 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:43:57.974667 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:43:57.974687 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:43:59.975798 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:44:06.465827 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:44:06.466052 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:44:06.466105 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:44:08.466894 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:44:09.678649 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:44:09.678878 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:44:09.678935 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:44:11.679786 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:44:28.010590 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:44:28.011827 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:44:28.011884 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:44:30.011296 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:44:59.784338 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:44:59.785592 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:44:59.785645 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:00.415032 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:45:00.415123 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:45:00.415142 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:01.785430 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:45:02.416229 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:45:11.742745 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:45:11.742814 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:45:11.742836 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:13.743855 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:45:34.498604 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:45:34.499442 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:45:34.499450 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:36.499736 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:45:53.167058 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:45:53.167888 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:45:53.167937 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:55.168497 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2026/08/04 13:45:55.752147 [Debug] app/observatory/burst: checking xhttp-stall
2026/08/04 13:45:55.752346 [Info] app/dispatcher: taking platform initialized detour [xhttp-stall] for [tcp:www.google.com:443]
2026/08/04 13:45:55.752461 [Info] transport/internet/splithttp: XHTTP is dialing to tcp:127.0.0.1:9443, mode packet-up, HTTP version 2, host stall.example
2026/08/04 13:45:57.753514 [Warning] app/observatory/burst: error ping https://www.google.com/generate_204 with xhttp-stall: Head "https://www.google.com/generate_204": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Server log
This is the complete log of that instance's listener.
Details
2026/08/04 13:43:12 silent listener on 127.0.0.1:9443
2026/08/04 13:43:16 accepted 127.0.0.1:53490
2026/08/04 13:43:27 accepted 127.0.0.1:53500
2026/08/04 13:43:53 accepted 127.0.0.1:53515
Activity
Fangliding commentedon Aug 4, 2026
触发这个问题要求对端在tcp层保持连接 且响应keepalive 那是对端有意这么做 而且同样消耗对端资源 这已经是属于故意的恶意行为 恶意的对端总是有大量方法卡住你的连接 所以我才认为不需要处理 否则让AI审几道这种小问题修不完
count -1 的作用是保持套接字默认 不是 “With Count: -1 the probes never give up”