tproxy: UDP write-back connection not cached when netns is configured, causing excessive syscall overhead
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
dev-next
Installation type
Original sing-box Command Line
If you are using a graphical client, please provide the version of the client.
No response
Version
Description
Related
Follow-up to #3872. While #3872 addresses the leaked currentNs fd in ListenNetworkNamespace, this issue covers the underlying performance problem that makes the leak so severe.
Problem
In protocol/redirect/tproxy.go, tproxyPacketWriter.WritePacket explicitly skips connection caching when NetNs is configured:
// line 126: cache only used when NetNs == ""
if w.listener.ListenOptions().NetNs == "" {
conn := w.conn
if w.destination == destination && conn != nil {
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
// ...
return err
}
}
// line 144: cache only stored when NetNs == ""
if w.listener.ListenOptions().NetNs == "" && w.destination == destination {
w.conn = udpConn
} else {
defer udpConn.Close()
}
This means every single UDP response packet goes through the full netns switch cycle:
runtime.LockOSThread()
→ netns.Get() // syscall: open("/proc/.../ns/net")
→ netns.Set(targetNs) // syscall: setns()
→ net.ListenPacket() // syscall: socket() + bind()
→ netns.Set(currentNs) // syscall: setns()
runtime.UnlockOSThread()
→ WriteToUDPAddrPort() // actual send
→ udpConn.Close() // syscall: close()
That is 6+ syscalls per UDP response packet, compared to 1 syscall (WriteToUDPAddrPort) when NetNs is empty and the conn is cached.
For high-throughput UDP flows (e.g., QUIC), this results in thousands of unnecessary netns switches per second, significant CPU overhead, and was the amplifier that made the fd leak
in #3872 so quickly fatal.
Expected Behavior
A socket created inside a target netns via ListenNetworkNamespace remains bound to that netns after the thread switches back — the fd is valid and usable from any thread. There is no
reason to skip caching.
The write-back conn should be cached and reused regardless of whether NetNs is configured, the same way it already works for the NetNs == "" case.
Suggested Fix
Remove the two NetNs == "" guards in WritePacket:
func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer buffer.Release()
// Use cached conn (regardless of NetNs)
conn := w.conn
if w.destination == destination && conn != nil {
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
if err != nil {
w.conn = nil
}
return err
}
// ... create new conn via ListenNetworkNamespace ...
// Cache conn (regardless of NetNs)
if w.destination == destination {
w.conn = udpConn
} else {
defer udpConn.Close()
}
return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), w.source))
}
Cleanup is already handled: preparePacketConnection closes writer.conn when the udpnat2 entry is evicted on timeout.
Impact
- Before: Every UDP response packet = 6+ syscalls + thread lock (with netns)
- After: First packet per flow = 1 netns switch; all subsequent packets = 1 syscall
Reproduction
Logs
Supporter
- I am a sponsor
Integrity 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 confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
- I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
- I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.
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 in protocol/redirect/tproxy.go at tproxyPacketWriter.WritePacket and inspect how ListenNetworkNamespace creates the UDP connection. Verify the cached connection is reused when NetNs is configured, while preparePacketConnection still closes writer.conn after udpnat2 eviction; completion should eliminate repeated netns setup and close operations for packets in the same flow.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, linux
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100