fix: prevent DNS redirect loop causing unbounded memory growth

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.
This commit is contained in:
zhangthree78
2026-08-16 23:59:43 +08:00
parent 1550d843d0
commit 525654ec9b
9 changed files with 184 additions and 134 deletions
+4 -4
View File
@@ -104,7 +104,7 @@ func (h *DnsHandler) HandleQuery(ctx context.Context, query *DnsQuery) (*DnsResp
if query.ClientIP != nil {
clientIP = query.ClientIP.String()
}
log.Printf("[dns] query: %s %s from %s", QTypeToString(query.QType), query.Name, clientIP)
dnsLogf("[dns] query: %s %s from %s", QTypeToString(query.QType), query.Name, clientIP)
// Get module references.
module := h.module
@@ -216,7 +216,7 @@ func (h *DnsHandler) handleRoute(ctx context.Context, query *DnsQuery, module *D
if result.RuleID != "" {
ruleInfo = fmt.Sprintf(" rule(%s)", result.RuleID)
}
log.Printf("[dns] route: %s %s → %s(%s)%s action(route)",
dnsLogf("[dns] route: %s %s → %s(%s)%s action(route)",
QTypeToString(query.QType), query.Name, upstreamID, proxyInfo, ruleInfo)
// Check cache (if enabled).
@@ -288,7 +288,7 @@ func (h *DnsHandler) handleRoute(ctx context.Context, query *DnsQuery, module *D
}
// Log successful upstream response.
log.Printf("[dns] upstream: %s via %s (rtt=%dms)",
dnsLogf("[dns] upstream: %s via %s (rtt=%dms)",
upstream.Addr, proxyInfo, rtt.Milliseconds())
// Store response in cache (if enabled and not a bootstrap query).
@@ -393,7 +393,7 @@ func (h *DnsHandler) handleBypass(query *DnsQuery, module *DnsModule, result *Ro
}
// Log successful bypass response.
log.Printf("[dns] upstream: %s:53 direct (rtt=%dms)",
dnsLogf("[dns] upstream: %s:53 direct (rtt=%dms)",
upstream.Addr, rtt.Milliseconds())
module.stats.mu.Lock()
+1 -1
View File
@@ -172,7 +172,7 @@ func (l *DnsListener) handlePacket(w dns.ResponseWriter, msg *dns.Msg) {
query.ClientIP = addr.IP
}
log.Printf("[dns] query: %s %s from %s", dns.Type(q.Qtype).String(), q.Name, clientAddr.String())
dnsLogf("[dns] query: %s %s from %s", dns.Type(q.Qtype).String(), q.Name, clientAddr.String())
ctx, cancel := context.WithTimeout(l.ctx, time.Duration(l.config.Timeout)*time.Second)
defer cancel()
+2 -2
View File
@@ -84,7 +84,7 @@ func (r *Router) Route(query *DnsQuery) *RouteResult {
// Rule matched — determine upstream address.
upstreamAddr := r.upstreamMap[rule.Upstream]
log.Printf("[dns router] rule %q matched query %s %s → upstream=%s action=%s",
dnsLogf("[dns router] rule %q matched query %s %s → upstream=%s action=%s",
rule.ID, query.Name, rule.Upstream, upstreamAddr, rule.Action)
action := rule.Action
@@ -111,7 +111,7 @@ func (r *Router) Route(query *DnsQuery) *RouteResult {
}
// No rule matched — use default upstream.
log.Printf("[dns router] no rule matched for %s %d → using default upstream %s",
dnsLogf("[dns router] no rule matched for %s %d → using default upstream %s",
query.Name, query.QType, r.defaultUpstream)
defaultAddr := r.upstreamMap[r.defaultUpstream]
+70
View File
@@ -3,9 +3,79 @@
package dns
import (
"log"
"net"
"os"
"strconv"
"syscall"
"time"
"github.com/miekg/dns"
)
// dnsDebugEnabled controls per-query verbose logging. It is disabled by
// default because at info level every DNS query logs 2-3 lines; under a
// high query load (e.g. cloud-agent monitoring traffic) that log storm
// consumes CPU/IO/memory and can OOM the process. Enable with
// V2RAYA_DNS_DEBUG=1 when troubleshooting.
var dnsDebugEnabled = func() bool {
v, err := strconv.ParseBool(os.Getenv("V2RAYA_DNS_DEBUG"))
if err != nil {
return false
}
return v
}()
// dnsLogf logs a line only when verbose DNS debug logging is enabled.
// High-frequency per-query logs MUST use this instead of log.Printf.
func dnsLogf(format string, args ...interface{}) {
if dnsDebugEnabled {
log.Printf(format, args...)
}
}
// markFd returns a net.Dialer.Control function that sets SO_MARK=0x80 on
// every socket created by the dialer. 0x80 is the fwmark checked by the
// v2raya nftables/iptables DNS redirect chains (mark & 0x80 == 0x80 → RETURN),
// which prevents the DNS module's own upstream queries from being hijacked
// back into itself (redirect loop → unbounded memory growth).
func markFd(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
_ = setSocketMark(fd) // SO_MARK=36
})
}
// markedDialer returns a *net.Dialer that sets SO_MARK=0x80 on all sockets.
func markedDialer() *net.Dialer {
return &net.Dialer{
Timeout: 5 * time.Second,
Control: markFd,
KeepAlive: 30 * time.Second,
}
}
// newMarkedDnsClient creates a *dns.Client whose every socket carries the
// SO_MARK=0x80 mark, so upstream DNS queries bypass the transparent-proxy
// redirect rules. Use this for ALL DNS upstream clients; creating a client
// without the mark (e.g. a bare &dns.Client{}) makes its queries loop back
// into the DNS module and exhaust memory.
//
// Note: miekg/dns dials through net.Dialer for UDP as well, so the marked
// Dialer covers every transport this client supports — no manual socket
// path is needed. UDPSize is set to 4096 so queries without an EDNS0 OPT
// record still get a 4 KiB receive buffer instead of miekg/dns's 512-byte
// default (queries built with SetEdns0 override this via their OPT record).
func newMarkedDnsClient(network string) *dns.Client {
return &dns.Client{
Net: network,
UDPSize: 4096,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Dialer: markedDialer(),
}
}
// setSocketMark sets SO_MARK on a socket identified by its file descriptor.
// SO_MARK (option 36) is used for iptables/nftables mark-based filtering
// to prevent DNS query loops (mark 0x80 → iptables RETURN).
+55
View File
@@ -2,8 +2,63 @@
package dns
import (
"log"
"net"
"os"
"strconv"
"syscall"
"time"
"github.com/miekg/dns"
)
// dnsDebugEnabled controls per-query verbose logging (see socket_unix.go).
var dnsDebugEnabled = func() bool {
v, err := strconv.ParseBool(os.Getenv("V2RAYA_DNS_DEBUG"))
if err != nil {
return false
}
return v
}()
// dnsLogf logs a line only when verbose DNS debug logging is enabled.
func dnsLogf(format string, args ...interface{}) {
if dnsDebugEnabled {
log.Printf(format, args...)
}
}
// setSocketMark is a no-op on Windows since SO_MARK is a Linux-specific
// socket option used for iptables/nftables mark-based filtering.
func setSocketMark(fd uintptr) error {
return nil
}
// markFd is a no-op Control function on Windows (SO_MARK unsupported).
func markFd(network, address string, c syscall.RawConn) error {
return nil
}
// markedDialer returns a plain dialer on Windows (no socket marking).
func markedDialer() *net.Dialer {
return &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
}
// newMarkedDnsClient creates a *dns.Client. On Windows the mark is a no-op;
// on Linux it sets SO_MARK=0x80 to bypass transparent-proxy DNS redirects.
// UDPSize=4096 gives queries without an EDNS0 OPT record a 4 KiB receive
// buffer instead of miekg/dns's 512-byte default.
func newMarkedDnsClient(network string) *dns.Client {
return &dns.Client{
Net: network,
UDPSize: 4096,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Dialer: markedDialer(),
}
}
+6 -13
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"log"
"sync"
"time"
"github.com/miekg/dns"
)
@@ -92,12 +91,7 @@ func NewUpstreamManager(configs []UpstreamConfig) *UpstreamManager {
ProxyTag: proxyTag,
Healthy: true,
Bootstrap: cfg.Bootstrap,
Client: &dns.Client{
Net: net,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
},
Client: newMarkedDnsClient(net),
}
compositeKey := instance.CompositeKey()
@@ -176,12 +170,7 @@ func (m *UpstreamManager) GetOrCreateUpstream(config UpstreamConfig) *UpstreamIn
ProxyTag: proxyTag,
Healthy: true,
Bootstrap: config.Bootstrap,
Client: &dns.Client{
Net: net,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
},
Client: newMarkedDnsClient(net),
}
m.upstreams[compositeKey] = instance
@@ -241,6 +230,10 @@ func (m *UpstreamManager) pingUpstream(upstream *UpstreamInstance) bool {
msg.SetQuestion(".", dns.TypeSOA)
msg.RecursionDesired = true
// upstream.Client is always built by newMarkedDnsClient, whose Dialer
// sets SO_MARK=0x80 on every socket (UDP included — miekg/dns dials via
// net.Dialer, which honors Control for all socket types). The mark lets
// health-check pings bypass the transparent-proxy DNS redirect rules.
_, _, err := client.Exchange(msg, upstream.Addr)
return err == nil
}
+10 -97
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net"
"syscall"
"time"
"github.com/miekg/dns"
@@ -105,28 +104,13 @@ func (m *UpstreamManager) exchangeDirect(upstream *UpstreamInstance, query *DnsQ
protocol = "udp"
}
log.Printf("[dns upstream] exchange direct: %s %s → %s (%s)",
dnsLogf("[dns upstream] exchange direct: %s %s → %s (%s)",
dns.Type(uint16(query.QType)).String(), query.Name, upstream.Addr, protocol)
// Create DNS client with SO_MARK=0x80 on all sockets.
// The Control function sets the socket mark to 0x80, which iptables
// DNS_MARK/TP_OUT chains check and RETURN (skip), preventing the loop.
markFd := func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
_ = setSocketMark(fd) // SO_MARK=36
})
}
client := &dns.Client{
Net: protocol,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Dialer: &net.Dialer{
Timeout: 5 * time.Second,
Control: markFd,
},
}
client := newMarkedDnsClient(protocol)
// Attempt the exchange with retry logic.
var resp *dns.Msg
@@ -140,11 +124,7 @@ func (m *UpstreamManager) exchangeDirect(upstream *UpstreamInstance, query *DnsQ
time.Sleep(100 * time.Millisecond)
}
if protocol == "udp" {
resp, rtt, err = m.exchangeUDPWithMark(client, msg, upstream.Addr)
} else {
resp, rtt, err = client.Exchange(msg, upstream.Addr)
}
resp, rtt, err = client.Exchange(msg, upstream.Addr)
if err != nil {
log.Printf("[dns upstream] attempt %d error: %s %s → %s: %v", attempt+1,
@@ -157,14 +137,7 @@ func (m *UpstreamManager) exchangeDirect(upstream *UpstreamInstance, query *DnsQ
log.Printf("[dns upstream] truncated response, falling back to TCP: %s %s → %s",
dns.Type(uint16(query.QType)).String(), query.Name, upstream.Addr)
tcpClient := &dns.Client{
Net: "tcp",
Timeout: 5 * time.Second,
Dialer: &net.Dialer{
Timeout: 5 * time.Second,
Control: markFd,
},
}
tcpClient := newMarkedDnsClient("tcp")
resp, rtt, err = tcpClient.Exchange(msg, upstream.Addr)
if err != nil {
log.Printf("[dns upstream] TCP fallback error (attempt %d): %s %s → %s: %v", attempt+1,
@@ -215,72 +188,13 @@ func (m *UpstreamManager) exchangeDirect(upstream *UpstreamInstance, query *DnsQ
Cached: false,
}
log.Printf("[dns upstream] direct response: %s %s → %s (rcode=%d, rtt=%v, answers=%d)",
dnsLogf("[dns upstream] direct response: %s %s → %s (rcode=%d, rtt=%v, answers=%d)",
dns.Type(uint16(query.QType)).String(), query.Name, upstream.Addr,
resp.Rcode, rtt, len(resp.Answer))
return dnsResp, nil
}
// exchangeUDPWithMark sends a DNS query over UDP with SO_MARK=0x80 set on the socket.
// miekg/dns.Client.Exchange for UDP doesn't use the Dialer.Control function,
// so we create the UDP socket manually to set the socket mark.
func (m *UpstreamManager) exchangeUDPWithMark(client *dns.Client, msg *dns.Msg, addr string) (*dns.Msg, time.Duration, error) {
// Resolve UDP address
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, 0, fmt.Errorf("resolve udp: %w", err)
}
// Create UDP connection with SO_MARK=0x80
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, 0, fmt.Errorf("dial udp: %w", err)
}
defer conn.Close()
// Set SO_MARK on the UDP socket
rawConn, err := conn.SyscallConn()
if err != nil {
return nil, 0, fmt.Errorf("get raw conn: %w", err)
}
rawConn.Control(func(fd uintptr) {
_ = setSocketMark(fd)
})
// Pack and send the DNS query
packed, err := msg.Pack()
if err != nil {
return nil, 0, fmt.Errorf("dns pack: %w", err)
}
start := time.Now()
if _, err := conn.Write(packed); err != nil {
return nil, 0, fmt.Errorf("dns write: %w", err)
}
// Read response — use dns.MaxMsgSize (65535) as the receive buffer.
// The query has EDNS0 UDP size 4096, so the upstream may respond with
// up to 4096 bytes. dns.DefaultMsgSize (512) is too small and would
// silently truncate large responses (UDP datagram remainder is lost).
respBuf := make([]byte, dns.MaxMsgSize)
conn.SetReadDeadline(time.Now().Add(client.ReadTimeout))
n, err := conn.Read(respBuf)
rtt := time.Since(start)
if err != nil {
return nil, rtt, fmt.Errorf("dns read: %w", err)
}
// Unpack response
resp := new(dns.Msg)
if err := resp.Unpack(respBuf[:n]); err != nil {
return nil, rtt, fmt.Errorf("dns unpack: %w", err)
}
return resp, rtt, nil
}
// exchangeViaProxy sends a DNS query through a proxy channel.
// It uses the configured ProxyAddrResolver to find the SOCKS5/HTTP proxy
// address for the given proxy tag, then sends the DNS query over TCP
@@ -573,12 +487,11 @@ func (m *UpstreamManager) getClientForProtocol(upstream *UpstreamInstance, proto
}
// Create a new client for the requested protocol.
return &dns.Client{
Net: protocol,
Timeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
// IMPORTANT: the client MUST carry SO_MARK=0x80 (newMarkedDnsClient),
// otherwise its queries are hijacked by the transparent-proxy redirect
// rules back into this DNS module, forming an infinite loop that grows
// memory until the host is exhausted.
return newMarkedDnsClient(protocol)
}
// ExchangeRaw sends a raw *dns.Msg to the upstream and returns the raw response.
+12 -9
View File
@@ -77,11 +77,6 @@ iptables -w 2 -t nat -A TP_RULE -m mark --mark 0x80/0x80 -j RETURN
# DNS 重定向到新 DNS 模块端口 52353(必须在通用 REDIRECT 规则之前)
iptables -w 2 -t nat -A DNS_REDIRECT -m mark --mark 0x80/0x80 -j RETURN
iptables -w 2 -t nat -A DNS_REDIRECT -j REDIRECT --to-port 52353
# 在 PREROUTING 和 OUTPUT 中插入 DNS 规则(优先于 TP_PRE/TP_OUT)
iptables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j DNS_REDIRECT
`
for _, v := range GetExcludedInterfaces() {
commands += fmt.Sprintf("iptables -w 2 -t nat -A TP_RULE -i %s -j RETURN\n", strings.ReplaceAll(v, "*", "+"))
@@ -103,6 +98,13 @@ iptables -w 2 -t nat -I PREROUTING -p tcp -j TP_PRE
iptables -w 2 -t nat -I OUTPUT -p tcp -j TP_OUT
iptables -w 2 -t nat -A TP_PRE -j TP_RULE
iptables -w 2 -t nat -A TP_OUT -j TP_RULE
# DNS 跳转在 TP_PRE/TP_OUT 之后插入:-I 总是插到链首,后插入者排在前面,
# 因此 DNS 规则实际位于通用透明代理规则之前(否则本机/局域网 TCP DNS 会
# 先命中 TP_OUT/TP_PRE 被重定向到 52345,绕过 DNS 模块)。
iptables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j DNS_REDIRECT
iptables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j DNS_REDIRECT
`
if IsIPv6Supported() {
commands += `
@@ -112,10 +114,6 @@ ip6tables -w 2 -t nat -N TP_RULE
ip6tables -w 2 -t nat -N DNS_REDIRECT
ip6tables -w 2 -t nat -A DNS_REDIRECT -m mark --mark 0x80/0x80 -j RETURN
ip6tables -w 2 -t nat -A DNS_REDIRECT -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -A TP_RULE -d ::/128 -j RETURN
ip6tables -w 2 -t nat -A TP_RULE -d ::1/128 -j RETURN
ip6tables -w 2 -t nat -A TP_RULE -d 64:ff9b::/96 -j RETURN
@@ -150,6 +148,11 @@ ip6tables -w 2 -t nat -I PREROUTING -p tcp -j TP_PRE
ip6tables -w 2 -t nat -I OUTPUT -p tcp -j TP_OUT
ip6tables -w 2 -t nat -A TP_PRE -j TP_RULE
ip6tables -w 2 -t nat -A TP_OUT -j TP_RULE
# DNS 跳转最后插入(-I 后到者居首),确保排在 TP_PRE/TP_OUT 之前
ip6tables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j DNS_REDIRECT
ip6tables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j DNS_REDIRECT
`
}
return Setter{
+24 -8
View File
@@ -37,6 +37,9 @@ iptables -w 2 -t mangle -X DNS_MARK 2>/dev/null || true
iptables -w 2 -t nat -D PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
iptables -w 2 -t nat -D OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
iptables -w 2 -t nat -D OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
# 清理 mark 0x80 豁免规则(配合上述 REDIRECT 的防回环豁免,成对删除避免重复累积)
iptables -w 2 -t nat -D OUTPUT -m mark --mark 0x80/0x80 -j RETURN 2>/dev/null || true
iptables -w 2 -t nat -D PREROUTING -m mark --mark 0x80/0x80 -j RETURN 2>/dev/null || true
# IPv6 清理
ip6tables -w 2 -t mangle -F DNS_MARK 2>/dev/null || true
ip6tables -w 2 -t mangle -D PREROUTING -p udp --dport 53 -j DNS_MARK 2>/dev/null || true
@@ -55,6 +58,9 @@ ip6tables -w 2 -t mangle -X DNS_MARK 2>/dev/null || true
ip6tables -w 2 -t nat -D PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
ip6tables -w 2 -t nat -D OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
ip6tables -w 2 -t nat -D OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353 2>/dev/null || true
# 清理 IPv6 mark 0x80 豁免规则
ip6tables -w 2 -t nat -D OUTPUT -m mark --mark 0x80/0x80 -j RETURN 2>/dev/null || true
ip6tables -w 2 -t nat -D PREROUTING -m mark --mark 0x80/0x80 -j RETURN 2>/dev/null || true
# 清理 TProxy 链
iptables -w 2 -t mangle -F TP_OUT 2>/dev/null || true
iptables -w 2 -t mangle -D OUTPUT -j TP_OUT 2>/dev/null || true
@@ -152,19 +158,29 @@ func writeTransparentProxyRules(tmpl *Template) (err error) {
// 无论哪种透明代理模式,都用 nat 表的 REDIRECT 将 DNS 流量(:53)转到 DNS 模块(:52353)。
// 同时拦截 OUTPUT(本地进程)和 PREROUTING(LAN 设备)的 DNS 查询。
// TPROXY 模式对回环(loopback)流量的 TPROXY 拦截不可靠,而 REDIRECT 在 OUTPUT 链上稳定。
//
// IMPORTANT (fix): mark 0x80 豁免规则必须排在 REDIRECT 规则之前,否则 v2raya-core
// 自己向上游转发的 DNS 查询(socket 带 SO_MARK=0x80)会被自己的 REDIRECT 规则劫持回
// :52353,形成无限回环(内存雪崩直至 OOM)。iptables 按顺序匹配:
// - REDIRECT 用 -A(追加到链尾),确保在 mark 豁免之后
// - mark 豁免用 -I(插入到链首),确保最先匹配
if ShouldLocalDnsListen() {
dnsRedirect := `
iptables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353
iptables -w 2 -t nat -I OUTPUT -m mark --mark 0x80/0x80 -j RETURN
iptables -w 2 -t nat -I PREROUTING -m mark --mark 0x80/0x80 -j RETURN
`
if iptables.IsIPv6Supported() {
dnsRedirect += `
ip6tables -w 2 -t nat -I PREROUTING -p udp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -I PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -I OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -I OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 52353
ip6tables -w 2 -t nat -I OUTPUT -m mark --mark 0x80/0x80 -j RETURN
ip6tables -w 2 -t nat -I PREROUTING -m mark --mark 0x80/0x80 -j RETURN
`
}
cmds.ExecCommands(dnsRedirect, false)