DNS 1 毫秒就返回了,网站却打不开:SmartDNS × AdGuard Home 的三层陷阱
这次检查最容易让人放松警惕的地方,是 DNS 一直在回答。
服务状态是 running,端口能连上,国内网站正常,几个查询甚至只花了零到一毫秒。继续看返回的地址,问题才露出来:部分境外域名被指向了不相干的地址,换到第二个 DNS 入口也没有恢复。
最终修复涉及三个不同层次:让正确的域名走正确的上游,为境外查询建立实测可信的 DoH 路径,以及处理 SmartDNS 47.1 与现场代理之间的 CONNECT 目标兼容问题。最后再收拾缓存和客户端入口,才算走完闭环。

图 1:本文原创示意图。低延迟是一项性能指标,地址正确性需要另外建立证据。
一、先别急着换 DNS,看看究竟是谁在回答
我之前写过一篇 AdGuard Home 与 DoH 排障文章。这次现场让我给那篇文章补上一条很重要的边界:把某个上游的传输协议改成 HTTPS,并不能保证它对所有域名都给出你需要的可靠结果。
这次的环境也更像许多家庭实验室的日常:一台软路由运行 dnsmasq 和 SmartDNS,另一台 Linux 主机运行 AdGuard Home,客户端同时拿到了两个 DNS 地址。代理服务则已经存在,并不和 SmartDNS 运行在同一台设备上。
为了方便阅读,本文只使用两个角色:A 是运行 SmartDNS 的软路由,B 是运行 AdGuard Home 的过滤主机。所有真实设备地址、计算机名称、本地域名、账号和私有路径均已省略。文中的端口、版本、测试数量与配置行为来自实际记录;配图中的拓扑是脱敏后的原创示意。
想象学校里有两处问路窗口。一个窗口负责检查来访名单,另一个窗口负责查询教室位置。你在手机上填了两个窗口的号码,并不意味着每次都会严格按“先第一个、失败再第二个”的顺序拨号。不同系统、应用和浏览器可能缓存、竞争查询,或者自行选择入口。
所以我先确认了实际链路,而没有直接修改客户端的 DNS 列表。

图 2:主链路与备用入口。角色表示职责,不对应任何公开的真实设备标识。
现场的主入口是 dnsmasq 的 53 端口,它只把外部解析交给本机 SmartDNS 的 6053 端口。SmartDNS 还有一个 5335 第二入口,指定使用境外组。AdGuard Home 则是客户端的另一个入口,承担广告过滤和静态重写。
进程、监听和转发关系都正常,说明“服务没有启动”这个假设可以排除了。接下来的问题是:这些服务把同一个问题交给了谁,又把什么答案交还给了客户端?
二、第一层陷阱:建了 china、oversea 两个组,却没有真正分流
原配置里,上游分别标注了 china 和 oversea。这个界面很容易让人产生一种直觉:国内域名已经走国内组,境外域名已经走境外组。
实际生成的配置没有支持这种直觉。
SmartDNS 的上游组、默认组成员,以及域名规则,是几件分别控制的事情。一个服务器有 -group china 标签,并不自动意味着它已经离开默认组。主入口没有域名转发规则时,也不会凭组名里的英文单词推断域名应该归谁回答。

图 3:真实浏览器截图,裁切自本文固定提交版本的 SmartDNS 官方配置文件。参数说明明确区分分组与排除默认组。
可以把它想成食堂打饭:你给两个窗口挂上“普通餐”和“过敏专用餐”的牌子,排队系统却仍把所有同学随机送到任意窗口。牌子写对了,分配规则仍然可能缺失。
这次主入口仍混用了国内外上游。第二入口虽然指定了境外组,但组里的服务器仍通过明文 UDP 连接,直接查询这些上游也会得到异常结果,因此换端口并没有建立新的可信路径。
我采用的策略是:国内域名清单命中时交给 china,其余查询使用经过验证的 oversea;国内组从默认组排除。已有的局域网解析留在本地处理,避免把内部名字送到外部查询。

图 4:这里采用“国内清单命中,其余走可信境外组”的策略。这是一项策略选择,会让少量尚未收录的国内域名使用境外上游,并非零成本的万能分类器。
下面是配置结构示意。占位变量需要由你的私有配置填入,不能原样复制成正式配置:
server-https https://dns.alidns.com/dns-query -group china -exclude-default-group -host-ip <DOMESTIC_DOH_ADDRESS>
server-https https://dns.google/dns-query -group oversea -proxy doh-proxy -host-ip <OVERSEAS_DOH_ADDRESS>
domain-set -name china-domains -file /etc/smartdns/domain-set/china-domains.list
domain-rules /domain-set:china-domains/ -nameserver china
实际部署还配置了另一家国内 DoH 和另一家境外 DoH,保留可用候选。原来的 UDP 上游只被停用,配置记录仍在备份中;其中两个旧节点在两轮独立查询里均超时,没有继续留在活动组里。
国内清单采用维护中的公开项目 dnsmasq-china-list,转换成 SmartDNS 的域名列表格式。现场安装时共 109,797 条,并设置了每周更新。下载失败、格式不符或条目数量明显下降时保留旧文件,替换文件时也保留上一版。更新失败不能顺手把最后一份能用的清单清空。
三、第二层陷阱:DoH 保护传输,仍然需要选择可信的回答者
看到境外 UDP 查询异常,下一步当然会想到 DoH。但我没有把“成功建立 HTTPS”当成修复完成。
在这次现场,通过国内 DoH 查询某些境外域名,仍然拿到了异常地址。这里能确认的是结果存在明显异常、这条上游路径不满足本次需求;仅凭这些查询,不能进一步认定是哪一个中间设备、运营商或解析器内部环节造成了它。

图 5:RFC 8484 原文真实截图。DoH 将 DNS 查询与响应承载在 HTTPS 中,定义的是传输与交互协议。
生活里的比方是密封信封。加密能让路上的人更难拆信、改信,也让你核验自己正在和哪个收信人通信。但收信人如果提供了过时的信息、采用了特殊策略,或者它继续向外问路时遇到了问题,密封信封不会自动把信里的内容改正确。
因此,这里至少需要分开验证三件事:
- 连接是否经过预期路径,证书链和域名校验是否通过。
- 返回的 DNS 业务状态、记录类型和地址是否符合预期,而不只看 HTTP 状态。
- 多个独立可信来源与实际业务访问,是否支持相同结论。
DNS 的 NOERROR 也有类似边界。它表示响应码没有报告对应的 DNS 错误,既不是“地址已经过真实性审计”的印章,也不保证回答区一定包含请求的记录。遇到空回答、SOA、NXDOMAIN 或 SERVFAIL,应继续查看记录类型与上下文,不能全部归为“DNS 坏了”。
经过现场现有代理访问 Google 和 Cloudflare DoH 时,返回结果恢复正常,Google 的 HTTPS 控制请求也返回了 204,证书校验始终开启。至此才找到了可供下一步配置使用的境外解析通道。
国内服务继续使用直接可达的国内 DoH,是为了保留合适的本地 CDN 调度与可用性。境外服务使用实测可信的境外 DoH,是因为当前现场需要这条路径。这个结论来自现场对照,不代表某个品牌在所有网络里都固定更好。
四、第三层陷阱:curl 能通,SmartDNS 却一直 SERVFAIL
这一步最有意思,也最值得记录。
代理上的 DoH 已经用 curl 跑通了,我把同样的服务配置到临时 SmartDNS 实例,国内分流正常,境外查询却失败。正式 DNS 仍保持原配置,问题只发生在临时端口上,因此可以继续查,不必拿全家网络做实验。
对照条件收紧之后,差异出现在代理 CONNECT 的目标形式上:
| 控制条件 | 本次现场结果 |
|---|---|
| 代理收到 DoH 服务的域名目标 | 查询成功 |
| 代理收到对应的数字地址目标 | 连接超时 |
| SmartDNS 原生代理路径 | 境外查询 SERVFAIL |
这不是说 HTTP CONNECT 不能连接数字地址。它完全可以。这里的事实是当前代理路径对这两种目标表现不同;具体原因可能与代理规则、路由或实现有关,没有继续把未核实的内部原因写成结论。
为了确认 SmartDNS 究竟传入了什么,我检查了与现场行为相符的 Release47 源码,并固定到一个可复查的提交,而不是在文章里引用随时变化的分支头。

图 6:真实源码截图。本文所查实现的 client_tls.c 在创建代理连接时使用了 server_info->ip。
关键调用可以缩成下面几行:
proxy = proxy_conn_new(
server_info->proxy_name,
server_info->ip,
server_info->port, 0, 1
);
继续往下看,HTTP 代理握手会把收到的目标拼成 CONNECT 请求。

图 7:同一固定提交的 proxy.c 真实截图。它解释了为什么仅在后一层调整编码方式,无法找回前一层已经没有传下来的域名。
这也解释了为什么我没有把 -use-domain 当成一个随手加上就能解决问题的开关。假设快递员拿到的收件信息里只剩门牌号,后面即使改用“按姓名分类”的信封格式,也无法凭空恢复收件人的名字。
范围必须说清楚:这是现场 SmartDNS 47.1、所检查源码路径与当前代理之间的兼容问题。很多代理本来就能正常处理数字目标,它们不需要本文这个适配层;其他版本也应重新检查实际实现,不应直接套用结论。
五、修复兼容问题:加一个很窄的适配器,让 TLS 完整穿过去
这次没有更换旁路由上的 SmartDNS 程序。我在过滤主机上部署了一个小型 CONNECT 适配服务,把指定 DoH 端点的数字目标转换回对应的公开服务域名,再交给既有 HTTP 代理。

图 8:适配器只处理代理连接目标。DNS 内容仍在客户端与 DoH 提供者之间的 TLS 会话中传输。
它只接受指定来源,只允许两家 DoH 提供者的已配置端点与目标端口,不负责任意网址转发;服务使用 systemd 动态用户运行,并配置开机启动。未授权目标的测试得到了 403。
这里特别保留了证书与域名校验。适配器不生成证书、不解密 DNS,不需要给客户端安装一个新的根证书,也没有使用跳过证书检查的参数。
我还用一个隔离实例模拟了代理不可用:国内域名仍然能通过国内组解析,境外新查询返回失败,没有悄悄交给一个会产生异常结果的替代上游。现场同时保留了受限的过期缓存服务,已缓存的有效结果可以按既定上限继续服务;这和“临时去问另一个不可信回答者”是两种行为。
代价也需要承认:境外新查询现在依赖适配服务和既有代理在线。服务独立启动、来源与目标受限、配置可以回滚,这些措施让依赖更清楚,但不能把依赖变没。如果你的代理原生支持数字 CONNECT,直接使用原生路径通常更简单。
六、AdGuard 的加密入口、上游与引导解析,要分开看
过滤主机上的 AdGuard Home 原来直接使用国内 DoH 上游,因此同样会向客户端返回部分异常结果。另一个需要顺手修复的问题,是该主机的系统解析文件仍指向一个已经停用的解析服务,链接实际失效了。
修复后,AdGuard 的外部查询交给软路由的统一解析入口;它继续保留自己的广告过滤与静态重写。主机系统解析文件也被修复,没有额外启动一个和 AdGuard 争用端口的服务。

图 9:这是本次配置核对的实际数量,不是 AdGuard Home 的默认配置,也不是界面截图。
编辑配置时,三个字段要区分开:
| 字段或设置 | 负责的事情 |
|---|---|
| 对客户端提供加密 DNS 的监听设置 | 客户端如何连接 AdGuard |
upstream_dns |
AdGuard 把外部查询交给谁 |
bootstrap_dns |
遇到上游域名时,先如何取得连接它所需的地址 |
fallback_dns |
正常上游失败时,是否以及向谁尝试替代查询 |
把入站加密监听打开,并不等于出站上游已经加密。只修改 bootstrap,也不等于普通业务查询就会走它。
本次先停掉 AdGuard,再编辑配置,启动后重新比较,确认除 DNS 上游相关字段外,其余设置保持一致。过滤列表、用户规则和重写记录都不能因为“修一个 DNS”就顺手清空。
客户端仍然有两个 DNS 入口,因此四条局域网静态记录也同步到备用入口,避免同一个名字换一个回答者就查不到。广告过滤则仍由 AdGuard 提供:客户端如果直接使用软路由入口,会绕过这一层过滤。双 DNS 提供可用性选择,并不会自动提供相同的过滤策略。
七、为什么上游修好了,客户端还像没修过?
接下来轮到缓存。
原配置同时存在 SmartDNS 的固定一小时 TTL 和 dnsmasq 的一小时最小缓存 TTL,此外还开启了过期缓存服务。这样的组合会让部分结果保留得更久;如果最初进入缓存的就是异常地址,命中率越高,用户反而越容易反复拿到同一份旧答案。

图 10:真实源码配置截图。过期数据能服务多久,与客户端收到的过期应答 TTL,是两个不同的控制项。
这像教室门口贴着一张旧课程表。教务处已经改了新表,门口那张复印件还没被换掉。同学查得再快,也只是更快地读到旧安排。
我移除了强制一小时 TTL 的覆盖,主要遵循上游 TTL,同时给过期服务设置了一小时上限,过期应答的 TTL 设为 30 秒。这个选择是现场对可用性与新鲜度的折中,不代表所有环境都应该使用同一组数值。

图 11:缓存不会判断某个旧答案是不是本次事故里的坏答案,清理与验证需要由运维流程完成。
还有一个容易漏掉的顺序问题:持久缓存文件应在 SmartDNS 停止之后备份或移走。进程退出时可能重新写入内存缓存,如果你先挪文件、再停进程,它就可能把刚才想清掉的旧数据重新写回来。
最后回到客户端,既检查指定服务器的查询,也检查操作系统真正使用的解析路径。浏览器自己的安全 DNS、应用内缓存和代理的远端解析都是独立变量,不能只凭一条指定端口的 dig 就宣布所有客户端已经恢复。
八、三平台自动化套件:先审计,再应用,失败后回滚
为了便于复用,我把这次做法整理成 DNS Care Kit 下载包,提供 Windows 11、Ubuntu 26.04、macOS 27 三个入口。完整源代码、国内域名清单、角色配置样例和回滚逻辑都在包内,也可以先查看 使用说明。
这里的“一键”指一次填好自己的角色参数后,用一条命令执行约定流程。脚本不调用大模型 API,不依赖云端自动化平台,不临时下载远程脚本,也不拉取新镜像;它通过原生 SSH 操作你已经拥有的软路由、过滤主机和代理。DNS 验证当然仍要连接你选择的 DNS 服务,这项网络依赖属于功能本身。
适用前提是本文这类拓扑:dnsmasq 监听 53,SmartDNS 主入口为 6053;过滤主机运行 Ubuntu 与使用绑定配置目录的 AdGuard 容器,已有 Python 3、Docker 和可用的 HTTP CONNECT 代理。macOS 入口使用系统自带的 Bash、plutil 和 OpenSSH,不要求在 Mac 上安装 Python。Ubuntu 入口使用系统 Python 3;Windows 入口使用 PowerShell 和 Windows OpenSSH 客户端。
方法一:由人启动,脚本自动执行
解压后复制 settings.example.json 为 settings.private.json,只在自己的设备上填写 SSH 别名、设备地址、代理地址,以及四个 DoH 上游和可信对照端点。上游可以使用自建服务;套件的对照查询采用标准 DoH 报文,不绑定厂商专用 API。不要把真实配置上传到工单、文章评论或公共仓库。SSH 应事先能正常登录,首次主机指纹核验也应在使用套件前完成。
默认只读审计:
# Windows 11:进程级执行参数,不修改系统全局执行策略
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\windows-11.ps1 -Mode Audit
# Ubuntu 26.04
bash ./ubuntu-26.04.sh audit
# macOS 27
bash ./macos-27.command audit
确认符合该配置模型后,切换到应用模式。脚本会先验证可信 DoH 通道并准备端点信息,再安装受限适配器、备份并修复 SmartDNS,检查软路由后才调整 AdGuard:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\windows-11.ps1 -Mode Apply
bash ./ubuntu-26.04.sh apply
# 或
bash ./macos-27.command apply
失败时,入口会尝试恢复两端原配置,并打印本次运行编号。网络已经断开的情况下,任何自动回滚都无法保证远程执行成功,所以包内也保留显式回滚入口:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\windows-11.ps1 -Mode Rollback -RunId RUN_ID
bash ./ubuntu-26.04.sh rollback RUN_ID
bash ./macos-27.command rollback RUN_ID
套件的精简检查包含 28 项协议与记录检查,并将 Google 回答与可信 DoH 的地址前缀做一致性核对。前缀比较只是一条排障线索,会受 CDN 调度影响,不能当成 DNSSEC 验证或域名真实性的最终证明。现场更完整的验收是后文的 56 项,二者不要混为一谈。
测试口径也明确列在包内:macOS 27、Ubuntu 26.04 的只读链路均已实机运行并通过 28 项检查;Windows 入口经过 PowerShell 解析及四种流程模拟检查,不把 Linux 上的 PowerShell 检查写成完整 Windows 11 实机认证。Apply 是针对上述双主机拓扑的参数化实现,应先审阅,不能用于盲目覆盖任意 OpenWrt 或任意代理配置。
方法二:让已有 Agent 自动配置
如果你已经在使用本地运维 Agent,可以把解压后的目录交给它。这里不要求某一家 Agent 产品,也不要求购买某个在线服务。以下提示词可以直接作为任务说明:
你已被授权修复 settings.private.json 指定的两台设备的 DNS。
先阅读本地套件 README 和源代码,识别当前系统,运行对应的 Audit 入口。
确认实际拓扑、可信 DoH 对照、代理连接方式与该配置模型一致后,运行 Apply。
应用前保留备份,应用后验证两端的 UDP/TCP、A/AAAA、负例和过滤行为。
不要更改 DHCP、网关、SSH 认证、代理订阅、用户过滤规则或静态重写。
不要关闭 TLS 证书校验,不要把失败的境外查询转给未经验证的上游。
如果拓扑不同,保留诊断结果并说明差异,不执行不适用的配置步骤。
失败时执行本次运行编号对应的回滚,并核对恢复状态。
私有配置只留在本地;交付报告仅使用角色名、计数和脱敏结果。
真正有价值的 Agent 自动化,是把检查、变更、验收和回滚连起来。让它反复猜一个“万能 DNS 地址”,并不能减少链路里的不确定性。
九、怎样才算修完?把“能回答”升级为“证据闭环”
最终没有以服务 running 作为收工标准。我分别检查了软路由的 53、6053、5335,以及过滤主机的 53,覆盖国内外域名、A/AAAA、UDP/TCP、TXT、HTTPS 类型与不存在的域名。
原来的 HTTPS 记录抑制策略得到保留,因此对应测试关注其响应是否符合现有策略;这和网页 HTTPS 连接是否成功是不同测试。

图 12:重启相关服务后,56 项解析检查全部通过。中位数包含缓存命中,仅代表本次快照,不是不同 DNS 品牌的性能排名。
另外完成了八次局域网静态记录检查、广告过滤检查、代理失效场景与客户端系统解析检查。Google 的 IPv4 与 IPv6 回答恢复正常,旧的异常结果不再出现。AdGuard 的原有数据也重新核对过,临时 SmartDNS 实例全部停止,正式进程只留下一个。
这里没有执行整机重启。已确认的是服务重启后的实际行为,以及适配服务和原有 DNS 服务的启动配置;整机启动的先后顺序,仍可以在合适的维护窗口再做断电或重启验收。把已经验证的范围说清楚,比补一句未经执行的“重启也没问题”更有用。
Q&A:几个容易再次绕回去的问题
只配置一个公共 DNS,会不会更省事?
对于简单网络,当然可能。前提仍是它在你的网络里可达,并能满足实际解析需求。多加十个服务器不会自动增加可信度,还会增加竞争回答、延迟和排障变量。
多个 DNS 地址一定严格按主备顺序工作吗?
不应这样假设。操作系统、应用、浏览器和当前网络状态都可能影响选择。如果两个入口的分流与过滤策略不同,用户体验也可能随入口变化。本次统一了上游分流与本地静态记录,仍明确保留 AdGuard 独有的过滤层。
使用 DoH 后还需要 DNSSEC 吗?
它们解决不同层面的问题。DoH 保护与所选解析器的连接;DNSSEC 在域名已正确签名且验证链完整时,为 DNS 数据提供可验证性。不是所有域名都签名,也不能因为启用了其中一个就省略其他检查。
发现异常后,先清缓存是否最快?
清掉缓存后,如果仍向原来的异常路径提问,新缓存很快又会装入同样的问题。先建立可信上游和正确分流,再清缓存、做客户端复测,通常更容易解释每一步的效果。
每个人都需要这个 CONNECT 适配器吗?
不需要。先在自己的网络对比域名目标与数字目标。原生代理路径正常就继续使用它。这里新增一个服务,是为了处理有证据支持的特定兼容问题,并非推荐给所有家庭网络的标准组件。
DNS 修好后,所有境外网站都会自动直连成功吗?
不会。正确解析只是建立连接的一个步骤。路由、代理规则、TLS、HTTP、账号和应用自身仍各有条件。本文修复的是解析链路,没有把 DNS 描述成解决所有网络问题的开关。
参考资料与配图来源
本文的五张真实截图均由浏览器打开官方原文后截取、裁切;其余七张为本文原创示意图,并提供中英文配图。截图没有用绘制的终端窗口冒充现场输出。
- SmartDNS 固定提交:TLS 创建代理连接的位置
- 同一提交:HTTP CONNECT 目标构造
- 同一提交:上游分组与过期缓存配置说明
- RFC 8484:DNS Queries over HTTPS
- RFC 8767:Serving Stale Data to Improve DNS Resiliency
- AdGuard Home 当前配置知识库
- dnsmasq-china-list 项目
相关旧文也可以一起读:DNS 的演进与现代加固、家庭网络 Bufferbloat 排障。这几次排障反复提醒我的,是同一件很具体的事:看见一个漂亮指标之后,再沿着真实请求走一遍。