h2mux + TCP Brutal can retain an unusable session and repeatedly return closed-pipe errors
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.2k
- Forks
- 4.6k
- Avg merge
- 19d 15h
- Merged PRs (30d)
- 1
Description
Operating system
Linux
System version
- Client/router: Debian GNU/Linux 13.6 (trixie), Linux 6.12.90+deb13-amd64
- Server: Ubuntu 22.04.5 LTS, Linux 5.15.0-152-generic, official Docker image
- TCP Brutal DKMS source package on client/server: v1.0.3. The source still contains
MODULE_VERSION("1.0.2"), somodinforeports v1.0.2. Client, affected server, and healthy control server have the samebrutal.cSHA-256 (171d775cc494f9378c89772ef68ad8001e093f701a35d1566ec9c7e459cb81c0) and modulesrcversion(73251705C4C828ACBF07FDD).
Installation type
Original sing-box Command Line on the client; official sing-box Docker image on the server.
Version
Reproduced with the following controlled matrix:
| Client | Server | Result |
|---|---|---|
| 1.13.15 | 1.13.15 | reproduces |
| 1.13.15 | 1.13.14 | reproduces |
| 1.13.14 | 1.13.14 | reproduces |
For 1.13.14 on both sides:
sing-box version 1.13.14
Environment: go1.25.11 linux/amd64
Revision: 25a600db24f7680ad9806ce5427bd0ab8afe1114
CGO: enabled
Both 1.13.14 and 1.13.15 use github.com/sagernet/sing-mux v0.3.5.
Description
A long-lived VLESS + Reality outbound using h2mux and TCP Brutal eventually stops accepting usable streams. New HTTPS connections fail while writing their first payload:
connection: connection upload handshake: io: read/write on closed pipe
At that point:
- the underlying client-to-server TCP connection is still
ESTABLISHED; - the server does not receive the failed multiplex streams;
- restarting the client clears the condition temporarily;
- disabling TCP Brutal only on the client, while leaving h2mux enabled, prevents the failure in repeated A/B testing;
- other independent outbounds remain healthy.
Expected behavior: when an h2mux session can no longer open streams, it should be marked dead and evicted, or another session should be selected.
Actual behavior: new streams can repeatedly receive an already closed pipe while the underlying TCP connection remains established.
Relevant configuration
Sensitive endpoint and Reality credentials are replaced with placeholders. The failing outbound options are:
{
"type": "vless",
"tag": "proxy",
"server": "<SERVER_IP>",
"server_port": 443,
"uuid": "<UUID>",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "addons.mozilla.org",
"reality": {
"enabled": true,
"public_key": "<PUBLIC_KEY>",
"short_id": "<SHORT_ID>"
},
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
},
"multiplex": {
"enabled": true,
"protocol": "h2mux",
"padding": false,
"max_connections": 4,
"min_streams": 4,
"brutal": {
"enabled": true,
"up_mbps": 1000,
"down_mbps": 1000
}
}
}
The server VLESS inbound has multiplex and Brutal enabled:
{
"type": "vless",
"tag": "vless-in",
"listen": "::",
"listen_port": 443,
"users": [
{
"uuid": "<UUID>",
"flow": "xtls-rprx-vision"
}
],
"tls": {
"enabled": true,
"server_name": "addons.mozilla.org",
"reality": {
"enabled": true,
"handshake": {
"server": "addons.mozilla.org",
"server_port": 443
},
"private_key": "<PRIVATE_KEY>",
"short_id": ["<SHORT_ID>"]
}
},
"multiplex": {
"enabled": true,
"padding": false,
"brutal": {
"enabled": true,
"up_mbps": 1000,
"down_mbps": 1000
}
}
}
The production client is a Linux TUN router carrying mixed TCP and UDP/XUDP traffic. DNS uses FakeIP, but the failure occurs after routing: the correct destination is logged on the client and is visible on the server for successful streams.
Reproduction
- Run the VLESS/Reality server with h2mux/Brutal enabled.
- Run a Linux TUN client/router with the outbound above.
- Send mixed LAN TCP and UDP/XUDP traffic through the outbound. Repeated HTTPS probes were made to three unrelated destinations.
- After approximately 1–3 minutes, the HTTPS probes begin failing together with curl error 35. The client logs
connection upload handshake: io: read/write on closed pipe. - Observe that the underlying TCP connection to the proxy server remains
ESTABLISHED. - Restarting the client temporarily restores all probes.
- Set only
multiplex.brutal.enabledtofalseon the client, keep h2mux enabled, and restart. The failure no longer occurs.
A smaller mixed inbound -> same outbound test carrying HTTP-only traffic did not reproduce in about three minutes, including 1,000 requests at concurrency 20. This suggests that mixed TCP and packet streams and/or the real LAN workload is part of the trigger. I am including this negative result to avoid claiming an incomplete minimal reproducer.
Controlled observations
With Brutal enabled:
- one TCP connection is used;
- TCP remains
ESTABLISHEDduring failures; - new streams repeatedly fail with closed-pipe;
- server logs stop showing the affected destinations during the failure window.
With client-side Brutal disabled:
- four BBR TCP connections are created, matching
max_connections: 4; - h2mux remains enabled;
- more than 230 HTTPS requests across two observation windows complete without a single curl error;
- client closed-pipe count is zero.
A representative failure window on 1.13.14/1.13.14:
# Client
INFO outbound/vless[proxy]: outbound multiplex connection to x.com:443
ERROR connection: connection upload handshake: io: read/write on closed pipe
INFO outbound/vless[proxy]: outbound multiplex connection to www.google.com:443
ERROR connection: connection upload handshake: io: read/write on closed pipe
INFO outbound/vless[proxy]: outbound multiplex connection to github.com:443
ERROR connection: connection upload handshake: io: read/write on closed pipe
The server had received the same destinations normally seconds earlier, but had no corresponding inbound multiplex stream records for these failed attempts. The base VLESS/mux connection also had no close record in that window.
TCP state during the failure:
ESTAB ... client:port -> server:443
brutal ... pacing_rate 1000000000bps ... delivery_rate ~43Mbps ... reord_seen 369
After disabling client-side Brutal:
established=4 brutal=0 bbr=4
closed_pipe=0
Possible source-level cause
In sing-mux v0.3.5, Client.offer() returns the first existing session whenever Brutal is enabled, without filtering by CanTakeNewRequest() and without applying the normal max_connections / min_streams selection:
https://github.com/SagerNet/sing-mux/blob/v0.3.5/client.go#L139-L178
For h2mux, a RoundTrip() error calls conn.setup(nil, err), but does not explicitly close/evict the parent session. IsClosed() only sees the local flag and HTTP/2 Closed / Closing state:
https://github.com/SagerNet/sing-mux/blob/v0.3.5/h2mux.go#L200-L258
A possible failure loop is therefore:
- an h2mux session can no longer open a request stream;
- the stream pipe is closed, but the underlying TCP/HTTP2 session is not considered closed;
- the Brutal branch continues returning
sessions[0]; - all new streams repeatedly fail with
io: read/write on closed pipe.
The single-session behavior under Brutal may be intentional, but an unusable session should still be detected and replaced.
Logs
Full debug logs contain private LAN destinations and are not posted verbatim. The relevant client/server sequence and the controlled A/B results are included above. I can collect additional targeted diagnostics or test a patch if needed.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with sing-mux v0.3.5 client.go around Client.offer() and h2mux.go around RoundTrip(), setup(), and IsClosed(). Trace how a failed stream affects the parent session and how the Brutal path selects sessions. Done means an unusable h2mux session is detected and evicted or replaced, preventing repeated closed-pipe errors; validate against the described h2mux and TCP Brutal workload.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100