Follow-up review of 525654e found:
1. Log gating was incomplete: only the direct/route path was gated;
cache-hit/cache-miss, proxy and dispatcher per-query logs were still
unconditional log.Printf (the hottest paths under load). Gate them
behind V2RAYA_DNS_DEBUG=1 too; error logs stay visible.
2. Stopping the transparent proxy left the direct nat OUTPUT/PREROUTING
REDIRECT --to-port 52353 rules and the fwmark 0x80 exemption rules in
place (Tproxy/Redirect GetCleanCommands only clean their own chains).
Once the DNS module exits, local DNS queries hit a dead port and DNS
fails. Add cleanDnsRedirectRules() and run it on the normal stop path.
3. module.go bootstrap client inlined its own markFd + marked dialer;
reuse newMarkedDnsClient (keeping the 3s fail-fast timeout).
4. dnsLogf/dnsDebugEnabled were duplicated in socket_unix.go /
socket_windows.go; move them to a common debug.go.
5. No unit tests for newMarkedDnsClient; add tests asserting
Net/UDPSize/Timeout and a non-nil Dialer.Control.
Verified: go build ./dns/... ./kernel/v2ray/... ; go test ./dns/...
Fixes defects in the v2.4.8+ transparent-proxy DNS path that cause the
core to exhaust memory within seconds on low-RAM hosts (see discussion
#1915: '2.4.9 内存占用过高' / '有严重的内存泄漏,别用了。本地DNS完全废掉了'):
1. core/dns: upstream queries from NewUpstreamManager /
GetOrCreateUpstream / getClientForProtocol used bare &dns.Client{}
with no Dialer configured, so their sockets never carried
SO_MARK=0x80. The unmarked queries were hijacked by the module's own
transparent-proxy DNS redirect rules and looped back into :52353 —
an infinite self-query loop and memory snowball.
2. service/kernel/v2ray/transparent.go: the unconditional DNS hijack
rules (REDIRECT --to-port 52353) had no fwmark-0x80 exemption, and
being inserted with -I they sat at the head of nat OUTPUT/PREROUTING,
so marked traffic was looped back as well.
3. core/dns: per-query info logging (query/route/upstream) produced
thousands of lines/second under load, amplifying CPU/IO/memory
pressure.
Fix:
- Route every upstream client through newMarkedDnsClient, whose
net.Dialer sets SO_MARK=0x80 on all sockets. miekg/dns dials UDP
through net.Dialer too, so Dialer.Control covers UDP sockets — no
manual socket path is needed. UDPSize=4096 keeps a sane receive
buffer for queries without an EDNS0 OPT record.
- Append the DNS redirects with -A and insert the mark exemption with
-I so the exemption is matched first; also delete the exemption
rules in the residual-rule cleanup so they don't accumulate across
restarts.
- redirect mode: insert the DNS_REDIRECT jumps after the TP_PRE/TP_OUT
jumps so they actually take precedence (-I inserts at chain head),
keeping local and LAN TCP DNS flowing to the DNS module (:52353)
instead of the proxy inbound (:52345).
- Gate per-query info logs behind V2RAYA_DNS_DEBUG=1; error logs stay
always visible.
Verified: unpatched v2.4.11 core RSS 230MB->1GB in ~6s then OOM/hang;
patched build stable at ~25MB idle / ~243MB loaded, DNS and SOCKS proxy
working.
## Issue 1: DNS rule matching logic (MatchAll + OrMatcher)
BuildMatchers flattened all geosite-expanded domains into a single list,
and the old MatchAny used AND logic (all matchers must match). This
made geosite:cn rules impossible to hit (e.g. qq.com and baidu.com
can never match simultaneously).
Fix: introduce OrMatcher to group matchers from the same rule field
with OR semantics, while different field groups use AND. The thousands
of domains from geosite:cn expansion are wrapped in a single OrMatcher
so matching any one domain satisfies the rule.
## Issue 2: Bootstrap self-reference loop (SO_MARK + public DNS fallback)
Bootstrap resolution runs before the DNS listener starts, but iptables
already hijacks :53 traffic:
- bootstrap query to system DNS(:53) → iptables redirects to 127.2.0.17:53
- DNS module not yet listening → connection refused → bootstrap fails
- Domain upstreams (e.g. dns.google) remain unresolved → relies on proxy
- Proxy DNS may re-enter DNS module → loop or REFUSED
Fix:
1. Add SO_MARK=0x80 to bootstrap DNS client to bypass iptables redirect
2. collectBootstrapDns() with 3-tier priority (config > system > hardcoded)
3. Hardcoded fallback includes Alibaba/Tencent/Google/Cloudflare: 13 servers
4. isOwnAddress() filters self-referencing addresses (127.0.0.1:53 etc.)
## Issue 3: handlePacket mutates cached response in-place
m = resp.RawMsg; m.SetReply(msg) directly modifies the *dns.Msg shared
with the cache, corrupting subsequent cache hits. SetReply also forces
Rcode=RcodeSuccess, overwriting NXDOMAIN etc. from upstream.
Fix: Pack/Unpack deep copy before modification; preserve original Rcode.
## Issue 4: Double OPT record causes FORMERR
Three exchange functions called AddECSSubnet BEFORE SetEdns0.
miekg/dns v1.1.72's SetEdns0 always appends a new OPT record without
checking for existing ones, producing a malformed query with two OPT
records.
Fix: call SetEdns0 before AddECSSubnet; use UDP size 4096 in
AddECSSubnet instead of 512 to preserve the size set by SetEdns0.
## Issue 5: localhost upstream skipped in template (template_dns.go)
localhost upstream was continue-skipped during config generation,
making the geosite:private rule reference a non-existent upstream.
Fix: map localhost to 127.0.0.1, keep it as a valid upstream.
## Incidental fixes
- handler.go: remove duplicate :53 in log format (dns.google:53:53)
- response.go: use 4096 instead of dns.DefaultMsgSize(512) for ECS