mirror of
https://github.com/v2rayA/v2rayA.git
synced 2026-10-28 21:08:07 +08:00
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:
+4
-4
@@ -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()
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user