当终止开关切断隧道时,DNS 通道也随之消失,而留下来的解析器往往并不是你预期的那一个。两种设计对这一刻的处理截然不同:封锁所有不在隧道内的 DNS 数据包,或者 保留一个解析器作为例外。两者都站得住脚。在浏览器标签页里看,症状几乎一模一样,所以第一件事是弄清楚你实际部署的是哪一种设计。

我始终把这条区别放在眼前,因为两种情形下的修法正好相反。一种设计需要更宽的例外,另一种需要更窄的例外。靠猜,代价要么是隐私,要么是一下午。

同一故障的两种 DNS 设计

设计 A 把解析器当作隧道后面的普通服务。防火墙规则只允许在隧道接口上放行 UDP 和 TCP 53,发给客户端的地址是隧道内的解析器。隧道断掉时查询就停止,这是预期结果,而不是故障。

设计 B 在本地网络上保留一个可达的解析器,通常是 192.168.1.1 的路由器,或 127.0.0.53 的系统 stub,理由是名称解析不该随隧道一起死掉。在家用 LAN 里这很方便,路由器为全屋解析。但在酒店或机场 Wi-Fi 上,同一个例外指向的是别人的服务器,你输入的每个名字都会被暴露,即使网页流量仍被封锁。

分界线在于解析器归谁所有。如果你信任本地那一个,例外就是一个小而诚实的洞;如果不信任,那就只有纯隧道设计站得住,代价是隧道一断,所有名字都跟着消失。

为什么 VPN 客户端自己的查询还能工作

客户端必须在隧道存在之前解析网关主机名,所以严格的封锁不能覆盖客户端的启动路径。大多数客户端要么缓存端点地址,要么为这唯一一台主机提供有文档的启动例外。如果你的规则集封掉了那次查询,隧道就再也回不来,日志会显示一个连自己服务器都到不了的客户端——这是策略缺陷,不是 DNS 缺陷。

拆分 DNS 是这件事出现的第二个地方。客户端把内部区域推给专用解析器,其余一切交给系统解析器。断开之后,公网名字可能还能解析,内部名字却会超时,因为专用区域已经没有路由了。对用户来说这是「VPN 把网弄坏了」,对解析器日志来说这是一个从未到达的查询。

在 systemd-resolved 下可以直接看到这种拆分。resolvectl status 会列出每个链路的 DNS 服务器和路由域;隧道断开后,内部域仍然指向一个已不存在链路上的服务器。127.0.0.53 的 stub 仍会接收查询,然后等待一个不会应答的上游,这就是为什么故障要等上几秒,而不是立刻返回。

加密 DNS 绕不过数据包过滤器

DNS over HTTPS 和 DNS over TLS 走的是 443 和 853,所以一条封锁 53 端口的规则对它们毫无约束。按端口封锁「DNS」是针对端口的策略,而不是针对名称解析的策略。如果你的终止开关封锁隧道外的全部流量,DoH 只是碰巧被覆盖;如果你为强制门户或公共解析器留了口子,那就没被覆盖。

客户端侧的控制是更省力的杠杆。当金丝雀域名 use-application-dns.net 无法解析时,Firefox 会放弃自己的 DoH 解析器,这就是受管网络把该域发布为 NXDOMAIN 的原因;企业策略也可以把它固定为关闭。Android Private DNS 和 Chrome 的安全 DNS 各有各的开关和默认值。关掉它们比追着解析器地址跑更可靠,因为地址列表会变,而你的规则集不会变。

还有一种情况根本与泄漏无关。固定了解析器的应用仍然需要一条网络路径才能到达它。加密改变的是谁能读到查询,而不是数据包能不能离开这台机器,所以整机封锁不用碰这个设置就能让它失效。

把症状对应到层

大多数报告来到你面前只有一句话,而那句话通常已经点出了层。下面这张表是我在动任何规则之前先过一遍的东西;要点是判断查询是被封了、被指错了,还是根本没发出去。

SymptomLikely layerFirst check
Every name fails at once, instantlyResolver answering with a refusalQuery the resolver by address, not by name
Lookups hang for seconds, then time outPackets dropped with no responseWatch port 53 on the physical link
Public names resolve, internal ones do notSplit-DNS routing domainPer-link DNS servers on the tunnel
Names resolve, the page still failsNot DNS: routing or MTUConnect to the IP directly
Only one application failsPinned resolver or cached answerCheck that app's DNS setting

前两行正是人们最容易误判的。静默丢弃和拒绝在浏览器里看起来都像「DNS 坏了」,但只有一种意味着你的终止开关在正常工作。拒绝意味着有东西应答了,所以封锁不在你以为的地方。

先恢复解析器策略,再证明它

要修的是设计,不是症状。对于只在隧道内解析的方案,在隧道接口上放行 53,把内部解析器随隧道配置一起下发,并让物理链路不带任何 DNS 许可。在 wg-quick 里,把解析器写进接口配置的就是 DNS = 这一行,所以省略它的隧道会把旧解析器留在原地,每个查询都走在错误的路径上。wg-quick 手册 按平台记录了这种行为。

然后按这个顺序验证。在 Linux 上,resolvectl status 会显示当前生效的链路,dig +short example.com 测试公网名字,dig @10.8.0.1 internal.example.com 测试内部名字,而 sudo tcpdump -ni any port 53 会在你同时运行两者时显示数据包走的是哪个接口。在 Windows 上,Get-DnsClientServerAddress -AddressFamily IPv4 和 Resolve-DnsName example.com 给出同样两个事实:谁被配置了,以及谁在应答。

最后做反向测试。断开隧道,确认一个全新的公网名字失败,确认内部名字失败,并 确认没有任何 53 端口的数据包离开物理网卡。只在缓存答案下成立的封锁,等于根本没有成立。

关闭工单前的检查清单:

  • 故障期间使用的解析器正是策略所指定的那一个。
  • 客户端自己的端点查询仍然被允许,无论是靠缓存还是靠例外。
  • DoH 和 DoT 要么被封锁覆盖,要么被策略关闭。
  • 隧道消失后,专用区域是失败关闭,而不是失败打开。

相关文章