VPN 终止开关封锁 53 端口后 DNS 无法解析
终止开关可能故意封锁 DNS。先比较只在隧道内解析与为本地解析器开例外这两种做法,再判断究竟是哪一层出了问题。
当终止开关切断隧道时,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 各有各的开关和默认值。关掉它们比追着解析器地址跑更可靠,因为地址列表会变,而你的规则集不会变。
还有一种情况根本与泄漏无关。固定了解析器的应用仍然需要一条网络路径才能到达它。加密改变的是谁能读到查询,而不是数据包能不能离开这台机器,所以整机封锁不用碰这个设置就能让它失效。
把症状对应到层
大多数报告来到你面前只有一句话,而那句话通常已经点出了层。下面这张表是我在动任何规则之前先过一遍的东西;要点是判断查询是被封了、被指错了,还是根本没发出去。
| Symptom | Likely layer | First check |
|---|---|---|
| Every name fails at once, instantly | Resolver answering with a refusal | Query the resolver by address, not by name |
| Lookups hang for seconds, then time out | Packets dropped with no response | Watch port 53 on the physical link |
| Public names resolve, internal ones do not | Split-DNS routing domain | Per-link DNS servers on the tunnel |
| Names resolve, the page still fails | Not DNS: routing or MTU | Connect to the IP directly |
| Only one application fails | Pinned resolver or cached answer | Check 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 要么被封锁覆盖,要么被策略关闭。
- 隧道消失后,专用区域是失败关闭,而不是失败打开。