技术指南

私有 DNS 上线:限制递归范围、验证加密与故障回退

把私有 DNS 上线拆成端口盘点、精确准入、加密边界、canary 切换、上游故障与客户端回滚。

自建 DNS 的首要目标不是堆叠协议、过滤表和转发器,而是让一组明确的客户端获得可观察、可撤销的解析服务。先写清服务对象、查询路径、隐私边界与失败回退,再决定是否需要过滤、DoH、DoT 或分流。任何时候都不要把递归解析器无条件开放给互联网。

来源提供组件清单,本文收敛为一次可回滚上线

归档来源完整覆盖 AdGuard Home、过滤列表、加密 DNS、Mosdns、Tailscale 与客户端配置,可保留的是端口盘点、私网接入、上游检查和日志排障思路。它也包含产品排行、固定性能结论、广告拦截率、过时规则语法、无条件停用 systemd-resolved 和复制即用的公网入口。本文删除这些保证,把范围收敛为一台私有转发解析器的准入、切换与故障恢复。

先把五个对象写进解析契约

对象发布前必须回答证据
客户端哪些设备或网段可以查询逐个 IP/CIDR、私网身份与测试设备
监听入口监听哪个地址、端口和传输socket、主机防火墙与云防火墙
解析器缓存、过滤、日志和保留策略是什么版本化配置与运行状态
上游由谁看到查询、失败时怎样处理端点、证书、超时、失败样本
客户端回退切换失败时恢复到哪里旧配置、回滚命令与恢复时间

权威 DNS、递归 DNS 和本地 stub 不是同一角色。AdGuard Home 在这里作为私有转发解析器;它向上游查询并对下游提供缓存/过滤。域名注册商的权威服务器仍负责发布区域数据,systemd-resolved 等本机组件仍可能负责应用到解析器之间的本地路由。

先盘点端口、主机解析链和现有回退

这些命令只读。输出可能包含内部域名、搜索后缀和地址;证据外发前脱敏
date --utc --iso-8601=seconds
ss -lntup '( sport = :53 )'
resolvectl status
resolvectl dns
readlink -f /etc/resolv.conf
systemd-analyze cat-config systemd/resolved.conf

看到 127.0.0.53:53 只说明本机 stub 正在监听,不能直接推出冲突方案。先确认 `/etc/resolv.conf` 模式、每条链路的 DNS、VPN/容器接管和断开新解析器后的回退。若确实要关闭 stub listener,先用 drop-in 保存候选和回滚文件,并在独立会话中证明主机仍能解析;不要删除 resolv.conf 或直接停掉服务。

固定发布物,并在解包前核对官方校验和

示例版本在 2026-08-17 经官方 release API 与 checksums.txt 核对;部署时重新选择受审版本和架构,并保存摘要
release=v0.107.78
asset=AdGuardHome_linux_amd64.tar.gz
base=https://github.com/AdguardTeam/AdGuardHome/releases/download/$release

curl --fail --location --remote-name "$base/$asset"
curl --fail --location --remote-name "$base/checksums.txt"
grep "  ./$asset
quot; checksums.txt > selected.sha256 sha256sum --check selected.sha256 tar -tzf "$asset" | sed -n '1,20p'

容器部署同样要固定镜像摘要并保存 Compose 配置。官方 Docker 文档只在使用 DHCP 服务时明确要求 host networking;桥接和 host 模式会改变客户端地址、端口发布和 allowlist 语义,必须按实际网络验证,不能把 `network_mode: host` 当通用默认值。

候选配置同时限制监听与客户端

地址只是文档示例;按实际接口和客户端填写。先用 `AdGuardHome --check-config` 验证完整候选
dns:
  bind_hosts:
    - 127.0.0.1
    - 100.101.102.103   # 示例:本机实际的私网地址
  port: 53
  allowed_clients:
    - 127.0.0.1
    - 100.101.102.104   # 示例:明确授权的客户端
  ratelimit: 20
  refuse_any: true
  upstream_dns:
    - https://resolver.example/dns-query
  upstream_timeout: 3s
  cache_enabled: true
  cache_optimistic: false

`bind_hosts` 决定进程在哪些本地地址收包,`allowed_clients` 决定哪些来源获准查询,两者不能互相替代。主机防火墙和云安全组继续只允许管理网、LAN 或 tailnet 路径。`ratelimit` 与 `refuse_any` 是额外缓解,不是开放公网后的授权系统。

加密 DNS 只保护客户端到解析器这一跳

传输得到什么仍需处理
明文 DNS内网兼容性与简单故障定位链路可见查询;必须限制在可信路径
DoT客户端到解析器的 TLS 机密性与完整性证书名、端口、授权、上游与日志
DoH把查询映射到 HTTPS 交换HTTP/TLS 证书、代理信任、授权与元数据
DoQ基于 QUIC 的加密 DNS 传输客户端支持、UDP 路径、证书与授权

RFC 8484 明确 DoH 依赖底层 HTTP/TLS 安全,也提醒加密不能消除流量分析或解析器侧隐私问题。解析器和它选择的上游仍可能看到查询;‘使用 HTTPS’不能翻译成匿名、不可封锁、零泄漏或绝对更快。

反向代理只信任已知代理地址

AdGuard Home 的官方加密文档要求 DoH 反向代理进入 `trusted_proxies` 后,才把受支持的转发头解释为客户端地址。该列表只能包含真实代理地址或精确网段;否则攻击者可伪造来源,绕过按客户端配置的准入、统计或策略。代理层还要限制请求大小、速率和管理路径。

HTTP 状态本身不证明 DNS 响应正确;还要从获准与未获准客户端各发一次真实查询
# 从客户端验证证书、HTTP 与 DNS 三层
curl --fail --silent --show-error \
  --output /dev/null --write-out '%{http_code}\n' \
  https://dns.example.com/dns-query

# 使用支持 DoH 的 dig 时再做真实查询;否则至少从同一路径检查 TLS
openssl s_client -connect dns.example.com:443 \
  -servername dns.example.com -verify_return_error </dev/null

上游选择写成隐私与故障策略,不写排行榜

  1. 保存上游运营方、端点、协议、隐私/日志政策和核对时间。
  2. 从部署网络验证证书、IPv4/IPv6、超时与错误响应,不用单次最快结果排名。
  3. 决定失败时返回 SERVFAIL、使用备用上游还是短时服务过期缓存,并写清最大窗口。
  4. 多个上游若策略不同,可能让过滤或 split DNS 失效;不要假设客户端或解析器严格按列表顺序使用。
  5. 过滤列表、重写和上游分别变更,一次只发布一个变量。

Tailscale 文档明确指出现代系统和其 DNS 代理可能并行查询、重排或负载均衡多个解析器。因此‘把旧 DNS 放第二位’不是可靠的主备设计。需要一致回答时,用一个受控解析入口在内部执行条件转发;需要回滚时,恢复客户端的上一份完整配置。

私网 DNS 与 MagicDNS 用明确域名边界共存

Tailscale 的 restricted nameserver 只处理指定搜索域,global nameserver 处理所有查询。私有解析器若使用 Tailscale IP,客户端还必须接受 tailnet DNS 设置并被 ACL 允许到达该地址。不要手工复制 MagicDNS 设备记录来‘共存’;先决定哪些后缀归私有解析器,其他名称继续由 MagicDNS 或公共解析路径处理。

过滤效果用代表性集合和误杀预算验收

集合验收失败动作
应解析域名关键业务、更新、登录、支付和通知全部成功恢复过滤版本并记录被拦规则
应阻断域名已授权测试域名得到预期阻断响应核对列表加载与匹配规则
未知域名不因关键词或宽泛通配符误杀缩小自定义规则
故障域名NXDOMAIN、SERVFAIL、超时能被区分不要用重写掩盖上游故障

DNS 只能按名称回答或阻断,无法可靠区分同域上的广告与内容,也无法给出通用拦截率。过滤列表属于可变依赖:固定来源与更新时间,保存更新前后规则数量和差异,在 canary 客户端观察误杀后再扩大范围。

先显式查询,再切一台 canary 客户端

`.invalid` 为保留顶级域;负面答案用于检查错误语义,不代表上游可用性
resolver=100.101.102.103

# 先绕过系统配置,分别验证 UDP、TCP 与负面响应
dig +time=2 +tries=1 @"$resolver" example.com A
dig +tcp +time=2 +tries=1 @"$resolver" example.com A
dig +time=2 +tries=1 @"$resolver" does-not-exist.invalid A

# 切换 canary 后,验证应用实际使用的系统路径
resolvectl query example.com
resolvectl statistics
  • 从获准客户端确认 UDP 和 TCP;从未授权来源确认被拒绝或无响应。
  • 验证 A、AAAA、CNAME、NXDOMAIN、DNSSEC 相关域和一个关键业务域。
  • 切换一台 canary,观察至少一个完整业务窗口的错误、延迟、上游状态和误杀。
  • 再按设备组扩大,不同时修改 DHCP、tailnet、过滤列表、上游和缓存。
  • 每一步保存旧 DNS 配置和 TTL/租约影响,能在约定时间内恢复。

回退是客户端配置事务,不是多填一个地址

预先定义触发门槛,例如连续查询超时、SERVFAIL 比例、关键域名失败、上游证书错误或误杀超过预算。触发后停止扩大范围,恢复 canary 或 DHCP/tailnet 的上一份解析配置,等待客户端重新取配置,再从系统路径复测。解析器恢复后先只接受测试客户端,不要立即重新全量切换。

记录能定位故障的最小数据

  • 请求总量、响应码、上游、耗时分布、缓存命中和被阻断原因按时间聚合。
  • 查询名、客户端地址和内部重写都可能敏感;缩短保留期、限制访问并避免无目的全量日志。
  • 同时监控 UDP/TCP socket、进程重启、配置摘要、磁盘、时钟与证书到期。
  • 从 resolver 主机、canary 和外部未授权网络分别运行探测,明确每个探测应成功还是失败。
  • 任何自动更新都先下载、校验、比较、验证候选并保留上一版本。

隔离演练验证了准入、故障和恢复

VPScope 下载 AdGuard Home v0.107.78 Linux amd64 官方归档及 checksums.txt,SHA-256 `2070f644644be8299232f4a7bff857036fb1423563c1bf8c787e07aaf4f88278` 一致。程序只在 127.0.0.1 的高端口启动,完整候选通过 `--check-config`;一个一次性本地上游为测试域返回保留地址 192.0.2.22。

来自 127.0.0.1 的 UDP 与 TCP 查询都得到 192.0.2.22;从 127.0.0.2 发出的查询被精确 allowlist 拒绝并以非零退出。停止上游后,新查询返回 SERVFAIL;重启上游后,新的测试名称恢复。最后两个子进程停止,15355、15455、18125 端口关闭,临时目录由受限清理条件删除。这个演练不证明公网、真实 tailnet、TLS、过滤列表、生产缓存或服务管理器行为。

私有 DNS 上线前的可复查清单

  • 客户端、监听地址、传输、上游、日志和回退对象已写入解析契约。
  • 端口 53、systemd-resolved、VPN、容器与 resolv.conf 的现状已保存,没有盲停本机解析。
  • 二进制或容器版本已固定并核对官方摘要,候选配置通过当前版本验证。
  • 监听地址、allowed_clients、主机防火墙和云防火墙形成一致的最小准入。
  • Web 管理面不向普通查询客户端开放,凭据、配置和查询日志按敏感数据保护。
  • DoH/DoT/DoQ 的证书、授权与代理信任边界已单独验证,没有宣称匿名或绝对性能。
  • 上游运营方、协议、超时、失败策略和隐私边界有当前记录。
  • 过滤列表用应通过/应阻断/未知集合验收,并有误杀回退版本。
  • 显式 UDP/TCP、未授权来源、canary 系统路径和关键业务查询均符合预期。
  • 上游失败、SERVFAIL、恢复和客户端配置回滚已演练;所有临时进程与端口已清理。

返回知识库