Skip to content

XHTTP: a stalled dial parks its goroutine with no timeout #6590

Closed as not planned

Description

@embogomolov

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:

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:

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

  1. Save the listener from the server config section as silent.go and run go run silent.go. It accepts connections and never writes, which is what a peer looks like when it stalls inside TLS.
  2. Start the release binary with the client config below.
  3. 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 main

import (
"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

  1. Fangliding commented on Aug 4, 2026

    @Fangliding
    Member

    触发这个问题要求对端在tcp层保持连接 且响应keepalive 那是对端有意这么做 而且同样消耗对端资源 这已经是属于故意的恶意行为 恶意的对端总是有大量方法卡住你的连接 所以我才认为不需要处理 否则让AI审几道这种小问题修不完
    count -1 的作用是保持套接字默认 不是 “With Count: -1 the probes never give up”

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