From 525654ec9bb8cfe60d53274b3f5425ff8abdc2ae Mon Sep 17 00:00:00 2001 From: zhangthree78 Date: Sun, 16 Aug 2026 22:17:27 +0800 Subject: [PATCH] fix: prevent DNS redirect loop causing unbounded memory growth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/dns/handler.go | 8 +-- core/dns/listener.go | 2 +- core/dns/router.go | 4 +- core/dns/socket_unix.go | 70 ++++++++++++++++++ core/dns/socket_windows.go | 55 ++++++++++++++ core/dns/upstream.go | 19 ++--- core/dns/upstream_stub.go | 107 +++------------------------- service/kernel/iptables/redirect.go | 21 +++--- service/kernel/v2ray/transparent.go | 32 ++++++--- 9 files changed, 184 insertions(+), 134 deletions(-) diff --git a/core/dns/handler.go b/core/dns/handler.go index d049225d..24318b2b 100644 --- a/core/dns/handler.go +++ b/core/dns/handler.go @@ -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() diff --git a/core/dns/listener.go b/core/dns/listener.go index e958e2ac..a369568e 100644 --- a/core/dns/listener.go +++ b/core/dns/listener.go @@ -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() diff --git a/core/dns/router.go b/core/dns/router.go index c26023e0..ea14ee05 100644 --- a/core/dns/router.go +++ b/core/dns/router.go @@ -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] diff --git a/core/dns/socket_unix.go b/core/dns/socket_unix.go index 15930edd..c92da958 100644 --- a/core/dns/socket_unix.go +++ b/core/dns/socket_unix.go @@ -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). diff --git a/core/dns/socket_windows.go b/core/dns/socket_windows.go index 3cbd4cc1..737f556e 100644 --- a/core/dns/socket_windows.go +++ b/core/dns/socket_windows.go @@ -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(), + } +} diff --git a/core/dns/upstream.go b/core/dns/upstream.go index 31da830d..9a13e899 100644 --- a/core/dns/upstream.go +++ b/core/dns/upstream.go @@ -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 } diff --git a/core/dns/upstream_stub.go b/core/dns/upstream_stub.go index 1f8574a1..227b043a 100644 --- a/core/dns/upstream_stub.go +++ b/core/dns/upstream_stub.go @@ -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. diff --git a/service/kernel/iptables/redirect.go b/service/kernel/iptables/redirect.go index 2e0e856c..36ff0caa 100644 --- a/service/kernel/iptables/redirect.go +++ b/service/kernel/iptables/redirect.go @@ -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{ diff --git a/service/kernel/v2ray/transparent.go b/service/kernel/v2ray/transparent.go index 13609203..f12f3e30 100644 --- a/service/kernel/v2ray/transparent.go +++ b/service/kernel/v2ray/transparent.go @@ -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)