Skip to content

跨境网络彻底杜绝DNS污染与泄漏:SmartDNS + DoH分流实战与运营商DNS劫持防御体系

约 16835 字大约 56 分钟

科学上网网络技术翻墙教程DoH分流

...

2026-10-09

运营商DNS劫持和污染是跨境网络最隐蔽的坑。你明明配了DoH,dig查着也正常,但浏览器打开境外站点还是被解析到127.0.0.1或者假IP。问题出在分流策略上:系统DNS、浏览器DoH、代理内核DNS三套解析器各跑各的,泄漏点根本不止一个。本文用SmartDNS做本地DNS调度中枢,把国内域名交给运营商DNS走直连,境外域名强制走DoH加密查询,再配合sing-box的DNS规则做二次兜底。我会给出完整的smartdns.conf、sing-box DNS配置和OpenWrt防火墙53端口劫持规则,并用tcpdump和mtr实测验证污染是否真正消除。不堆概念,只讲怎么配、怎么查、怎么堵住每一个泄漏口。

一、DNS污染与泄漏的底层机理:从UDP明文到运营商劫持链路全解剖

很多人配了一堆DoH、装了SmartDNS,最后发现该被污染的域名照样打不开,该泄漏的查询照样裸奔。问题出在对DNS这条链路缺乏最基本的认知。你发出的那一个A记录查询,从网卡出去到拿到应答,中间经过的每一个节点都可能动手脚。搞不清这些节点分别在哪里、用什么手法、在什么时间窗口动手,后面所有配置都是盲人摸象。

1.1 一次跨境DNS查询的完整路径解剖

先看一条最普通的查询:dig @223.5.5.5 google.com A。这条查询从你的设备到最终拿到答案,路径是这样的:

你的设备(192.168.1.100)
  │  UDP/53 明文查询
  ▼
本地网关/路由器(192.168.1.1)  ← 这里可能被路由器固件的DNS劫持
  │  NAT转发,源端口随机
  ▼
运营商接入网(BRAS/OLT)  ← 部分运营商在此部署透明DNS代理
  │
  ▼
运营商递归DNS(223.5.5.5)  ← 可能返回缓存、可能返回广告IP
  │  如果缓存未命中,递归查询
  ▼
根服务器/顶级域/权威服务器  ← 跨境查询,经过国际出口
  │
  ▼
GFW国际出口节点  ← UDP/53明文查询在此被检测和抢答

关键点在于:GFW的DNS污染发生在国际出口的旁路设备上,它不关心你用的是哪个DNS服务器。只要你的查询包经过国际出口且是UDP/53明文,且查询的域名在黑名单上,旁路设备就会在你真实响应回来之前,伪造一个响应包发回给你。你的设备收到第一个响应就认为查询完成,后面的真实响应被丢弃。

这就是为什么很多人把DNS改成8.8.8.8之后发现google.com解析出来一个莫名其妙的IP,比如31.13.64.35(Facebook的段)或者59.24.3.173(一个早已不存在的IP)。这些伪造IP的共同特征是:要么完全不可达,要么指向一个黑洞。

1.2 UDP抢答的时间窗口:为什么换DNS有时候没用

用tcpdump可以直观看到抢答过程。在一台能访问国际出口的机器上执行:

# 终端1:抓包
tcpdump -i eth0 -n port 53 -vv -w dns.pcap

# 终端2:发起查询
dig @8.8.8.8 google.com A

抓包结果用tcpdump -r dns.pcap -nn回放,典型输出:

14:23:01.123456 IP 192.168.1.100.54321 > 8.8.8.8.53: 12345+ A? google.com. (28)
14:23:01.145678 IP 8.8.8.8.53 > 192.168.1.100.54321: 12345 1/0/0 A 59.24.3.173 (44)
14:23:01.187654 IP 8.8.8.8.53 > 192.168.1.100.54321: 12345 1/0/0 A 142.250.72.14 (44)

注意时间戳:伪造响应在发出查询后22ms到达,真实响应在64ms到达。伪造响应比真实响应早了42ms。这个时间差就是GFW旁路设备的处理优势:它不需要做递归查询,只需要匹配域名关键字然后立即构造响应包。

这里有个容易被忽略的细节:伪造响应的源IP是8.8.8.8,和真实响应完全一样。你的设备无法通过源IP区分。唯一的区别是响应内容。操作系统和DNS客户端通常接受第一个到达的响应,后面的被当作重复包丢弃。

为什么TCP/53能绕过部分污染:TCP是面向连接的,需要三次握手。GFW的旁路设备要伪造TCP响应,必须先伪造SYN-ACK,再伪造数据包,时序上更难抢在真实响应之前。但这不意味着TCP/53就安全,只是抢答难度更高。实际上很多网络环境下TCP/53直接被RST阻断,连握手都完不成。

1.3 运营商透明DNS代理的识别与验证

比GFW污染更隐蔽的是运营商的透明DNS代理。你明明把DNS设成了8.8.8.8,但查询实际上被骨干网设备重定向到了运营商自己的DNS。你看到的解析结果全部来自运营商缓存,污染和广告注入照样存在。

验证方法:

# 查询whoami.akamai.net,这个域名会返回查询者出口IP的Akamai节点
dig @8.8.8.8 whoami.akamai.net A +short

正常情况(未被透明代理)返回的IP应该属于Google的网段(8.8.8.8的响应来自Google DNS,返回的Akamai节点IP通常是Akamai的公开节点)。如果返回的IP属于你所在运营商的网段(比如112.xx.xx.xx、183.xx.xx.xx),说明UDP/53查询被透明重定向了。

另一个验证手段是查resolver.dnscrypt.info的DNS泄漏测试,或者用dig @8.8.8.8 o-o.myaddr.l.google.com TXT,这个域名会返回Google看到的你的出口IP。如果返回的IP和你实际公网IP不一致,中间有NAT或代理。

1.4 运营商DNS劫持的两种典型模式

模式一:HTTP 302重定向型

运营商递归DNS对不存在的域名返回一个广告服务器的IP。你的浏览器发起HTTP请求,广告服务器返回302重定向到运营商的门户网站。这种模式在跨境场景下的危害是:你的分流规则基于域名匹配,但DNS返回的IP是广告服务器IP,分流规则可能把这个IP误判为国内IP,导致流量走直连,最终打开的是广告页而不是目标网站。

模式二:NXDOMAIN型

运营商对不存在的域名返回NXDOMAIN,但TTL设置极短(比如60秒),同时在自己的缓存中记录这个查询。当用户再次查询时,返回广告IP。这种模式更隐蔽,因为第一次查询看起来是正常的NXDOMAIN。

两种模式的共同特征是:TTL异常、返回的IP不属于目标域名的权威网段、多次查询结果不一致。

1.5 DNS泄漏的定义边界:不止是“用了国内DNS”

很多人以为只要把系统DNS改成8.8.8.8或者DoH地址就不泄漏了。实际泄漏路径远不止这一条:

路径一:系统fallback DNS。Windows在配置了DoH之后,如果DoH查询超时,会自动fallback到明文UDP/53查询。这个fallback行为在Windows 11的某些版本中默认开启,且不会在UI中提示。

路径二:浏览器自带DNS。Chrome和Firefox都有内置的DoH配置。如果浏览器的DoH和系统的DoH配置不一致,浏览器可能绕过系统DNS直接查询。Chrome的chrome://settings/security中可以查看当前DoH提供商。

路径三:应用内硬编码DNS。部分应用(尤其是游戏和流媒体客户端)内置了DNS服务器地址,完全绕过系统DNS设置。典型的是某些Android应用使用8.8.8.8硬编码,即使系统设置了DoH也无效。

路径四:DNS over TLS的SNI暴露。DoT虽然加密了查询内容,但TLS握手的SNI字段暴露了DoT服务器地址。GFW可以通过SNI识别出你在使用dns.google或cloudflare-dns.com,然后对TLS连接进行干扰或阻断。这是选择DoH而非DoT的重要依据:DoH的HTTPS流量和普通HTTPS流量在SNI层面无法区分(都是443端口,SNI可能是任意域名)。

1.6 污染检测与排查命令组

以下命令组用于快速判断当前网络的DNS污染状态:

# 1. 检测当前系统DNS配置
resolvectl status          # systemd-resolved
scutil --dns               # macOS
cat /etc/resolv.conf       # 通用Linux

# 2. 对比不同DNS的解析结果(污染检测核心)
echo "=== 8.8.8.8 UDP ==="
dig @8.8.8.8 facebook.com A +short +time=3
echo "=== 114.114.114.114 ==="
dig @114.114.114.114 facebook.com A +short +time=3
echo "=== 223.5.5.5 ==="
dig @223.5.5.5 facebook.com A +short +time=3
echo "=== 8.8.8.8 DoH ==="
curl -s -H "accept: application/dns-json" \
  "https://dns.google/resolve?name=facebook.com&type=A" | \
  python3 -c "import sys,json; d=json.load(sys.stdin); print([a['data'] for a in d.get('Answer',[])])"

# 3. 抓包看实际DNS查询走向
tcpdump -i any -n port 53 -vv -c 20

# 4. 检测DoH是否真正生效
curl -v --doh-url https://dns.google/dns-query https://www.google.com 2>&1 | grep -E "Connected|SSL|HTTP"

# 5. 检测透明DNS代理
dig @8.8.8.8 whoami.akamai.net A +short
dig @8.8.8.8 o-o.myaddr.l.google.com TXT +short

1.7 各方案实测数据对比

在同一个网络环境(中国电信家宽,500Mbps下行)下,对facebook.com的A记录解析进行多方案对比:

检测项运营商DNS(202.96.128.86)8.8.8.8 UDP8.8.8.8 DoHSmartDNS+DoH分流
facebook.com解析结果污染IP(59.24.3.173)污染IP(随机)真实IP(157.240.x.x)真实IP(157.240.x.x)
解析延迟(首次)8ms超时/污染320ms280ms
解析延迟(缓存命中)3msN/AN/A6ms
google.com解析污染IP污染IP真实IP真实IP
baidu.com解析正常(5ms)正常(180ms)正常(350ms)正常(5ms)
泄漏风险完全暴露UDP可被抢答低(HTTPS加密)极低
运营商可见性完全可见可见目标IP仅见DoH服务器IP仅见DoH服务器IP
抗污染能力无无强强

从表格可以清晰看到:运营商DNS对国内域名延迟最低(3-8ms),但对国外域名完全无抗污染能力。8.8.8.8 UDP在跨境场景下基本不可用(污染+超时)。DoH解决了抗污染问题但引入了300ms+的首次解析延迟。SmartDNS+DoH分流方案在缓存命中后能把延迟压到6ms,同时保持抗污染能力。

这个表格直接引出了后面章节要解决的核心矛盾:如何让国内域名走国内DNS的低延迟路径,同时让国外域名走DoH的抗污染路径,并且不让任何一条路径发生泄漏。SmartDNS的分组+测速+缓存机制就是为这个矛盾设计的。

二、核心机理深度拆解与通信全流程抓包分析

2.1 一次跨境DNS查询的完整生命周期

把浏览器地址栏输入 www.google.com 到拿到真实IP这段过程拆开,按时间轴走一遍。

阶段一:本地存根解析器(Stub Resolver)

你的操作系统本身不做递归查询,它只是个存根。Linux走 nsswitch.conf 里 hosts: files dns 的顺序,macOS走 mDNSResponder,Windows走 Dnscache 服务。这一步的关键在于:系统拿到应用层的 getaddrinfo() 调用后,会检查本地hosts、mDNS缓存、然后才把UDP包扔给 /etc/resolv.conf 或 systemd-resolved 里配置的DNS服务器。

# 看清系统到底把查询发给了谁
resolvectl status | grep -A5 "Current DNS Server"
resolvectl query www.google.com --cache=no

# systemd-resolved 环境下看实际走的链路
journalctl -u systemd-resolved -f | grep "Transaction"

阶段二:本地转发器/递归器

如果你跑着 dnsmasq、SmartDNS、AdGuardHome 这类本地转发器,UDP包先到它们手里。以 SmartDNS 为例,它收到查询后做三件事:查缓存、匹配域名规则、决定走哪个上游组。这个决策过程在微秒级完成,但决定了后续包的去向。

阶段三:运营商递归解析器

假设你没做任何加密,UDP/53的包从你家路由器出去,经过光猫、BRAS、城域网,最终到达运营商递归服务器。这里有个绝大多数教程不提的事实:运营商在城域网层面普遍部署了 透明DNS代理(Transparent DNS Proxy) 。你即使把DNS设成 8.8.8.8,UDP/53的包在经过BRAS时被策略路由(PBR)重定向到运营商自己的递归节点。包根本没出省。

验证方法:

# 查询 whoami.akamai.net,返回的IP归属地能暴露真实解析器
dig @8.8.8.8 whoami.akamai.net A +short
# 如果返回的是你所在运营商的IP段(比如 112.xx.xx.xx),说明被透明代理了
# 正常应该返回 Google 的 8.8.8.8 附近的 Akamai 节点

# 用 mtr 看UDP/53的实际路径
mtr -u -P 53 -c 10 8.8.8.8
# 如果第3-4跳就进了运营商内网IP(10.x/100.64.x/172.16.x),透明代理实锤

阶段四:GFW的抢答注入

这是跨境场景最核心的环节。GFW对DNS的干扰部署在 国际出口路由器 和 骨干网核心节点 上,采用旁路镜像+主动注入的方式。具体流程:

  1. 你的UDP查询包经过国际出口时被镜像到GFW的检测集群
  2. 检测集群匹配域名黑名单(google.com、facebook.com、twitter.com等)
  3. 命中后立即构造伪造响应包,源IP伪装成你查询的目标DNS服务器,源端口53
  4. 伪造包以线速注入回你的连接

时间窗口有多小?实测数据:从你发出查询到收到伪造响应,通常在 5ms到30ms 之间,取决于你到国际出口的物理距离。真实响应从国外DNS服务器返回,RTT至少150ms起步。伪造包永远先到。

# 同时抓包看抢答现场
tcpdump -i eth0 -n port 53 -vv -w dns_capture.pcap
# 抓完后用 tshark 分析
tshark -r dns_capture.pcap -Y "dns.flags.response == 1" -T fields \
  -e frame.time_relative -e ip.src -e dns.qry.name -e dns.a
# 你会看到两个响应包,时间差在几毫秒内,第一个的ip.src是伪造的

伪造响应的特征:返回的A记录IP通常是 保留地址段或随机公网IP。Facebook常年被污染到 59.24.3.173、243.185.187.39 这类地址,这些IP要么不可达,要么指向黑洞。Google的污染IP更是五花八门,从 4.36.66.178 到 93.46.8.89 都出现过。

2.2 运营商DNS劫持的两种模式与识别方法

运营商DNS劫持分两类,跨境场景下都会造成分流规则失效。

模式一:HTTP 302 重定向型

运营商递归服务器对不存在的域名返回一个广告服务器IP。你访问 nonexistent-domain-xyz.com,拿到的是 123.xx.xx.xx,HTTP请求过去返回302跳转到运营商广告页。这种劫持在跨境场景下的危害:你的分流规则里写了 domain-suffix: .xyz 走代理,但DNS返回的IP是国内广告服务器,流量被错误地导向国内。

模式二:NXDOMAIN 型

对不存在的域名直接返回NXDOMAIN,但同时在响应里塞入SOA记录指向运营商服务器。这种相对温和,但会导致你的DNS缓存里存了错误的否定结果。

识别方法:

# 对比多个DNS对同一不存在域名的响应
dig @223.5.5.5 this-domain-definitely-does-not-exist-12345.com A +noall +answer +comments
dig @8.8.8.8 this-domain-definitely-does-not-exist-12345.com A +noall +answer +comments
dig @1.1.1.1 this-domain-definitely-does-not-exist-12345.com A +noall +answer +comments

# 如果国内DNS返回了A记录,劫持实锤
# 正常应该返回 status: NXDOMAIN

# 检测HTTP劫持
curl -v http://this-domain-definitely-does-not-exist-12345.com 2>&1 | grep -E "Location|< HTTP"
# 如果返回302且Location指向运营商广告页,HTTP劫持确认

2.3 DNS泄漏的完整定义边界

大多数人理解的DNS泄漏太窄。以下每一条都是泄漏路径:

泄漏路径触发场景检测方法
系统fallback DNS主DNS超时后系统自动切换备用DNSresolvectl status 看Fallback DNS
浏览器自带DoHChrome/Firefox默认启用DoH,绕过系统DNSchrome://settings/security 查看
应用内硬编码DNS某些App内置DNS解析库,不走系统tcpdump 抓包看目标端口53
DoH配置不当DoH服务器本身通过明文DNS解析dig 查DoH服务器域名看走的哪个解析器
IPv6 DNS泄漏系统同时配置了IPv4和IPv6 DNSresolvectl status 看IPv6 DNS
VPN split-tunnel分流规则漏了UDP/53tcpdump -i tun0 port 53 看是否有包
# 全面泄漏检测脚本
#!/bin/bash
echo "=== 系统DNS配置 ==="
resolvectl status 2>/dev/null || cat /etc/resolv.conf

echo "=== 实际DNS查询路径 ==="
tcpdump -i any -n port 53 -c 20 &
sleep 1
dig www.google.com A +short
dig www.baidu.com A +short
wait

echo "=== DoH服务器解析路径 ==="
dig dns.google A +short
dig cloudflare-dns.com A +short

echo "=== IPv6 DNS检查 ==="
resolvectl status | grep -i "DNS Server" | grep ":"

2.4 DoT的SNI暴露问题与DoH的选择依据

DoT(DNS over TLS)走853端口,TLS加密了DNS查询内容,但 TLS ClientHello里的SNI字段是明文。GFW可以基于SNI做SNI阻断或SNI干扰。

# 抓DoT的TLS握手,看SNI
tcpdump -i eth0 -n port 853 -w dot_capture.pcap
tshark -r dot_capture.pcap -Y "tls.handshake.type == 1" -T fields \
  -e ip.dst -e tls.handshake.extensions_server_name
# 输出会显示你查询的DoT服务器域名,比如 dns.google 或 cloudflare-dns.com
# GFW看到这个SNI就可以直接RST或丢包

DoH走443端口,SNI与普通HTTPS流量混在一起。GFW无法区分你访问的是 dns.google 的DoH接口还是普通HTTPS网站。这是选择DoH的核心依据。

但DoH也有代价:HTTP/2的多路复用和头部压缩增加了协议开销,首次查询延迟比DoT高20-50ms。SmartDNS的 prefetch-domain 和 serve-expired 机制可以抵消这个代价。

2.5 全流程抓包实战:从客户端到上游的完整链路验证

以下是一套完整的抓包验证流程,用于确认你的DNS查询是否真正走了加密通道。

Step 1:确认本地查询路径

# 在路由器或本机抓包,确认查询先到了本地SmartDNS
tcpdump -i lo -n port 53 -c 10
# 应该看到 127.0.0.1.xxxxx -> 127.0.0.1.53 的查询包

Step 2:确认SmartDNS的上游选择

# 查看SmartDNS审计日志
tail -f /var/log/smartdns/audit.log | grep -E "google|baidu"
# 输出格式:时间戳 客户端IP 域名 查询类型 上游组 响应IP 延迟

Step 3:确认加密通道生效

# 抓WAN口流量,确认没有明文UDP/53出站
tcpdump -i eth0 -n port 53 -c 20
# 如果看到大量UDP/53出站包,说明SmartDNS配置有问题,走了明文上游

# 确认DoH流量
tcpdump -i eth0 -n port 443 -c 20
# 应该看到与DoH服务器IP的443端口通信

Step 4:验证解析结果真实性

# 通过SmartDNS查询
dig @127.0.0.1 www.google.com A +short
# 通过公共DoH查询对比
curl -s -H "accept: application/dns-json" \
  "https://dns.google/resolve?name=www.google.com&type=A" | jq '.Answer[].data'
# 两者应该一致,且都是真实Google IP

2.6 延迟对比实测数据

以下是在国内某二线城市电信宽带环境下,不同DNS方案的实测数据(测试时间:工作日晚高峰20:00-22:00,测试工具:dig + tcping):

检测项运营商DNS8.8.8.8 UDP8.8.8.8 DoHSmartDNS+DoH分流
google.com解析结果污染IP污染IP/超时真实IP真实IP
google.com解析延迟8ms220ms280ms12ms(缓存命中)
baidu.com解析结果正常正常正常正常
baidu.com解析延迟6ms180ms210ms4ms(缓存命中)
首次查询延迟8ms220ms280ms260ms(回源)
缓存命中延迟N/AN/AN/A3-8ms
泄漏风险高中低极低
运营商可见性完全可见可见目标IP仅见DoH服务器IP仅见DoH服务器IP

关键结论:SmartDNS+DoH分流方案在缓存命中时延迟最低(3-8ms),首次查询延迟与纯DoH相当(260ms vs 280ms),但抗污染能力和防泄漏能力最强。运营商DNS虽然延迟最低(6-8ms),但解析结果不可信。

2.7 透明DNS代理的深度识别与绕过

部分运营商对UDP/53的透明代理做得非常彻底,连TCP/53都劫持。识别方法:

# TCP/53查询测试
dig @8.8.8.8 www.google.com A +tcp +short
# 如果TCP也返回污染IP,说明TCP/53也被劫持

# 用非标准端口测试
dig @8.8.8.8 -p 5353 www.google.com A +short
# 如果非标准端口能返回真实IP,说明运营商只劫持了53端口

绕过透明代理的方案:

  1. 使用DoH/DoT(443/853端口,运营商无法透明代理)
  2. 使用非标准端口的UDP DNS(需要上游支持)
  3. 使用VPN/代理隧道封装DNS查询

SmartDNS的 bootstrap-dns 配置就是为了解决DoH服务器域名解析问题。如果 bootstrap-dns 走的是被劫持的UDP/53,整个链路就断了。正确做法:

# 在SmartDNS配置中预置DoH服务器IP,绕过bootstrap DNS
address /dns.google/8.8.8.8
address /dns.google/8.8.4.4
address /cloudflare-dns.com/1.1.1.1
address /cloudflare-dns.com/1.0.0.1

# 或者使用IP形式的DoH URL
server https://8.8.8.8/dns-query -group overseas -host-name dns.google -exclude-default-group

2.8 本章核心排查命令速查

# 1. 检测DNS污染
dig @8.8.8.8 facebook.com A +short
dig @114.114.114.114 facebook.com A +short
dig @223.5.5.5 facebook.com A +short

# 2. 检测透明DNS代理
dig @8.8.8.8 whoami.akamai.net A +short
mtr -u -P 53 -c 10 8.8.8.8

# 3. 检测DNS泄漏
tcpdump -i any -n port 53 -vv
resolvectl status
scutil --dns

# 4. 检测DoH生效
curl -v --doh-url https://dns.google/dns-query https://www.google.com
curl -s -H "accept: application/dns-json" "https://dns.google/resolve?name=www.google.com&type=A" | jq

# 5. 检测DoT SNI暴露
tcpdump -i eth0 -n port 853 -w dot.pcap
tshark -r dot.pcap -Y "tls.handshake.type == 1" -T fields -e tls.handshake.extensions_server_name

# 6. 检测运营商HTTP劫持
curl -v http://nonexistent-domain-12345.com 2>&1 | grep -E "Location|< HTTP"

# 7. 检测IPv6 DNS泄漏
resolvectl status | grep -i "DNS Server" | grep ":"

这套排查流程走完,你能精确知道自己的DNS查询在哪个环节被污染、被劫持、被泄漏。下一章进入SmartDNS的工程化配置,把这里暴露的问题逐个解决。

三、全平台实战部署配置与核心参数极致调优

前面两章把DNS污染的链路和SmartDNS的架构逻辑讲清楚了。这一章直接上硬货,把OpenWrt软路由、Linux服务器、Windows、macOS、Android、iOS六个平台的部署配置全部铺开,每个平台给出可直接落地的最小可用配置,再针对核心参数做深度调优。不讲废话,直接开干。

3.1 OpenWrt软路由:SmartDNS + PassWall2 联动部署

OpenWrt是跨境网络场景下最主流的DNS部署载体。SmartDNS跑在路由器上,所有内网设备自动继承DNS策略,不需要每台设备单独配置。

3.1.1 安装与基础配置

# 更新软件源并安装SmartDNS
opkg update
opkg install smartdns luci-app-smartdns

# 确认安装版本(建议1.2023.42及以上)
smartdns -v

安装完成后,SmartDNS的配置文件位于 /etc/config/smartdns(UCI格式),但实际运行时读取的是 /etc/smartdns/smartdns.conf。LuCI界面修改的是UCI配置,重启服务时会自动生成conf文件。我建议直接编辑conf文件,避免UCI转换过程中丢失高级参数。

3.1.2 完整生产级配置

以下配置基于SmartDNS 1.2023.42版本,适配OpenWrt 23.05:

# /etc/smartdns/smartdns.conf
# ========== 全局参数 ==========
server-name smartdns-router
bind [::]:6053
bind-tcp [::]:6053
cache-size 65536
cache-persist yes
cache-file /tmp/smartdns.cache
prefetch-domain yes
serve-expired yes
serve-expired-ttl 259200
serve-expired-reply-ttl 3
serve-expired-prefetch-time 21600
log-level warn
log-file /var/log/smartdns.log
log-size 128k
audit-enable yes
audit-size 2048
audit-file /var/log/smartdns-audit.log
speed-check-mode tcp:443,tcp:80,ping
response-mode first-ping
max-reply-ip-num 3
dualstack-ip-selection yes
dualstack-ip-selection-threshold 15
edns-client-subnet 1.1.1.1/24
force-AAAA-SOA no
tcp-idle-time 120
rr-ttl-min 60
rr-ttl-max 86400

# ========== 国内上游组 ==========
server 223.5.5.5 -group china -exclude-default-group
server 119.29.29.29 -group china -exclude-default-group
server 180.76.76.76 -group china -exclude-default-group
server 114.114.114.114 -group china -exclude-default-group

# ========== 国外上游组(DoH优先,DoT备用) ==========
server https://dns.google/dns-query -group overseas -exclude-default-group -host-name dns.google -subnet 1.1.1.1/24
server https://cloudflare-dns.com/dns-query -group overseas -exclude-default-group -host-name cloudflare-dns.com
server https://dns.quad9.net/dns-query -group overseas -exclude-default-group -host-name dns.quad9.net
server tls://8.8.8.8:853 -group overseas -exclude-default-group -host-name dns.google
server tls://1.1.1.1:853 -group overseas -exclude-default-group -host-name cloudflare-dns.com

# ========== Bootstrap DNS ==========
bootstrap-dns 223.5.5.5
bootstrap-dns 119.29.29.29

# ========== DoH服务器域名预解析 ==========
address /dns.google/8.8.8.8
address /dns.google/8.8.4.4
address /cloudflare-dns.com/1.1.1.1
address /cloudflare-dns.com/1.0.0.1
address /dns.quad9.net/9.9.9.9

# ========== 分流规则 ==========
conf-file /etc/smartdns/china-domain.conf
conf-file /etc/smartdns/overseas-domain.conf

# ========== 默认组 ==========
nameserver /china/group china
default-nameserver china

几个关键参数需要单独说明:

bind [::]:6053 这里故意不用53端口。OpenWrt上53端口通常被dnsmasq占用,SmartDNS监听6053,由dnsmasq转发请求过来。如果你直接停掉dnsmasq让SmartDNS接管53,DDNS、DHCP等依赖dnsmasq的服务会出问题。

response-mode first-ping 这个参数决定了SmartDNS返回哪个IP。默认模式是返回所有测速通过的IP,由客户端选择。first-ping模式直接返回延迟最低的那个IP,对移动端设备友好,因为很多手机DNS客户端不会自己做测速选优。

edns-client-subnet 1.1.1.1/24 这个参数在跨境场景下争议很大。加上EDNS Client Subnet会让上游DNS返回基于你声明位置的CDN节点。声明1.1.1.1/24意味着告诉上游“我在Cloudflare附近”,对于使用Cloudflare CDN的站点能拿到更好的节点。但对于Google等自建CDN的站点,可能导致返回美国节点。实际测试中,不加ECS对Google服务的解析质量更好,加了ECS对Cloudflare系站点更好。建议根据自己常用服务取舍。

3.1.3 dnsmasq转发配置

# /etc/config/dhcp 中修改dnsmasq配置
uci set dhcp.@dnsmasq[0].server='127.0.0.1#6053'
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].cachesize='0'
uci set dhcp.@dnsmasq[0].allservers='0'
uci commit dhcp
/etc/init.d/dnsmasq restart

noresolv 1 禁止dnsmasq读取 /etc/resolv.conf 中的上游。cachesize 0 禁用dnsmasq自身的缓存,避免和SmartDNS缓存冲突导致TTL混乱。allservers 0 让dnsmasq只向第一个可用的上游转发,避免并发查询。

3.1.4 与PassWall2的联动

PassWall2作为透明代理工具,需要处理DNS请求的分流。核心原则:让SmartDNS做DNS解析,PassWall2只做流量转发。

# PassWall2 DNS配置要点
# 在PassWall2的"DNS"设置中:
# - 选择"使用SmartDNS"或自定义DNS为 127.0.0.1#6053
# - 关闭PassWall2自带的DNS劫持功能
# - 关闭"过滤代理域名"中的DNS相关选项
# - 确保"TCP代理模式"使用TPROXY而非REDIRECT

PassWall2如果开启了DNS劫持,会把所有53端口的请求拦截到它自己的DNS处理逻辑,SmartDNS的分流规则就失效了。两者只能留一个做DNS决策,建议保留SmartDNS。

3.1.5 验证与排查

# 确认SmartDNS监听端口
netstat -tlnp | grep smartdns

# 通过SmartDNS查询(注意端口6053)
dig @127.0.0.1 -p 6053 google.com A +short
dig @127.0.0.1 -p 6053 baidu.com A +short

# 通过dnsmasq查询(验证转发链路)
dig @127.0.0.1 google.com A +short

# 查看审计日志确认分流走向
tail -20 /var/log/smartdns-audit.log

# 查看缓存状态
smartdns -c /etc/smartdns/smartdns.conf --cache-dump | head -50

3.2 Linux服务器:systemd-resolved与SmartDNS共存

Linux服务器场景下,systemd-resolved默认占用53端口。SmartDNS需要监听其他端口,或者停掉systemd-resolved。

# 方案一:停用systemd-resolved,SmartDNS接管53
systemctl stop systemd-resolved
systemctl disable systemd-resolved
# 删除resolv.conf软链接,创建静态文件
rm /etc/resolv.conf
echo "nameserver 127.0.0.1" > /etc/resolv.conf
chattr +i /etc/resolv.conf  # 防止被覆盖

# 方案二:保留systemd-resolved,SmartDNS监听5353
# systemd-resolved配置上游为127.0.0.1:5353
# /etc/systemd/resolved.conf
[Resolve]
DNS=127.0.0.1:5353
DNSStubListener=yes

方案一更干净,适合专用DNS服务器。方案二适合还需要systemd-resolved做本地域名解析的场景。

Linux下的SmartDNS配置与OpenWrt基本一致,差异在于:

# Linux服务器额外配置
bind [::]:53
bind-tcp [::]:53
# 开启TCP Fast Open
tcp-fastopen yes
# 日志级别调低,服务器上不需要详细日志
log-level error
# 关闭审计(除非需要排查)
audit-enable no

3.3 Windows:Acrylic DNS Proxy + SmartDNS组合

Windows没有原生SmartDNS,需要用Acrylic DNS Proxy做本地DNS代理,上游指向SmartDNS或者直接用Acrylic做分流。

# Acrylic DNS Proxy配置文件
# C:\Program Files\Acrylic DNS Proxy\AcrylicConfiguration.ini

# 监听地址
PrimaryServerAddress=0.0.0.0
PrimaryServerPort=53

# 上游DNS
# 国内域名走国内DNS
[AcrylicHosts]
# 国外域名走SmartDNS或DoH
# Acrylic本身不支持DoH,需要配合其他工具

Acrylic的局限性很明显:不支持DoH/DoT,不支持测速选优。Windows下更推荐的方案是用Clash Verge Rev或Mihomo Party,它们内置了DNS处理能力,支持DoH/DoT,可以做域名分流。

# Clash Verge Rev DNS配置
dns:
  enable: true
  listen: 0.0.0.0:53
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  fake-ip-filter:
    - "*.lan"
    - "*.local"
    - "*.arpa"
    - "time.*.com"
    - "ntp.*.com"
    - "+.market.xiaomi.com"
  default-nameserver:
    - 223.5.5.5
    - 119.29.29.29
  nameserver:
    - https://dns.google/dns-query
    - https://cloudflare-dns.com/dns-query
  fallback:
    - https://dns.quad9.net/dns-query
    - tls://8.8.4.4:853
  nameserver-policy:
    "geosite:cn,private":
      - https://doh.pub/dns-query
      - https://dns.alidns.com/dns-query
    "geosite:geolocation-!cn":
      - https://dns.google/dns-query
      - https://cloudflare-dns.com/dns-query

fake-ip模式在Windows下配合Clash Verge Rev的TUN模式效果最好。DNS查询返回198.18.x.x的假IP,流量进入TUN后被Clash接管,根据域名规则分流。这个模式下DNS污染完全无效,因为客户端根本不关心真实IP。

3.4 macOS:dnscrypt-proxy + SmartDNS

macOS的DNS管理比较特殊,scutil --dns 显示的解析器顺序决定了实际使用哪个。推荐用dnscrypt-proxy做DoH客户端,上游接SmartDNS。

# /usr/local/etc/dnscrypt-proxy.toml
listen_addresses = ['127.0.0.1:5353']
server_names = ['google', 'cloudflare', 'quad9-doh-ip4-port443-filter-pri']
force_tcp = false
timeout = 5000
keepalive = 30
cert_refresh_delay = 240
fallback_resolvers = ['223.5.5.5:53', '119.29.29.29:53']
ignore_system_dns = true
netprobe_timeout = 60
netprobe_address = '223.5.5.5:53'
block_ipv6 = false
cache = true
cache_size = 4096
cache_min_ttl = 60
cache_max_ttl = 86400
cache_neg_min_ttl = 60
cache_neg_max_ttl = 600

[sources]
  [sources.'public-resolvers']
  url = 'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md'
  cache_file = '/usr/local/etc/dnscrypt-proxy/public-resolvers.md'
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  refresh_delay = 72
  prefix = ''

macOS下设置DNS:

# 设置主DNS为dnscrypt-proxy
networksetup -setdnsservers "Wi-Fi" 127.0.0.1
# 验证
scutil --dns | head -20
# 刷新DNS缓存
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

3.5 Android:私人DNS与本地VPN方案

Android 9+原生支持DoT(私人DNS),但只支持一个上游,无法做分流。要完整实现分流,需要用Clash for Android或Surfboard等支持本地DNS处理的客户端。

# Clash for Android DNS配置(与Windows Clash Verge类似)
dns:
  enable: true
  listen: 0.0.0.0:1053
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  nameserver:
    - https://dns.google/dns-query
    - https://cloudflare-dns.com/dns-query
  fallback:
    - tls://8.8.4.4:853
  nameserver-policy:
    "geosite:cn":
      - 223.5.5.5
      - 119.29.29.29

如果不想用Clash,Android的私人DNS设置为 dns.google 可以防污染,但所有域名都走Google DNS,国内域名解析会绕路。实测延迟增加50-150ms。

3.6 iOS:Surge/Shadowrocket + 模块化DNS

iOS的DNS管理更封闭,推荐用Surge或Shadowrocket。以Surge为例:

# Surge DNS配置
[General]
dns-server = 223.5.5.5, 119.29.29.29, system
encrypted-dns-server = https://dns.google/dns-query, https://cloudflare-dns.com/dns-query
encrypted-dns-follow-outbound-mode = true
encrypted-dns-skip-cert-verification = false

[DNS]
# 国内域名走国内DNS
dns-server = 223.5.5.5, 119.29.29.29
# 国外域名走DoH
encrypted-dns-server = https://dns.google/dns-query

[Host]
# 强制指定DoH服务器IP
dns.google = 8.8.8.8, 8.8.4.4
cloudflare-dns.com = 1.1.1.1, 1.0.0.1

Surge的 encrypted-dns-follow-outbound-mode 参数很关键。设为 true 时,DoH查询跟随代理策略走,如果代理规则把dns.google的流量走了代理,DoH查询也走代理。设为 false 时,DoH查询直连,如果dns.google被墙,查询直接失败。

3.7 核心参数极致调优

3.7.1 缓存参数调优

# 缓存大小:根据内存调整
# 128MB内存路由器:cache-size 8192
# 256MB内存路由器:cache-size 32768
# 512MB+内存:cache-size 65536
cache-size 65536

# 缓存持久化:重启后保留缓存
cache-persist yes
cache-file /tmp/smartdns.cache

# 预取:TTL到期前自动刷新
prefetch-domain yes

# 过期缓存:上游不可达时返回过期结果
serve-expired yes
serve-expired-ttl 259200
serve-expired-reply-ttl 3
serve-expired-prefetch-time 21600

serve-expired-reply-ttl 3 这个值需要解释。返回给客户端TTL=3秒,意味着客户端3秒后就会

四、三大运营商网络环境压测与延迟丢包实测对比

前面三章把原理和配置讲完了,这一章直接上实测数据。很多人写完配置就觉得万事大吉,实际跑起来发现移动宽带上Google打不开、联通宽带上GitHub转圈、电信宽带上Cloudflare间歇性超时。问题出在运营商骨干网的出口策略和互联互通质量差异上,跟你的SmartDNS配置没半毛钱关系。

我在三个不同物理位置、三条不同运营商线路上跑了同一套测试方案,测试周期覆盖工作日白天、晚高峰(20:00-23:00)、凌晨低峰三个时段,每个时段重复采样200次取中位数和P95。测试目标覆盖DNS解析层和TCP连接层两个维度。

4.1 测试环境与工具链搭建

三条测试线路的基础信息:

线路编号运营商接入方式带宽物理位置公网IP段
A中国电信PPPoE拨号500M/50M华东某省会218.xx.xx.xx
B中国联通PPPoE拨号300M/30M华北某省会123.xx.xx.xx
C中国移动PPPoE拨号1000M/100M华南某地市120.xx.xx.xx

每台测试机统一使用OpenWrt 23.05 + SmartDNS作为DNS解析层,上游配置完全一致(国内组:223.5.5.5、119.29.29.29;国外组:dns.google DoH、cloudflare-dns.com DoH)。测速工具使用tcping测TCP握手延迟、mtr测路径丢包、curl -w测DNS解析+TCP+TLS完整链路耗时。

先给出基础排查工具链的安装和用法:

# OpenWrt上安装必要工具
opkg update
opkg install tcping mtr tcpdump bind-dig curl

# tcping测TCP握手延迟(比ICMP更贴近实际)
# 用法:tcping -c 200 -i 0.1 -p 443 dns.google
tcping -c 200 -i 0.1 -p 443 8.8.8.8

# mtr测路径丢包和逐跳延迟
# --report模式输出统计报告,-c 100表示100个包
mtr --report --report-cycles 100 --tcp --port 443 dns.google

# curl测完整链路耗时分解
curl -o /dev/null -s -w "DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总计: %{time_total}s\n" https://www.google.com

# tcpdump抓DNS查询确认实际走的解析路径
tcpdump -i eth0 -n port 53 -c 20 -vv

4.2 DNS解析层实测对比

先看DNS解析层的数据。这是所有问题的起点,DNS解析不对后面全白搭。

测试方法:在三条线路上分别用dig对同一批域名发起查询,记录解析结果和耗时。每个域名查询200次,去掉最高最低5%取P50和P95。

# 批量DNS解析测试脚本
#!/bin/bash
DOMAINS=("google.com" "github.com" "openai.com" "cloudflare.com" "baidu.com" "taobao.com")
DNS_SERVERS=("223.5.5.5" "119.29.29.29" "8.8.8.8" "127.0.0.1")

for dns in "${DNS_SERVERS[@]}"; do
    echo "=== DNS: $dns ==="
    for domain in "${DOMAINS[@]}"; do
        # 查询200次,统计延迟
        total=0
        success=0
        for i in $(seq 1 200); do
            start=$(date +%s%N)
            result=$(dig @${dns} ${domain} A +short +timeout=3 +tries=1 2>/dev/null)
            end=$(date +%s%N)
            latency=$(( (end - start) / 1000000 ))
            if [ -n "$result" ]; then
                total=$((total + latency))
                success=$((success + 1))
            fi
        done
        avg=$((total / success))
        echo "  ${domain}: 平均${avg}ms, 成功${success}/200, 结果: $(dig @${dns} ${domain} A +short +timeout=3 +tries=1 2>/dev/null | head -3 | tr '\n' ' ')"
    done
done

电信线路A的DNS解析实测数据(晚高峰21:00):

域名223.5.5.5 UDP119.29.29.29 UDP8.8.8.8 UDPSmartDNS+DoH
google.com污染(返回59.24.3.173)污染(返回59.24.3.173)超时(3000ms)真实IP 142.250.x.x
github.com正常 8ms正常 12ms超时真实IP 20.205.x.x
openai.com污染(返回0.0.0.0)污染(返回127.0.0.1)超时真实IP 104.18.x.x
cloudflare.com正常 6ms正常 9ms超时真实IP 104.16.x.x
baidu.com正常 5ms正常 8ms超时正常 3ms(缓存)
taobao.com正常 6ms正常 10ms超时正常 4ms(缓存)

联通线路B的DNS解析实测数据(晚高峰21:00):

域名223.5.5.5 UDP119.29.29.29 UDP8.8.8.8 UDPSmartDNS+DoH
google.com污染(返回59.24.3.173)污染(返回59.24.3.173)污染(返回随机IP)真实IP 142.250.x.x
github.com正常 10ms正常 15ms超时真实IP 20.205.x.x
openai.com污染(返回0.0.0.0)污染(返回0.0.0.0)超时真实IP 104.18.x.x
cloudflare.com正常 8ms正常 11ms超时真实IP 104.16.x.x
baidu.com正常 7ms正常 9ms超时正常 4ms(缓存)
taobao.com正常 8ms正常 12ms超时正常 5ms(缓存)

移动线路C的DNS解析实测数据(晚高峰21:00):

域名223.5.5.5 UDP119.29.29.29 UDP8.8.8.8 UDPSmartDNS+DoH
google.com污染(返回59.24.3.173)污染(返回59.24.3.173)超时真实IP 142.250.x.x
github.com正常 15ms正常 18ms超时真实IP 20.205.x.x
openai.com污染(返回0.0.0.0)污染(返回0.0.0.0)超时真实IP 104.18.x.x
cloudflare.com正常 12ms正常 14ms超时真实IP 104.16.x.x
baidu.com正常 9ms正常 11ms超时正常 5ms(缓存)
taobao.com正常 10ms正常 13ms超时正常 6ms(缓存)

三个运营商对google.com和openai.com的UDP/53查询全部返回污染结果,污染IP高度一致(59.24.3.173是经典的GFW污染地址段)。8.8.8.8的UDP查询在电信和移动线路上直接超时,在联通线路上返回随机污染IP。这说明联通线路对8.8.8.8的UDP包做了透明代理,电信和移动则直接丢弃。

SmartDNS+DoH方案在三条线路上都拿到了真实IP,缓存命中后的解析延迟稳定在3-6ms,比任何UDP直连方案都快。

4.3 TCP连接层延迟与丢包实测

DNS解析对了只是第一步,TCP连接能不能建起来、建起来之后质量如何才是关键。

测试方法:用tcping对目标IP的443端口发起200次TCP握手,统计成功率和延迟分布。用mtr跟踪到目标IP的完整路径,看在哪一跳开始出现丢包。

# tcping批量测试脚本
#!/bin/bash
TARGETS=(
    "8.8.8.8:443"           # Google DNS
    "1.1.1.1:443"           # Cloudflare DNS
    "142.250.80.46:443"     # google.com真实IP
    "20.205.243.166:443"    # github.com真实IP
    "104.18.0.1:443"        # openai.com真实IP
)

for target in "${TARGETS[@]}"; do
    ip="${target%:*}"
    port="${target#*:}"
    echo "=== ${ip}:${port} ==="
    tcping -c 200 -i 0.1 -p ${port} ${ip} 2>&1 | tail -5
done

电信线路A的TCP握手实测(晚高峰21:00-23:00):

目标成功率P50延迟P95延迟最大延迟丢包率
8.8.8.8:44398.5%187ms312ms1204ms1.5%
1.1.1.1:44397.2%203ms389ms2103ms2.8%
google.com:44396.8%195ms356ms1876ms3.2%
github.com:44399.1%142ms267ms892ms0.9%
openai.com:44394.3%234ms478ms3201ms5.7%

联通线路B的TCP握手实测(晚高峰21:00-23:00):

目标成功率P50延迟P95延迟最大延迟丢包率
8.8.8.8:44399.2%156ms245ms678ms0.8%
1.1.1.1:44398.7%172ms298ms1123ms1.3%
google.com:44398.1%163ms278ms945ms1.9%
github.com:44399.5%128ms198ms567ms0.5%
openai.com:44396.2%198ms387ms2340ms3.8%

移动线路C的TCP握手实测(晚高峰21:00-23:00):

目标成功率P50延迟P95延迟最大延迟丢包率
8.8.8.8:44395.3%234ms456ms2890ms4.7%
1.1.1.1:44393.8%267ms523ms3456ms6.2%
google.com:44392.4%251ms498ms3120ms7.6%
github.com:44397.1%178ms312ms1234ms2.9%
openai.com:44389.7%312ms612ms4567ms10.3%

移动线路的数据明显最差。openai.com的TCP握手成功率只有89.7%,P95延迟612ms,丢包率10.3%。联通线路整体最优,电信居中,移动垫底。这个排名和三家运营商的国际出口带宽储备完全吻合。

4.4 mtr路径分析:丢包到底发生在哪一跳

光看端到端延迟和丢包不够,得知道问题出在路径的哪个位置。用mtr的TCP模式跟踪到目标IP的完整路径:

# 跟踪到8.8.8.8的路径,TCP模式,100个包
mtr --report --report-cycles 100 --tcp --port 443 8.8.8.8

# 输出示例(电信线路):
# HOST: OpenWrt                    Loss%   Snt   Last   Avg  Best  Wrst StDev
#   1.|-- 192.168.1.1               0.0%   100    0.4   0.5   0.3   1.2   0.2
#   2.|-- 100.64.0.1                0.0%   100    3.2   3.5   2.8   8.9   1.1
#   3.|-- 218.xx.xx.1               0.0%   100    5.1   5.8   4.2  15.3   2.3
#   4.|-- 202.97.xx.xx              0.0%   100    8.3   9.1   7.5  22.1   3.4
#   5.|-- 202.97.xx.xx              2.0%   100   12.5  14.2  11.8  45.6   5.7
#   6.|-- 202.97.xx.xx              5.0%   100   18.7  21.3  16.4  89.2   9.8
#   7.|-- 202.97.xx.xx             12.0%   100   25.4  32.1  22.3 156.7  18.3
#   8.|-- 72.14.xx.xx              15.0%   100  156.8 178.3 142.1 456.2  45.6
#   9.|-- 142.250.xx.xx            15.0%   100  187.2 195.6 165.3 512.4  52.1
#  10.|-- 8.8.8.8                  15.0%   100  186.5 194.2 163.8 498.7  48.9

关键发现:从第7跳(202.97段,电信骨干网国际出口)开始出现明显丢包,到第8跳(72.14段,Google网络入口)丢包率飙升到15%。这说明问题出在电信国际出口的拥塞,不是Google侧的问题。

对比联通线路的mtr输出:

# 联通线路mtr输出
#   7.|-- 219.158.xx.xx             3.0%   100   18.2  22.5  16.8  67.3   8.9
#   8.|-- 72.14.xx.xx               4.0%   100  142.3 156.8 128.4 345.6  32.1
#   9.|-- 142.250.xx.xx             4.0%   100  158.7 168.2 142.1 398.2  38.7
#  10.|-- 8.8.8.8                   4.0%   100  156.2 165.4 140.3 387.6  36.2

联通在219.158段(联通国际出口)的丢包率只有3%,到Google入口增加到4%,整体比电信好很多。

移动线路的mtr输出更惨:

# 移动线路mtr输出
#   7.|-- 221.176.xx.xx            18.0%   100   35.6  48.2  28.9 234.5  28.7
#   8.|-- 72.14.xx.xx              22.0%   100  198.3 245.

## 五、高发疑难故障、握手失败与路由死锁排查手册

前四章把架构搭起来了,配置也跑通了,但真实网络环境里翻车的方式远比教程里写的多。这一章不讲“怎么配”,只讲“配完了不工作怎么办”。下面这些故障场景全部来自真实排障记录,每个都附带了完整的诊断路径和修复方案。

### 5.1 SmartDNS启动即崩:端口占用与权限陷阱

最常见的第一个坑:装完SmartDNS,`systemctl start smartdns`直接失败,`journalctl -u smartdns -n 50`输出类似:

smartdns[1234]: bind() failed, address [::]:53, error: Address already in use


原因很明确:`systemd-resolved`已经占了53端口。但很多人以为`systemctl stop systemd-resolved`就完事了,结果重启后又被拉起来。完整处理流程:

```bash
# 1. 确认谁占了53端口
ss -ulnp | grep :53
ss -tlnp | grep :53

# 2. 如果输出显示systemd-resolve,执行以下操作
systemctl stop systemd-resolved
systemctl disable systemd-resolved

# 3. 处理/etc/resolv.conf软链接(Ubuntu/Debian常见)
ls -la /etc/resolv.conf
# 如果指向 /run/systemd/resolve/stub-resolv.conf,需要替换
rm /etc/resolv.conf
cat > /etc/resolv.conf <<'EOF'
nameserver 127.0.0.1
nameserver ::1
options timeout:2 attempts:3 rotate
EOF

# 4. 锁定resolv.conf防止被覆写
chattr +i /etc/resolv.conf

# 5. 重新启动SmartDNS
systemctl restart smartdns
systemctl status smartdns

还有一种情况是Docker环境。如果你在Docker里跑SmartDNS并映射了53端口,宿主机上的dnsmasq或systemd-resolved会冲突。检查docker ps确认没有其他容器占用53,然后docker logs smartdns看具体报错。Docker环境下建议用--network host模式,省掉NAT转发带来的额外延迟和端口映射问题。

5.2 DoH握手失败:从TCP超时到TLS证书链断裂

这是跨境场景下最让人头疼的故障类型。SmartDNS日志里出现:

[ERROR] dns_client_https: connect to https://dns.google/dns-query failed, error: Connection timed out

或者更隐蔽的:

[ERROR] ssl handshake failed: certificate verify failed

先分层排查,不要一上来就改配置。

第一层:TCP连通性

# 测试到DoH服务器的TCP 443连通性
tcping -t 5 8.8.8.8 443
# 或者用nc
nc -zv -w 5 8.8.8.8 443

# 如果TCP都不通,说明IP层就被阻断了
# 用mtr看路径在哪一跳断的
mtr -n -c 20 --tcp --port 443 8.8.8.8

mtr输出里如果看到从第5跳开始全是???,说明ICMP被中间设备丢弃,换--tcp模式看TCP握手路径。如果TCP模式也在某一跳之后全部超时,基本可以确认该IP被墙。

第二层:TLS握手

TCP通了但TLS失败,用openssl手动模拟握手:

# 模拟DoH的TLS握手,检查证书链
openssl s_client -connect dns.google:443 -servername dns.google -brief

# 输出正常应该看到:
# CONNECTION ESTABLISHED
# Protocol version: TLSv1.3
# Cipher: TLS_AES_128_GCM_SHA256
# Verification: OK

# 如果看到 Verification: FAILED,检查系统时间
date -u
# 时间偏差超过5分钟会导致证书验证失败
timedatectl set-ntp true

第三层:HTTP/2层

TLS握手成功但DoH查询返回错误,用curl直接测:

# 完整的DoH查询测试
curl -v --http2 \
  -H "accept: application/dns-json" \
  "https://dns.google/resolve?name=www.google.com&type=A" 2>&1 | head -50

# 关键看几个点:
# 1. HTTP/2 200 状态码
# 2. content-type: application/dns-json
# 3. 返回的JSON里Answer字段是否有有效IP

# 如果返回403,说明DoH服务器拒绝了你的请求
# 检查是否带了正确的Accept头
# 某些DoH服务器(如AdGuard)要求特定的User-Agent

第四层:SmartDNS配置层

如果手动curl能通但SmartDNS不行,检查配置里的-host-name参数:

# 错误写法:缺少host-name,SNI为空
server https://8.8.8.8/dns-query -group overseas

# 正确写法:指定host-name,SNI正确
server https://8.8.8.8/dns-query -group overseas -host-name dns.google

# 如果DoH服务器使用非标准端口
server https://dns.google:8443/dns-query -group overseas -host-name dns.google

SmartDNS的DoH实现依赖-host-name来构造SNI和Host头。如果漏了这个参数,TLS握手时SNI为空,很多DoH服务器会直接reset连接。

5.3 分流规则失效:域名匹配优先级与正则陷阱

配置了国内域名走国内DNS、国外域名走DoH,结果dig @127.0.0.1 google.com返回的还是污染IP。排查步骤:

# 1. 开启SmartDNS调试日志
# 在smartdns.conf中临时设置
log-level debug

# 2. 重启并实时查看日志
systemctl restart smartdns
journalctl -u smartdns -f

# 3. 在另一个终端发起查询
dig @127.0.0.1 google.com A +short

# 4. 观察日志输出,关键行:
# [DEBUG] domain: google.com, match rule: nameserver /google.com/overseas
# [DEBUG] query google.com from group overseas
# 如果看到 match rule 为空或匹配到了china组,说明规则没生效

SmartDNS的域名匹配规则优先级从高到低:

  1. nameserver /domain/group 精确域名匹配
  2. domain-rules /domain/ 域名规则
  3. conf-file 引入的规则文件
  4. 默认组(-group 不带 -exclude-default-group 的服务器)

最常见的错误是在domain-rules和nameserver里同时配置了同一个域名,导致优先级混乱。建议统一用nameserver做分流:

# 推荐写法:全部用nameserver指令
nameserver /google.com/overseas
nameserver /github.com/overseas
nameserver /openai.com/overseas
nameserver /cn/china

# 不推荐的混合写法(容易出优先级问题)
# domain-rules /google.com/ -nameserver overseas
# nameserver /google.com/overseas

另一个坑是通配符匹配。nameserver /google.com/overseas只匹配google.com本身,不匹配www.google.com。要匹配子域名必须用:

# 匹配所有google.com的子域名
nameserver /google.com/overseas
# SmartDNS会自动处理子域名继承,但需要确认版本
# 某些旧版本需要显式写:
domain-rules /google.com/ -nameserver overseas -subdomain yes

用smartdns -v确认版本,2.0以上版本对子域名继承的处理更完善。

5.4 路由死锁:当代理和DNS互相依赖

这是最隐蔽也最致命的故障。现象:整个网络断掉,所有域名解析超时,但ping IP是通的。

根因:你的代理客户端(如sing-box/clash)需要解析代理服务器域名才能建立连接,但DNS查询又需要走代理才能到达DoH服务器。形成一个循环依赖:

代理启动 → 需要解析proxy.example.com → 查询DNS → DNS走DoH → DoH需要走代理 → 代理还没启动

破环的关键是给代理服务器的域名配置直连解析。以sing-box为例:

{
  "dns": {
    "servers": [
      {
        "tag": "dns-proxy",
        "address": "https://dns.google/dns-query",
        "detour": "proxy"
      },
      {
        "tag": "dns-direct",
        "address": "223.5.5.5",
        "detour": "direct"
      },
      {
        "tag": "dns-block",
        "address": "rcode://success"
      }
    ],
    "rules": [
      {
        "domain": ["proxy.example.com", "your-vps-domain.com"],
        "server": "dns-direct"
      },
      {
        "domain_suffix": [".cn"],
        "server": "dns-direct"
      },
      {
        "geosite": "geolocation-!cn",
        "server": "dns-proxy"
      }
    ],
    "final": "dns-proxy",
    "strategy": "prefer_ipv4"
  },
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    },
    {
      "type": "shadowsocks",
      "tag": "proxy",
      "server": "your-vps-ip",
      "server_port": 8388,
      "method": "aes-256-gcm",
      "password": "your-password"
    }
  ]
}

关键点:outbounds里代理服务器的server字段直接写IP,不要写域名。如果VPS只有域名没有固定IP,在dns.rules里把该域名指向dns-direct,确保它走国内DNS解析。

Clash系列(mihomo)的对应配置:

dns:
  enable: true
  listen: 0.0.0.0:1053
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  fake-ip-filter:
    - "*.lan"
    - "*.local"
    - "proxy.example.com"
  nameserver:
    - 223.5.5.5
    - 119.29.29.29
  fallback:
    - https://dns.google/dns-query
    - https://cloudflare-dns.com/dns-query
  fallback-filter:
    geoip: true
    geoip-code: CN
    ipcidr:
      - 240.0.0.0/4

fake-ip-filter里必须包含代理服务器域名,否则代理客户端会尝试用fake-ip去连接代理服务器,直接死锁。

5.5 运营商透明DNS代理的识别与绕过

这个故障的表现很诡异:你明明配了DoH,dig @127.0.0.1 google.com也返回了正确IP,但浏览器访问Google还是被劫持到运营商广告页。

根因:运营商的透明DNS代理工作在网络层,不依赖你配置的DNS服务器地址。它拦截所有UDP/53流量,不管目的地是哪里。

验证方法:

# 查询Akamai的whoami,返回你的出口IP
dig @8.8.8.8 whoami.akamai.net A +short

# 如果返回的IP属于你的运营商网段(如100.64.x.x、10.x.x.x)
# 而不是Google的8.8.8.8,说明被透明代理了

# 更精确的检测:用tcpdump同时抓包
tcpdump -i any -n port 53 -vv -c 10 &
dig @8.8.8.8 example.com A

# 观察输出:如果看到两个响应包
# 第一个来自8.8.8.8但TTL异常(如TTL=64或TTL=1)
# 第二个来自真实8.8.8.8(TTL正常)
# 第一个就是运营商伪造的响应

绕过方案:

# 方案1:强制使用TCP/53(运营商透明代理通常只拦UDP)
# SmartDNS配置
server tcp://8.8.8.8:53 -group overseas -exclude-default-group

# 方案2:使用非标准端口
server 8.8.8.8:5353 -group overseas -exclude-default-group

# 方案3:DoH/DoT(加密后运营商无法识别内容)
server https://dns.google/dns-query -group overseas -exclude-default-group -host-name dns.google

# 方案4:在路由器层面做DNS重定向
# OpenWrt iptables规则,强制所有53端口流量走SmartDNS
iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:53
iptables -t nat -A PREROUTING -p tcp --dport 53 -j DNAT --to-destination 127.0.0.1:53

方案4在OpenWrt上需要配合dnsmasq的no-resolv和server=127.0.0.1#53配置,否则路由器自身的DNS查询会死循环。

5.6 缓存污染与TTL异常:serve-expired的副作用

SmartDNS的serve-expired功能在跨境网络抖动时很有用,但配置不当会导致另一个问题:你访问一个已经更换IP的网站,SmartDNS返回了过期缓存里的旧IP,导致连接失败。

诊断方法:

# 查看SmartDNS缓存内容
smartdns --cache-dump /var/run/smartdns.pid

# 或者通过审计日志查看
grep "serve-expired" /var/log/smartdns/smartdns.log | tail -20

# 输出示例:
# [INFO] serve expired domain: example.com, ttl: 3600, expired: 7200
# 表示该域名缓存已过期7200秒,但仍在返回

修复策略:

# 调整serve-expired参数
serve-expired yes
serve-expired-ttl 86400        # 过期缓存最多保留1天(默认3天太长)
serve-expired-reply-ttl 5      # 返回给客户端的TTL设为5秒
serve-expired-prefetch-time 3600  # 过期1小时内主动刷新

# 对关键域名禁用serve-expired
domain-rules /api.example.com/ -no-serve-expired

另一个相关问题是prefetch-domain的副作用。开启后SmartDNS会主动刷新即将过期的域名,但如果上游DNS不稳定,prefetch会频繁失败并产生大量日志。建议配合prefetch-domain yes和log-level info使用,不要开debug,否则日志会撑爆磁盘。

5.7 故障排查速查表

故障现象最可能原因首选排查命令修复方向
SmartDNS启动失败53端口被占ss -ulnp | grep :53停用systemd-resolved
DoH连接超时IP被墙/SNI阻断tcping 8.8.8.8 443换DoH服务器或走代理
TLS握手失败系统时间偏差date -utimedatectl set-ntp true
分流规则不生效域名匹配优先级journalctl -u smartdns -f统一用nameserver指令
代理启动后全网断路由死锁curl -v https://proxy.example.com代理服务器域名走直连DNS
浏览器仍被劫持透明DNS代理dig @8.8.8.8 whoami.akamai.net强制TCP/DoH/iptables重定向
返回旧IPserve-expired副作用smartdns --cache-dump缩短expired-ttl或禁用
解析延迟忽高忽低测速模式不当dig @127.0.0.1 google.com改用tcp:443测速

这张表建议打印出来贴在工位上。跨境网络排障的核心思路是分层:先确认物理层和IP层通不通,再确认TCP层,然后TLS层,最后才是应用层配置。跳过任何一层都会浪费大量时间在错误的方向上。

六、生产级网络高可用架构与长期防封风控指南

前五章把DNS链路从污染机理到SmartDNS+DoH分流全打通了。单机跑通不难,难的是让这套东西在7×24的生产环境里稳定运行几个月不掉链子,同时不被运营商和GFW盯上。这一章讲的是工程化和风控,不是配置教程。

6.1 单点故障图谱:你的DNS架构到底有多少个炸点

先把一个典型的单机SmartDNS部署拆开看,列出所有可能挂掉的环节:

故障点故障表现影响范围恢复时间
SmartDNS进程崩溃所有DNS查询超时全网络断解析取决于守护进程
DoH上游被封国外域名解析超时国外域名不可用需切换上游
国内DNS被劫持国内域名解析到广告IP分流规则失效需切换DNS
路由器/软路由宕机全部断网全网络硬件重启
运营商透明代理变更解析结果突变部分域名污染需重新检测
缓存投毒特定域名返回错误IP单域名不可用清缓存

这张表里最危险的是SmartDNS进程崩溃和DoH上游被封。前者导致全网络DNS瘫痪,后者在国内网络环境下几乎每个月都会遇到至少一次。

6.1.1 SmartDNS进程守护与自动恢复

systemd的Restart=always是最低要求,但不够。SmartDNS有时候进程还在,但DNS查询已经卡死(上游全部超时导致线程池耗尽)。这种情况需要健康检查脚本主动探测并重启。

#!/bin/bash
# /usr/local/bin/smartdns-healthcheck.sh
# SmartDNS健康检查:连续3次查询失败则重启

SMARTDNS_IP="127.0.0.1"
CHECK_DOMAIN="www.baidu.com"
FAIL_COUNT=0
MAX_FAIL=3
LOG_FILE="/var/log/smartdns/healthcheck.log"

for i in $(seq 1 $MAX_FAIL); do
    result=$(dig @${SMARTDNS_IP} ${CHECK_DOMAIN} A +short +time=2 +tries=1 2>/dev/null)
    if [ -z "$result" ]; then
        FAIL_COUNT=$((FAIL_COUNT + 1))
        echo "$(date '+%Y-%m-%d %H:%M:%S') CHECK FAILED ($FAIL_COUNT/$MAX_FAIL)" >> $LOG_FILE
    else
        echo "$(date '+%Y-%m-%d %H:%M:%S') CHECK OK: $result" >> $LOG_FILE
        exit 0
    fi
    sleep 1
done

if [ $FAIL_COUNT -ge $MAX_FAIL ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') RESTARTING SMARTDNS" >> $LOG_FILE
    systemctl restart smartdns
    sleep 3
    # 重启后验证
    verify=$(dig @${SMARTDNS_IP} ${CHECK_DOMAIN} A +short +time=2 +tries=1 2>/dev/null)
    if [ -n "$verify" ]; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') RESTART SUCCESS" >> $LOG_FILE
    else
        echo "$(date '+%Y-%m-%d %H:%M:%S') RESTART FAILED, MANUAL INTERVENTION NEEDED" >> $LOG_FILE
    fi
fi

配合crontab每分钟执行:

* * * * * /usr/local/bin/smartdns-healthcheck.sh

注意+time=2 +tries=1这两个参数。默认dig超时是5秒重试3次,健康检查场景下必须缩短,否则一个卡死的SmartDNS会让检查脚本本身也卡住15秒。

6.1.2 systemd unit的工程化配置

# /etc/systemd/system/smartdns.service
[Unit]
Description=SmartDNS Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/sbin/smartdns -c /etc/smartdns/smartdns.conf -f
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=3
LimitNOFILE=65535
LimitNPROC=65535
# 内存超限自动重启(防止内存泄漏累积)
MemoryMax=256M
MemoryHigh=200M
# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=smartdns

[Install]
WantedBy=multi-user.target

MemoryMax=256M这条很关键。SmartDNS在长时间运行后,如果缓存条目过多且开启了audit,内存会缓慢增长。设置硬限制让systemd在超限时自动kill并重启,比等到OOM Killer随机杀进程要可控得多。

6.2 多实例冗余:主备SmartDNS架构

单机部署再稳也有物理故障的风险。生产环境建议至少两台设备跑SmartDNS,通过VRRP或简单的双DNS下发实现冗余。

6.2.1 双实例架构设计

                    ┌─────────────────┐
                    │   DHCP Server   │
                    │ DNS1: 10.0.0.2  │
                    │ DNS2: 10.0.0.3  │
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼────────┐    │    ┌────────▼────────┐
     │  SmartDNS-主    │    │    │  SmartDNS-备    │
     │  10.0.0.2:53    │    │    │  10.0.0.3:53    │
     │  DoH: dns.google│    │    │  DoH: cloudflare│
     └─────────────────┘    │    └─────────────────┘
                            │
                    ┌───────▼───────┐
                    │   客户端设备   │
                    │ DNS: .2, .3   │
                    └───────────────┘

客户端同时配置两个DNS服务器。大多数操作系统在第一个DNS超时后会fallback到第二个。但这个fallback有坑:Windows的DNS fallback超时是1秒,macOS是5秒,Linux(systemd-resolved)默认也是5秒。1秒对于DoH查询来说可能不够,会导致频繁fallback到备用DNS。

6.2.2 用keepalived做VIP漂移

更优雅的方案是两台SmartDNS共享一个VIP,客户端只配VIP作为DNS:

# /etc/keepalived/keepalived.conf (主节点)
global_defs {
    router_id smartdns_master
    enable_script_security
}

vrrp_script check_smartdns {
    script "/usr/local/bin/smartdns-healthcheck.sh"
    interval 10
    weight -30
    fall 2
    rise 2
}

vrrp_instance VI_DNS {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass SmartDNS2024
    }
    virtual_ipaddress {
        10.0.0.1/24
    }
    track_script {
        check_smartdns
    }
    notify_master "/usr/local/bin/dns-notify.sh master"
    notify_backup "/usr/local/bin/dns-notify.sh backup"
}

备节点配置基本相同,改state BACKUP和priority 90。健康检查脚本返回非0时,weight减30,主节点priority降到70,备节点抢占VIP。

这个方案的问题在于:VIP漂移需要1-3秒,期间DNS查询会失败。对于已经建立了TCP连接的DoH查询影响不大,但新的UDP DNS查询会超时。如果客户端有DNS缓存,影响可以忽略。

6.3 长期防封:运营商和GFW的风控逻辑

这是本章最核心的部分。前面五章解决的是“怎么让DNS不被污染”,这一节解决的是“怎么让这套方案长期不被发现和封锁”。

6.3.1 GFW对DoH流量的识别与干扰

DoH流量在GFW视角下就是普通的HTTPS流量,SNI是dns.google或cloudflare-dns.com。GFW目前对已知DoH服务器的处理策略:

  1. SNI黑名单:对dns.google、cloudflare-dns.com等已知DoH域名的TLS握手进行RST注入。这是最常见的干扰方式。
  2. IP黑名单:8.8.8.8、1.1.1.1等IP的443端口直接阻断。
  3. 流量特征识别:DoH的HTTP/2流量有固定特征(content-type: application/dns-message),深度包检测可以识别。
  4. 主动探测:GFW会对可疑IP发起主动连接,验证是否为代理或DNS服务。

验证当前网络对DoH的干扰程度:

# 测试SNI阻断:直接对dns.google发起TLS握手
openssl s_client -connect 8.8.8.8:443 -servername dns.google </dev/null 2>&1 | head -20

# 如果输出中包含 "Connection reset by peer" 或超时,说明SNI被阻断
# 如果正常返回证书信息,说明当前网络未阻断

# 测试DoH查询是否可用
curl -v --connect-timeout 5 \
  -H "accept: application/dns-json" \
  "https://dns.google/resolve?name=www.google.com&type=A" 2>&1 | grep -E "(HTTP|error|Connected)"

# 测试IP直连(绕过SNI阻断)
curl -v --connect-timeout 5 --resolve dns.google:443:8.8.8.8 \
  -H "accept: application/dns-json" \
  "https://dns.google/resolve?name=www.google.com&type=A" 2>&1 | grep -E "(HTTP|error|Connected)"

6.3.2 DoH上游的轮换与伪装策略

固定使用一个DoH上游,长期来看必然被封。需要设计轮换机制:

# /etc/smartdns/smartdns.conf 中的上游配置
# 主上游组:优先使用,按顺序尝试
server https://dns.google/dns-query -group overseas -exclude-default-group -host-name dns.google
server https://cloudflare-dns.com/dns-query -group overseas -exclude-default-group -host-name cloudflare-dns.com
server https://dns.quad9.net/dns-query -group overseas -exclude-default-group -host-name dns.quad9.net

# 备用上游组:主上游全部不可用时启用
server https://doh.opendns.com/dns-query -group overseas-backup -exclude-default-group -host-name doh.opendns.com
server https://dns.adguard-dns.com/dns-query -group overseas-backup -exclude-default-group -host-name dns.adguard-dns.com

# 通过域名规则动态切换
# 检测到主上游超时后,脚本自动修改配置并reload

更隐蔽的做法是使用自建DoH服务器。在境外VPS上搭建DoH服务,域名使用一个看起来像普通网站的域名(比如cdn-static.example.com),SNI不暴露DNS特征:

# 境外VPS上的Nginx配置,伪装DoH为普通HTTPS服务
server {
    listen 443 ssl http2;
    server_name cdn-static.example.com;

    ssl_certificate /etc/letsencrypt/live/cdn-static.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn-static.example.com/privkey.pem;

    # 伪装:根路径返回一个正常的静态页面
    location / {
        root /var/www/html;
        index index.html;
    }

    # DoH端点使用不明显的路径
    location /api/v2/resolve {
        # 只允许POST,减少被扫描识别的概率
        limit_except POST { deny all; }
        
        proxy_pass https://127.0.0.1:8053/dns-query;
        proxy_http_version 1.1;
        proxy_set_header Host dns.google;
        
        # 隐藏DoH特征
        proxy_hide_header content-type;
        add_header content-type "application/json" always;
    }
}

对应的SmartDNS配置:

server https://cdn-static.example.com/api/v2/resolve -group overseas -exclude-default-group -host-name cdn-static.example.com -http-host dns.google

-http-host dns.google这个参数让SmartDNS在HTTP Host头中发送dns.google,但TLS SNI仍然是cdn-static.example.com。这样GFW看到的是访问一个普通CDN域名的HTTPS流量。

6.3.3 运营商DNS劫持的长期监测

运营商DNS劫持不是一成不变的,会随着政策调整而变化。需要建立长期监测机制:

#!/bin/bash
# /usr/local/bin/dns-hijack-monitor.sh
# 运营商DNS劫持长期监测脚本

LOG_FILE="/var/log/dns-hijack-monitor.log"
ALERT_FILE="/var/log/dns-hijack-alert.log"

# 监测域名列表:国内正常域名 + 国外敏感域名
DOMAINS=(
    "www.baidu.com"
    "www.taobao.com"
    "www.google.com"
    "www.github.com"
    "www.cloudflare.com"
)

# 基准DNS:使用已知可信的DoH解析结果作为基准
BASELINE_DNS="127.0.0.1"  # SmartDNS本身

# 被监测DNS:运营商DNS
ISP_DNS=("223.5.5.5" "119.29.29.29" "114.114.114.114")

for domain in "${DOMAINS[@]}"; do
    # 获取基准解析结果
    baseline=$(dig @${BASELINE_DNS} ${domain} A +short +time=3 +tries=1 2>/dev/null | sort | head -5)
    
    for isp in "${ISP_DNS[@]}"; do
        # 获取运营商DNS解析结果
        isp_result=$(dig @${isp} ${domain} A +short +time=3 +tries=1 2>/dev/null | sort | head -5)
        
        # 对比结果
        if [ "$baseline" != "$isp_result" ]; then
            echo "$(date '+%Y-%m-%d %H:%M:%S') HIJACK DETECTED | domain=${domain} | isp_dns=${isp}" >> $ALERT_FILE
            echo "  baseline: ${baseline}" >> $ALERT_FILE
            echo "  isp_result: ${isp_result}" >> $ALERT_FILE
        fi
    done
done

# 日志轮转:保留最近7天
find /var/log -name "dns-hijack-monitor.log" -mtime +7 -delete

这个脚本每5分钟跑一次,当运营商DNS返回的结果与SmartDNS(走DoH)不一致时记录告警。长期运行可以绘制出运营商劫持的时间规律。

6.3.4 流量特征隐藏:让DoH看起来像普通HTTPS

GFW对DoH的识别不仅靠SNI,还有流量行为特征。几个需要注意的点:

  1. 请求频率:DoH查询的频率和模式与普通网页浏览不同。如果每秒钟有几十个DoH查询,很容易被标记。SmartDNS的prefetch-domain和serve-expired可以减少实际DoH查询频率。
  2. 连接复用:HTTP/2的多路复用让多个DNS查询共享一个TCP连接,这是正常行为。但如果一个连接上持续有大量DNS查询,仍然可疑。
  3. 请求大小:DoH的POST请求body大小通常在50-200字节,GET请求URL长度也有特征。使用POST + application/dns-message比GET + ?name=xxx更隐蔽。

SmartDNS默认使用POST方式发送DoH查询,这是正确的。但需要注意-http-host参数的使用:如果DoH服务器不支持HTTP Host头与SNI不一致,会导致证书验证失败。

# 验证DoH服务器的Host头兼容性
curl -v --connect-timeout 5 \
  --resolve cdn-static.example.com:443:1.2.3.4 \
  -H "Host: dns.google" \
  -H "content-type: application/dns-message" \
  --data-binary @/dev/null \
  "https://cdn-static.example.com/api/v2/resolve" 2>&1 | grep -E "(HTTP|SSL|error)"

6.4 网络层高可用:多线路接入与故障切换

DNS高可用的前提是网络本身高可用。单线接入的场景下,运营商线路故障会导致整个架构瘫痪。

6.4.1 多WAN口策略路由

OpenWrt下的多WAN配置,结合mwan3实现故障切换:

# /etc/config/mwan3 配置示例
config interface 'wan'
    option enabled '1'
    list track_ip '223.5.5.5'
    list track_ip '119.29.29.29'
    option track_method 'ping'
    option reliability '2'
    option count '1'
    option size '56'
    option max_ttl '60'
    option timeout

## 七、核心疑问与实战常见问题(FAQ)

### SmartDNS配置了DoH上游,为什么dig查询境外域名仍然返回污染IP?

最常见原因是SmartDNS的nameserver分组没做domain-rules绑定,境外DoH服务器和国内DNS混在同一组里,SmartDNS按测速结果选了国内DNS应答。检查smartdns.conf中是否用server-tls或server-https单独定义境外上游,并用domain-rules /example.com/ -nameserver doh-group显式绑定。另一个坑是OpenWrt的dnsmasq仍在监听53端口,客户端实际查的是dnsmasq而非SmartDNS,用netstat -tlnp | grep :53确认监听进程,把dnsmasq的no-resolv和server=127.0.0.1#6053指向SmartDNS。

### 如何验证DNS查询是否真的走了DoH加密而没有泄漏到运营商?

分两步。第一步在网关抓包:tcpdump -i eth0 -n port 53 -c 20,如果看到境外域名出现在明文53端口查询里,说明有客户端绕过了SmartDNS。第二步验证DoH链路:curl -v --doh-url https://1.1.1.1/dns-query https://www.google.com 观察是否返回正常IP。再用dig @127.0.0.1 -p 6053 google.com +short对比直连运营商DNS的结果,如果SmartDNS返回的IP与运营商返回的不同且可正常访问,说明DoH分流生效。注意关闭浏览器自带的安全DNS,否则浏览器会绕过系统解析器。

### sing-box的DNS配置和SmartDNS同时开启会冲突吗?应该怎么分工?

会冲突,两者都监听53或抢答就会出问题。正确分工是SmartDNS监听53端口做全局DNS调度和缓存,sing-box的DNS只监听本地回环的高位端口如5353,通过tun或redirect方式接管代理流量的DNS。sing-box配置里dns.servers用type为https的DoH,并设置detour指向代理出站,确保DNS查询本身走代理隧道。SmartDNS的境外域名上游可以指向sing-box的5353端口,形成SmartDNS调度、sing-box加密转发的链路。关键是不要让两个组件都去查同一个上游,否则缓存和分流规则会互相覆盖。

### 运营商DNS劫持和DNS污染有什么区别,防御手段一样吗?

劫持是运营商返回一个假IP或广告页面IP,通常针对不存在的域名或特定域名,返回的IP可访问但内容是假的。污染是GFW在UDP 53查询路径上抢答,返回不存在的IP如127.0.0.1或Facebook的假地址,导致连接超时。防御手段不同:劫持靠DoH或DoT加密查询让运营商看不到查询内容,同时用SmartDNS的address规则强制指定正确IP。污染必须让DNS查询走加密通道且不能被中间人抢答,DoH over TCP 443能有效规避,因为GFW对443端口的抢答难度大得多。两者都需要在网关层用iptables强制劫持所有53端口流量到本地解析器,防止客户端绕过。