DoH 配置实战:在服务器、路由与内网解析器上落地 DNS over HTTPS
传统 DNS 查询走 UDP 53 端口,全程明文。你家的路由器、公司的出口交换机、咖啡馆的 Wi-Fi、还有沿途每一个运营商节点,都能清清楚楚看见你在查哪个域名。这不是危言耸听,DNS 明文是互联网上最容易被大规模采集的元数据之一。DNS over HTTPS(DoH,RFC 8484)把 DNS 查询包进 HTTPS 请求,用 443 端口和 TLS 加密,让旁观者只看到“这台机器在和某个 DoH 服务商通信”,却看不到具体查了什么。
但“知道 DoH 是什么”和“把它落到生产环境”是两回事。本文不重复讲原理,只讲运维真正要动手的部分:怎么在单台服务器上接上 DoH、怎么让整张网统一走加密解析、怎么自建内网 DoH 服务、以及怎么验证它真的生效。目标读者是中掌握了基本网络运维、需要在真实环境里把明文 DNS 替换掉的工程师。
1. 先想清楚:你要加密的是哪一段
动手前先定位痛点,不同位置的做法完全不同:
- 单台 Linux 服务器:只想让这一台机器的 DNS 不被旁人看见,改本机解析器即可,影响范围最小。
- 整张办公网 / 家庭网:希望所有终端(手机、笔记本、IoT)统一走加密 DNS,该在路由器或网关上做,向 DHCP 下发的就是加密解析器地址。
- 对外暴露的服务 / 公网出口:需要从被封锁或受限的网络里把 DNS 透出去,用 cloudflared 这类 DoH 代理最稳。
- 内网有私有域名:必须分层解析,否则内网服务全断。
先画清楚你的边界在哪,再决定用下面哪一段配置。混用很正常,关键是别让私有域被错误地发到公网 DoH 上。
2. 单台服务器:cloudflared 本地 DoH 代理
这是最通用、对系统侵入最小的方案。cloudflared 在本地起一个 DNS 监听(默认 53),把收到的查询通过 HTTPS 转发到上游 DoH 端点,本机其他进程只要把解析器指向 127.0.0.1 即可。
安装(Debian/Ubuntu):
# 添加 Cloudflare 仓库并安装
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared
配置文件 /etc/cloudflared/config.yml:
proxy-dns: true
proxy-dns-port: 53
proxy-dns-upstream:
- https://1.1.1.1/dns-query
- https://1.0.0.1/dns-query
然后以系统服务方式常驻:
最后把本机解析器指向本地代理。编辑 /etc/resolv.conf 或 systemd-resolved 配置,把 nameserver 改成 127.0.0.1,并保留一个非加密备用(仅当代理挂掉时兜底,注意这会退化成明文,是取舍不是最佳)。验证:
这套方案的好处是:应用层几乎无感,所有进程自动受益;缺点是只覆盖这一台机器,且 cloudflared 本身成了新的单点,需要监控其存活。
3. 系统级:systemd-resolved 的 DoT 与 DoH 误区
很多教程让你在 systemd-resolved 里写 DNS=1.1.1.1#cloudflare-dns.com 加 DNSOverTLS=yes,并号称这是 DoH。严格说,systemd-resolved 的 DNSOverTLS 实现的是 DoT(DNS over TLS),不是 DoH。两者都是加密,但端口和封装不同。这不算错,只是命名要准确,避免向上汇报时口径混乱。
如果你确实要在这层用 DoH,更现实的做法是:把 systemd-resolved 的上游设为上一步的本地 cloudflared(即 DNS=127.0.0.1),由 cloudflared 负责 DoH 封装。这样职责清晰:系统解析器管缓存和本机策略,cloudflared 管加密出口。
4. 整网统一:在路由器上向终端下发加密解析
只改服务器不够,手机和笔记本还是明文。真正的“全站加密”要在网关做。两种思路:
思路 A:网关自己跑 DoH 代理。 在 OpenWrt/爱快/软路由上装 cloudflared 或 dnscrypt-proxy,网关的 DNS 监听对外服务,DHCP 下发给终端的就是网关自己的地址。终端无感知,全部流量在网关汇聚后走 DoH 出去。
思路 B:DHCP 直接下发公共 DoH 端点。 部分现代路由器支持下发“加密 DNS”选项(DoH/DoT 的 bootstrap 域名),终端系统(新版 Android、Windows 11、iOS)能识别并直连。但这要求终端系统支持,老设备会忽略,覆盖不全。
推荐思路 A,因为它不依赖终端能力,且你能在网关统一加过滤和日志。注意:下发后,之前依赖网关 DNS 做广告拦截、家长控制的能力要重新接回网关的解析层,否则这些策略会随明文 DNS 一起消失。
5. 内网自建:用 CoreDNS 提供 DoH 服务
对有一定规模的团队,把解析器全部指向第三方 DoH 不总是合适——你把查询元数据交给了别人。更可控的是自建递归 + DoH 出口:用 CoreDNS 在 443 端口提供 DoH 端点,上游再转发到你信任的递归或公共 DoH。
Corefile 骨架:
https://.:443 {
tls /etc/coredns/cert.pem /etc/coredns/key.pem
forward . 1.1.1.1 8.8.8.8 {
tls_servername cloudflare-dns.com
}
cache 300
log
}
这里有两个工程要点。第一,私有域分流:在 forward 之前加 file 或 kubernetes 插件处理内部 zone,确保 *.internal 不外流。第二,证书:DoH 的 443 端口需要合法 TLS 证书,用 ACME 自动续期,证书过期是这类服务最常见的“悄悄挂掉”原因。
6. 验证:用抓包证明明文真的没了
配置完最忌“配完即上线”。DoH 是否生效,靠看配置是不行的,要靠证伪。
用 curl 打 DoH 的 JSON API 确认解析正常:
curl -s -H 'Accept: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=example.com&type=A' | jq
用 tcpdump 抓明文 DNS,确认没有外泄:
在 DoH 正确生效时,这条命令应当长时间静默——你不该看到任何发往外网 53 端口的查询。如果还在刷 8.8.8.8.53 的包,说明有进程绕过了:可能是某个容器硬编码了 8.8.8.8,可能是 /etc/resolv.conf 没改干净,也可能是应用直接用了自家 DNS 库。逐个排查,直到抓包静默为止。这一步是 DoH 上线的“质量门”,过不了就不算完成。
7. 选提供商的隐私口径
DoH 加密了路径,但没消除“谁在看”。主流提供商的口径差别很大,选型时应对齐合规要求:
- Cloudflare(1.1.1.1):承诺 24 小时内删除查询日志,每年由 KPMG 审计,速度通常最快。
- Google(8.8.8.8):临时记录完整源 IP,24-48 小时内匿名化,稳定性和覆盖好。
- Quad9(9.9.9.9):总部瑞士,默认不记录 IP,内置威胁情报拦截恶意域名。
- NextDNS:可定制过滤规则,适合做团队级策略,但属于商业服务。
若数据合规要求严格(如国内数据出境考量),优先自建递归或选管辖权友好的提供商,并保留 DNSSEC 校验。
8. 上线后的巡检与防退化纪律
DoH 配置是典型的“会悄悄退化”项:一次错误的回滚、一次证书过期、一个应用硬编码了明文 DNS,加密就破了,而且表面一切正常。建立纪律:
- 每日:监控 cloudflared/CoreDNS 进程存活与上游延迟,进程挂了终端会无声退化到明文兜底。
- 每周:跑一次 tcpdump 抽查 port 53,确认仍有终端在走明文(新接入的设备最容易漏)。
- 每月:检查 DoH 服务端 TLS 证书剩余有效期,证书过期会导致整个 DoH 端点不可用。
- 每次变更前:在测试机改解析器验证,再推全量;变更后立刻 dig + tcpdump 双重确认。
- 红线:禁止在生产里长期保留明文兜底 DNS 而不监控;禁止把内网私有域错误地转发到公网 DoH。