Hijacked DNS sessions ignore the documented 10s protocol timeout
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.1k
- Forks
- 4.6k
- Avg merge
- 19d 15h
- Merged PRs (30d)
- 1
Description
Hijacked DNS sessions ignore the documented 10s protocol timeout, causing "unpack request: bad rdata" floods
Version
sing-box 1.13.18 (verified against the tag; also present in 1.13.11).
Reproduced on Windows 10, Android 11 and iOS 26 clients using tun inbound.
What happens
With tun + action: hijack-dns, the log fills with parse failures for packets
that are not DNS at all:
router: process DNS packet: unpack request: bad question name: dns: bad rdata
router: process DNS packet: unpack request: bad question name: dns: buffer size too small
They arrive in bursts — one connection ID producing 8–12 consecutive failures —
and the connection age at failure time clusters right below the inbound
udp_timeout:
ERROR [2631868615 57.25s] router: process DNS packet: unpack request: bad question name: dns: bad rdata
ERROR [2631868615 57.30s] ... (x4)
ERROR [2631868615 1m0s] ...
ERROR [2631868615 1m1s] ...
That log is from a client configured with "udp_timeout": "60s". With the
default 5m the same failures appear, just spread over a much wider window.
Root cause
1. The sniffer decides "this is DNS" once, for the whole session.
common/sniff/dns.go:
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg
err := msg.Unpack(packet)
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
return err
}
metadata.Protocol = C.ProtocolDNS
return nil
}
Only the first packet is validated. Every later packet on that UDP NAT session
is handed to the DNS request parser unconditionally.
2. The NAT session outlives the client socket.
The application sends a query from ephemeral port X, gets its answer, and closes
the socket. The OS is then free to hand port X to another process — but the NAT
session is still alive and still flagged ProtocolDNS. The new owner's traffic
lands in ExchangeDNSPacket, msg.Unpack fails, and each packet logs an error.
This matches the observed timing exactly: failures cluster just under the
session timeout, i.e. at the very end of the session's life, which is when
port reuse becomes likely.
3. The timeout that would prevent this exists, but never reaches hijacked sessions.
constant/timeout.go defines exactly the right value:
var ProtocolTimeouts = map[string]time.Duration{
ProtocolDNS: 10 * time.Second,
ProtocolNTP: 10 * time.Second,
ProtocolSTUN: 10 * time.Second,
ProtocolQUIC: 30 * time.Second,
ProtocolDTLS: 30 * time.Second,
}
and the documentation promises it
(Rule Action → udp_timeout):
Default value for protocol sniffed connections:
10s—dns,ntp,stun
But that table is only consulted in route/conn.go, on the path for connections
routed out through an outbound:
var udpTimeout time.Duration
if metadata.UDPTimeout > 0 {
udpTimeout = metadata.UDPTimeout
} else {
protocol := metadata.Protocol
...
udpTimeout = C.ProtocolTimeouts[protocol]
}
Hijacked DNS does not go through ConnectionManager. It takes the separate path
in route/dns.go:
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, ...) error {
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
...
natConn.SetHandler(&dnsHijacker{...})
return nil
}
No timeout is applied here, so the session keeps the inbound-wide udp_timeout
(5m by default) instead of the documented 10s for DNS.
4. The API to fix it already exists and is never used.
sing/common/udpnat2/conn.go provides:
func (c *natConn) SetTimeout(timeout time.Duration) bool
git grep SetTimeout over sing-box v1.13.18 returns no call sites.
Suggested fix
In hijackDNSPacket, shorten the session to the protocol timeout right where the
handler is installed:
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{}
natConn.SetTimeout(C.ProtocolTimeouts[C.ProtocolDNS]) // <-- 10s, as documented
...
}
This makes the behaviour match the documentation, and shrinks the window during
which a reused ephemeral port can be mistaken for an ongoing DNS session by a
factor of 30 (5m → 10s).
Notes
- The failures are harmless in themselves — the packet is dropped and the
application retries — but they drown real errors in the log. On one client
they accounted for the majority ofERRORlines over a night. - A partial workaround exists for one known source, mDNS/Bonjour, by routing
UDP port 5353 directly instead of hijacking it. That removes one contributor
but not the mechanism. - Possibly the same root cause as #3478 and #3531, which report the identical
parse errors without an explanation.
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 route/dns.go at hijackDNSPacket and inspect how the udpnat connection handler is installed. Read constant/timeout.go and sing/common/udpnat2/conn.go to confirm the existing protocol timeout and SetTimeout API. Done means hijacked DNS sessions use the documented 10-second timeout instead of the inbound-wide udp_timeout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100