最近在一台 EndeavourOS 机器上排查网络问题,发现部分域名的解析结果明显异常:好几个不相关的域名全部解析到了同一个 IP,而且这个地址和网站真实的托管方毫无关系。这就是典型的 DNS 污染(DNS 缓存投毒)。本文记录问题的定位过程,以及分别在 Linux 和 Windows 上启用加密 DNS 的完整做法。

2026-10-11 更新:后来发现,当前网络访问 1.1.1.1 和 8.8.8.8 比较慢,因此补上阿里云公共 DNS 的配置。Windows 11 可以直接使用原生 DoH;Linux 可以把现有 DoT 上游换成阿里云,也可以通过 SmartDNS 使用阿里云 DoH。加密协议和上游选择是两件事:不必为了加密而固定使用海外 DNS,最终还是要在自己的网络上测试速度和目标域名的解析结果。

一、DNS 污染是怎么回事

DNS 查询绝大多数情况下走 UDP 53 端口,明文、无认证。UDP 本身是谁先应答就用谁的,攻击者只要抢在真实响应之前回一个伪造包,客户端就会采信伪造结果,真实响应到达时反而被丢弃。

于是当链路上存在会主动伪造响应的中间设备时(某些运营商网络、公共 Wi-Fi、被劫持的路由器都可能出现),明文查询的结果就不可信了。表现包括:

  • 域名解析到与网站真实托管方完全无关的 IP;
  • 多个不同域名解析到同一个 IP;
  • IPv6 查询全部返回同一个固定地址;
  • 访问站点超时,或被跳转到无关页面。

关键结论:这种情况下换任何明文 DNS 服务器(包括 8.8.8.8、1.1.1.1)都没用,因为污染发生在你到 DNS 服务器之间的链路上,跟服务器本身无关。换服务器只决定了"你问谁",而明文查询的应答在半路就被人抢答了。

解决思路只有一个:让查询内容加密,中间设备看不到你查了什么,也就无法伪造针对性的应答。主流方案是 DoH(DNS over HTTPS,走 443)和 DoT(DNS over TLS,走 853)。

二、先确认问题:怎么判断解析被污染

不要凭"网站打不开"下结论,先拿数据。最常用的是对比法:同一路径下问多个上游,再和加密通道的结果比对。

2.1 多上游对比

BASH
nslookup example.com 223.5.5.5
nslookup example.com 1.1.1.1
nslookup example.com 8.8.8.8

如果不同上游返回的地址段完全对不上,可以拿返回的 IP 反查 whois,检查归属是不是网站真实的托管方;多个无关域名反复出现同一陌生 IP 时尤其值得怀疑。不同 IP 本身还不能证明污染,CDN 调度也会造成差异。注意这里 8.8.8.8、1.1.1.1 返回的结果同样可能是伪造的,因为查询本身还是明文 UDP。

2.2 用 HTTPS JSON 接口拿到对照组

部分公共 DNS 提供 HTTPS JSON 接口,可以绕过链路上的明文抢答来取对照结果:

BASH
curl -fsS --connect-timeout 5 --max-time 10 \
     "https://120.53.53.53/dns-query?name=example.com&type=A" \
     -H "accept: application/dns-json"
curl -fsS --connect-timeout 5 --max-time 10 \
     "https://223.5.5.5/resolve?name=example.com&type=A"

返回体里 Answer 数组的 data 字段就是 A 记录。阿里云的 /resolve 是 JSON 查询接口,后面系统和浏览器的 DoH 配置则使用 /dns-query,两者不能混填。CDN、负载均衡和缓存也可能让正常解析得到不同 IP,所以差异只是排查线索,还要结合地址归属与实际访问判断。

2.3 一个容易被忽略的坑:上游自身的递归也可能被污染

这一点很重要:DoH/DoT 只保证"你到递归服务器"这一段是加密的,但递归服务器自己去查权威 DNS 时如果走的也是被污染的链路,它缓存里的答案照样是错的。我在实测中就遇到两个加密 DNS,一个返回正常地址段,另一个返回了明显伪造的地址。

所以部署前建议先用 2.2 的 JSON 接口逐个测一下候选上游,确认它对目标域名的递归结果是可信的,再把它设为系统 DNS。下面给出一个快速自测循环:

BASH
for h in 223.5.5.5 223.6.6.6; do
  echo "== 阿里云 $h =="
  curl -fsS --connect-timeout 5 --max-time 10 \
       "https://$h/resolve?name=example.com&type=A"
  printf '\n'
done

2.4 确认加密端口可达

DoT 走 TCP 853,个别网络会封非常用端口。部署前先握手测一下:

BASH
timeout 4 openssl s_client -connect 1.1.1.1:853 </dev/null 2>&1 | grep "Verify return code"

能拿到证书验证输出说明 853 可用。DoH 走 443,一般不用担心。

三、方案选型

方案 端口 特点 适用
DoT TCP 853 单一端口,容易被针对性封禁 Linux(systemd-resolved 原生支持)
DoH TCP 443 与 HTTPS 流量混在一起 Windows 11(原生支持)、浏览器

我最终的选择:Linux 上用 systemd-resolved 走 DoT(系统自带,零额外依赖),双上游配置;Windows 11 用原生 DoH。两条链路部署后互相独立,都不影响已有的网络配置。

3.1 1.1.1.1、8.8.8.8 太慢时:换成阿里云加密 DNS

阿里云公共 DNS 官网列出的服务地址如下:

项目 地址
IPv4 DNS 223.5.5.5、223.6.6.6
IPv6 DNS 2400:3200::1、2400:3200:baba::1
DoH 模板 https://dns.alidns.com/dns-query
DoT 主机名 dns.alidns.com(TCP 853)

只把网卡 DNS 改成 223.5.5.5、223.6.6.6,默认仍然是明文 DNS。要按后面的步骤启用 DoH 或 DoT,才能同时获得加密保护。

如果当前网络访问海外 DNS 延迟高,可以先试阿里云。测试下面两个 HTTPS 接口,既看结果,也看请求耗时:

BASH
for h in 223.5.5.5 223.6.6.6; do
  echo "== $h =="
  curl -fsS --connect-timeout 5 --max-time 10 \
       -w '\nHTTP %{http_code},总耗时 %{time_total}s\n' \
       "https://$h/resolve?name=github.com&type=A"
done

Windows 下可以用 curl.exe 执行单条请求。这里测的是 HTTPS JSON 请求,包含连接与 TLS 握手开销,不等于系统 DoH 复用连接后的单次解析延迟;建议换几个常用域名、多测几次。DNS 查询快也不代表网站一定快,网站连接、CDN 节点和跨境链路仍然会影响访问体验。

四、Linux:systemd-resolved 启用 DoT

环境:EndeavourOS(Arch 系),NetworkManager 管理网络,systemd-resolved 已安装但未启用。Ubuntu 20.04+ 默认已启用 systemd-resolved,跳过启用步骤即可,其余相同。

4.1 配置 resolved

编辑 /etc/systemd/resolved.conf:

INI
[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 120.53.53.53#dot.pub
FallbackDNS=8.8.8.8#dns.google
DNSOverTLS=yes
Cache=yes
CacheFromLocalhost=no
DNSStubListener=yes

几个要点:

  • IP#主机名 语法:# 后面的主机名用于 TLS 证书校验(SNI),不是随便写的。cloudflare-dns.com 是 1.1.1.1 的 DoT 域名,dot.pub 是腾讯的。需要系统装有 ca-certificates。
  • DNSOverTLS=yes 表示只走 DoT,不回退明文,杜绝漏网。
  • 多上游不是坏事:主上游 853 被封时还有备份。

如果这些海外上游较慢,又希望继续使用 systemd-resolved 原生 DoT,可以把上面的 DNS 和 FallbackDNS 两行替换为:

INI
DNS=223.5.5.5#dns.alidns.com 223.6.6.6#dns.alidns.com
FallbackDNS=

其余设置保持不变,仍然是 DNSOverTLS=yes。空的 FallbackDNS= 清除后备列表;resolved 的后备列表只在没有其他 DNS 配置信息时使用,并不是“主服务器慢了就自动换它”。使用下面的一键脚本时,也要同步替换脚本中的这两行。这是阿里云 DoT;如果需要走 443 的 DoH,看 4.8 节。

4.2 阻止 NetworkManager 注入明文 DNS

这是最容易踩的坑。NetworkManager 会把 DHCP 下发的 DNS(也就是运营商的明文 DNS)作为每链接(per-link)DNS 推给 resolved,而 per-link DNS 的优先级高于 resolved.conf 里的全局 DNS——结果是 DoT 配了但根本没被用上,查询还是从运营商明文走。

新建 /etc/NetworkManager/conf.d/10-dns-none.conf:

INI
[main]
dns=none

dns=none 让 NetworkManager 完全不参与 DNS 下发,resolv.conf 由我们自己管理。

4.3 启用并切换 resolv.conf

BASH
sudo systemctl enable --now systemd-resolved

# 把 resolv.conf 切到本地 stub
sudo rm -f /etc/resolv.conf
sudo ln -s ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

sudo systemctl restart systemd-resolved
sudo nmcli connection reload
sudo nmcli general reload

nmcli reload 只是重载配置,不会断网,SSH 远程操作也安全。

4.4 一键脚本

把上面的步骤整合成脚本,幂等可重复执行:

BASH
#!/usr/bin/env bash
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "请用 sudo 运行"; exit 1; }

TS=$(date +%Y%m%d-%H%M%S)
backup() { [ -f "$1" ] && cp -a "$1" "$1.bak.$TS" || true; }

backup /etc/resolv.conf
backup /etc/systemd/resolved.conf

cat > /etc/systemd/resolved.conf <<'EOF'
[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 120.53.53.53#dot.pub
FallbackDNS=8.8.8.8#dns.google
DNSOverTLS=yes
Cache=yes
CacheFromLocalhost=no
DNSStubListener=yes
EOF

mkdir -p /etc/NetworkManager/conf.d
cat > /etc/NetworkManager/conf.d/10-dns-none.conf <<'EOF'
[main]
dns=none
EOF

systemctl enable --now systemd-resolved
rm -f /etc/resolv.conf
ln -s ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl restart systemd-resolved
nmcli connection reload 2>/dev/null || true
nmcli general reload 2>/dev/null || true

4.5 验证

BASH
resolvectl status

确认输出里 Current DNS Server 是配置的 DoT 上游,且没有 per-link 的 DNS Servers(DHCP 的应该消失了)。

BASH
resolvectl query github.com

把解析到的 IP 与 2.2 节 DoH JSON 接口的结果比对,一致即正常。还可以确认流量真的走了 853:

BASH
sudo ss -tunap | grep ':853'

能看到与上游 IP 的 ESTABLISHED 连接。

4.6 回滚

BASH
sudo systemctl disable --now systemd-resolved
sudo rm /etc/NetworkManager/conf.d/10-dns-none.conf
sudo cp /etc/resolv.conf.bak.<TS> /etc/resolv.conf
sudo nmcli general reload

4.7 没有 systemd 的发行版

Void、Alpine 等无 systemd 的系统可以用 smartdns(支持 DoH/DoT 上游、多线路测速)或 dnscrypt-proxy(支持 DNSCrypt + DoH),思路相同:本地起一个监听 53 的服务,上游全部走加密通道,再把 /etc/resolv.conf 指向 127.0.0.1。

4.8 Linux 使用阿里云 DoH:SmartDNS 转发

systemd-resolved 原生支持 DoT,不能把 https://dns.alidns.com/dns-query 直接填进它的 DNS=。需要 DoH 时,可以让 SmartDNS 负责 HTTPS 上游,resolved 继续提供系统的本地 stub:

TEXT
应用 → systemd-resolved(127.0.0.53:53)
     → SmartDNS(127.0.0.1:6053)
     → 阿里云 DoH(HTTPS 443)

下面接在 4.1~4.3 的配置之后操作,保留 dns=none 和指向 stub 的 /etc/resolv.conf。在 EndeavourOS / Arch 上安装:

BASH
sudo pacman -S smartdns ca-certificates

其他发行版按 SmartDNS 官方 Linux 安装说明选择对应的软件包。先备份 /etc/smartdns/smartdns.conf,再将它的内容设为下面的最小配置;已有自定义规则的机器需要合并配置,并检查其他配置文件有没有额外的明文上游:

bind 127.0.0.1:6053
bind-tcp 127.0.0.1:6053
cache-size 4096
speed-check-mode none
dualstack-ip-selection no

server-https https://dns.alidns.com/dns-query -host-ip 223.5.5.5 -tls-host-verify dns.alidns.com
server-https https://dns.alidns.com/dns-query -host-ip 223.6.6.6 -tls-host-verify dns.alidns.com

-host-ip 固定上游连接 IP,避免先用明文 DNS 解析 DoH 域名,也避免本地解析循环;URL 保留域名,用于 HTTPS 主机信息,并通过 -tls-host-verify 校验证书名称。不要添加 -no-check-certificate 或 -k。这里关闭网站 IP 测速与双栈优选,让 SmartDNS 主要承担加密转发和缓存。参数见 SmartDNS 上游配置与测速模式。

启动服务,先确认 SmartDNS 能独立解析,再切换系统:

BASH
sudo systemctl enable --now smartdns
sudo systemctl restart smartdns
# dig 来自 Arch 的 bind 软件包或 Debian / Ubuntu 的 dnsutils
dig @127.0.0.1 -p 6053 github.com

查询成功后,新建 /etc/systemd/resolved.conf.d/60-alidns-doh.conf(目录不存在时先创建):

INI
[Resolve]
DNS=
DNS=127.0.0.1:6053
FallbackDNS=
DNSOverTLS=no

第一条空的 DNS= 清掉原来的全局上游,第二条只保留本地 SmartDNS。这里的 DNSOverTLS=no 只让 resolved 到本机 SmartDNS 的回环通信使用普通 DNS,SmartDNS 到阿里云仍然走 DoH。端口和配置覆盖规则见 resolved.conf 手册。

BASH
sudo systemctl restart systemd-resolved
sudo resolvectl flush-caches
resolvectl status
resolvectl query github.com

确认全局 DNS 为 127.0.0.1:6053,各网络链接没有残留的运营商 DNS。如果失败,先看 journalctl -u smartdns -n 50 --no-pager。需要验证实际传输时,在另一终端抓取外网接口的流量,再发起未命中缓存的查询:应看到到阿里云 IP 的 443 通信,没有对外的 UDP/TCP 53 查询。本机回环上的 53/6053 通信是正常的。

回到前面的原生 DoT 方案,只需移除本节的覆盖文件并重启 resolved,再停用 SmartDNS:

BASH
sudo rm /etc/systemd/resolved.conf.d/60-alidns-doh.conf
sudo systemctl restart systemd-resolved
sudo resolvectl flush-caches
sudo systemctl disable --now smartdns

如果 SmartDNS 原先已经在使用,应恢复备份的配置并保留服务,而不是停用它。

五、Windows 11:原生 DoH

Windows 11 和 Windows Server 2022 起系统原生支持 DoH,不需要装任何软件。

5.1 命令行方式(推荐)

下面以阿里云作为默认上游。管理员 PowerShell,先给两个 DNS 服务器注册 DoH 模板:

POWERSHELL
netsh dns add encryption server=223.5.5.5 dohtemplate=https://dns.alidns.com/dns-query autoupgrade=yes udpfallback=no
netsh dns add encryption server=223.6.6.6 dohtemplate=https://dns.alidns.com/dns-query autoupgrade=yes udpfallback=no
netsh dns show encryption
  • autoupgrade=yes:该服务器支持 DoH 时自动升级为加密查询;
  • udpfallback=no:加密不可用时不回退明文,避免静默降级成可污染的查询。

如果提示条目已经存在,把对应命令的 add encryption 改成 set encryption 后重新执行即可。命令参数可参考 Microsoft netsh DNS 客户端文档。

然后把网卡的 DNS 改成这个服务器:

POWERSHELL
# 查接口索引
Get-NetAdapter
# 按名字设置,例如以太网
Set-DnsClientServerAddress -InterfaceAlias "以太网" -ServerAddresses 223.5.5.5,223.6.6.6

清一下缓存让旧记录失效:

POWERSHELL
Clear-DnsClientCache

把 "以太网" 换成 Get-NetAdapter 显示的实际网卡名,例如 "WLAN"。从 Cloudflare 或 Google 切换时,应替换网卡的服务器列表,而不是把阿里云追加在慢上游后面。

如果网卡还保留了 DHCP 下发的 IPv6 DNS,也可能从那条路径发出查询。使用 IPv6 DNS 时,同样先注册模板,再把它们加入同一张网卡的服务器列表;通过 IPv4 DoH 查询也能获得 AAAA 记录,并不需要为了访问 IPv6 网站而关闭 IPv6:

POWERSHELL
netsh dns add encryption server=2400:3200::1 dohtemplate=https://dns.alidns.com/dns-query autoupgrade=yes udpfallback=no
netsh dns add encryption server=2400:3200:baba::1 dohtemplate=https://dns.alidns.com/dns-query autoupgrade=yes udpfallback=no
Set-DnsClientServerAddress -InterfaceAlias "以太网" -ServerAddresses 223.5.5.5,223.6.6.6,2400:3200::1,2400:3200:baba::1
Clear-DnsClientCache

上面的 IPv6 上游需要网络本身具有可用的 IPv6 连接。其他公共 DoH 服务也可按相同方式配置,替换服务器 IP 和模板即可:

提供商 IPv4 DNS DoH 模板
阿里云 223.5.5.5、223.6.6.6 https://dns.alidns.com/dns-query
Cloudflare 1.1.1.1、1.0.0.1 https://cloudflare-dns.com/dns-query
Google 8.8.8.8、8.8.4.4 https://dns.google/dns-query

5.2 GUI 方式

设置 → 网络和 Internet → 当前连接(WLAN/以太网)→ 硬件属性 → DNS 服务器分配 → 编辑:

  1. 选"手动",开启 IPv4;
  2. 首选 DNS 填 223.5.5.5,备用 DNS 填 223.6.6.6;
  3. 两个地址都启用 DoH。新版界面选择"开启(手动模板)",模板填 https://dns.alidns.com/dns-query,关闭"回退到纯文本";旧版界面则选"仅加密(DNS over HTTPS)";
  4. 保存后执行 ipconfig /flushdns。

如果无法选择 DoH,先执行 5.1 的命令注册模板;仍不可用时,检查系统版本及是否有禁用 DoH 的组策略。

5.3 验证

POWERSHELL
# 查看阿里云 DoH 模板与降级设置
Get-DnsClientDohServerAddress -ServerAddress 223.5.5.5,223.6.6.6

# 确认网卡实际使用的服务器,IPv4 / IPv6 都要检查
Get-DnsClientServerAddress -InterfaceAlias "以太网"

# 使用系统默认 DNS 路径查询
Resolve-DnsName github.com

确认 DohTemplate 为 https://dns.alidns.com/dns-query、AutoUpgrade 为 True、AllowFallbackToUdp 为 False,网卡服务器列表也已切换到阿里云。这些命令验证的是配置和解析功能;模板存在并不等于已经证明每次查询都走了 DoH。关于模板注册与网卡设置的关系,可参考 Microsoft DoH 配置说明。

要确认实际传输,可以在清缓存后用 Wireshark 抓取当前网卡的流量,再执行上面的解析命令;检查到阿里云 IP 的 443 通信,以及是否还有对外的 UDP/TCP 53 查询。netstat 不能可靠排除短暂的 UDP 查询,仅看到 443 连接也不能单独证明所有 DNS 已加密。验证系统默认路径时,省略 Resolve-DnsName 的 -Server 参数。

5.4 Windows 10 的替代

Windows 10 没有通用的原生 DoH 配置界面,可以选择:

  • 浏览器内置 DoH:Chrome/Edge 在"设置 → 隐私和安全 → 安全"里开启"使用安全 DNS"并选择自定义提供商;Firefox 在"设置 → 隐私与安全 → DNS over HTTPS"里开启。只保护浏览器内的解析,但对大多数场景已经够用;
  • 第三方本地 DoH 代理(如 smartdns、dnscrypt-proxy 的 Windows 版),监听 127.0.0.1:53,网卡 DNS 指向它。

浏览器的自定义提供商 URL 填 https://dns.alidns.com/dns-query,不要填 223.5.5.5 或 JSON 接口 /resolve。Windows 11、Linux 上也可以单独设置浏览器 DoH;如果系统已经切到阿里云,而浏览器仍指定了较慢的海外 DoH 上游,需要一并修改浏览器设置。

六、收尾与注意事项

  • 缓存要清理:切换后旧污染记录可能还在各级缓存里,Linux 用 resolvectl flush-caches,Windows 用 ipconfig /flushdns,浏览器也要重启。
  • 部分程序绕过系统 DNS:某些客户端硬编码 8.8.8.8 直发明文查询,系统层加密管不到它们,只能逐个在应用内设置。
  • 整网方案:家里多台设备不想逐台配置的话,在路由器(OpenWrt 等支持 DoH/DoT 上游的固件)上统一处理,下游设备无感。
  • 先测上游再上线:再次强调 2.3 节的结论,加密只保护你到递归的那一段,上游递归自身返回的结果要预先用 JSON 接口验证过。

至此两台设备的解析都恢复了正常:解析结果与 DoH JSON 接口完全一致,明文 53 上再也没了抢答的影子。整个过程不动防火墙、不改默认路由,任何一步出问题都可以按文中的回滚步骤退回原状。