你的笔记本电脑已连上 VPN,dig service.internal.example 返回 10.x 地址,curl 却仍然以 Could not resolve host 退出。隧道不是问题所在:它毫无怨言地把 dig 查询送了出去。差别在于提问的是谁。dig 向你自己指定的单个服务器发出一条查询,而 curl 走的是操作系统的查找链,这条链对“该问哪台服务器”有自己的规则。

同一个名字,两条查询路径

dig 自带 stub resolver。它读取 /etc/resolv.conf,挑一个 nameserver,发出单条 A 或 AAAA 查询并打印应答。它完全跳过 nsswitch,也不知道 systemd-resolved 或 NetworkManager 在那个文件之外维护的按链路状态,所以 dig 的应答可能正确,却仍然说明不了你的应用会看到什么。

应用程序调用 getaddrinfo,而 getaddrinfo 走的是更长的链:先是 nsswitch 的模块顺序,然后是处理该名字的模块,最后是由当前链路配置构建的 resolver。在 systemd 机器上,查询发往 127.0.0.53,systemd-resolved 依据绑定在 tun 接口上的路由域,为每条链路挑选上游服务器。

两条命令无需猜测就能区分这些层次:

getent ahosts service.internal.example
resolvectl query service.internal.example

getent 走的是与 curl 相同的路径,因此在 dig 正常时 getent 失败,就把故障定位在查找策略而不是数据包路由上。resolvectl query 还会打印给出应答的接口与上游,由此可以看出是隧道链路还是物理链路赢了这次决策。

读一读应用实际使用的 resolver

大多数桌面上的 /etc/resolv.conf 是自动生成的,VPN 客户端与它有三种交互方式:前置一个 nameserver、直接覆盖该文件,或把自己的服务器交给 NetworkManager 让守护进程重写。只有第三种方式保留了按链路 DNS,因为服务器始终挂在某个接口上,而不是被压平成一张全局列表。

在 NetworkManager 系统上,结果取决于 /etc/NetworkManager/NetworkManager.conf 里的 backend:

[main]
dns=systemd-resolved

使用 dns=default 时,客户端带来的服务器会以取胜的顺序并入 resolv.conf,私有查询仍可能被送到公共 resolver。使用真正理解链路的 backend,systemd-resolved 或 dnsmasq,隧道的服务器就只作用于隧道,只有匹配其路由域的名字才会发到那里。这种作用域正是 split DNS 在实践中的含义,也是客户端声称支持它时最常缺失的一环。

改动任何东西之前,先读出当前状态:

resolvectl status
resolvectl dns tun0

你希望看到 tun 接口下列出了 DNS 服务器和针对私有后缀的路由域,显示为 ~internal.example。链路上没有服务器,说明客户端什么都没推送。有服务器却没有域,说明查询要么全部发往那里、要么一条都不发,取决于客户端,而这两种都不是 split DNS。

nsswitch 在哪里提前结束了查找

桌面上的默认行大致是这样的:

hosts: files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns myhostname

mdns4_minimal 负责 multicast DNS,并声称对一切以 .local 结尾的名字拥有管辖权。像 api.infra.svc.cluster.local 这样的 Kubernetes 服务同样以 .local 结尾。当 mDNS 模块报告 NOTFOUND 时,动作 [NOTFOUND=return] 会结束整条查找,而不是继续交给 dns 模块,于是这个名字永远不会被送到 VPN 的 resolver。dig 完全绕开这个文件,这正是两种工具结论不一致的原因。

这是这个问题里最尖锐的对比,它归根结底取决于一个比你 VPN 还老的配置文件里的一个词。把动作改成 [NOTFOUND=continue],既能保住打印机和本地设备的 mDNS,又能让 DNS 看到这个名字。把该行换成 hosts: files dns 同样可行,但更粗暴:你会失去实际在用的那台打印机的 .local 解析。

只存在于单一网络中的名字

私有区域只在一个地方具有权威性。像 ip-10-0-4-31.eu-west-1.compute.internal 这样的 AWS 名字,经由该区域的 VPC resolver 应答,而且只在你的 VPN 端点与资源处于同一区域时才成立。连到法兰克福端点却去查爱尔兰的 compute.internal 名字,任何本地设置都救不了:持有该记录的区域从那里根本不可达。

搜索域带来的则是镜像问题。裸名 service 会被补上 resolver 认为适用的后缀,而在 VPN 链路上,本地域可能压过公司域。带结尾点去查询就能消除猜测,getent ahosts service.internal.example.,如果这样能通而短名不行,要修的是隧道上的搜索域,而不是服务器地址。

受管客户端在系统 resolver 之上再加一层。Windows VPN 配置文件可以自带名称解析策略、按应用的规则,以及一张在隧道断开时仍然生效的 NRPT 表。自带 DNS 缓存的应用(在 .NET 和 Java 运行时里很常见)会在链路修好很久之后仍保留旧的失败结果。Apple 客户端通过 DNS 设置里的匹配域来配置 split DNS,而不是通过 resolv.conf;Chromium 或 Firefox 在加密 DNS 模式下可能绕过系统 resolver 去解析公共名字。Microsoft 的 VPN 名称解析参考描述了这层策略在 Windows 一侧的做法。

判断该修哪一层

从症状走向层次,而不是按顺序试各种修法:

SymptomWhat it meansCheck first
dig answers, getent failslookup chain, not the packet pathnsswitch line and resolvectl status
Timeout rather than NXDOMAINprivate resolver unreachable through the tunnelresolvectl dns tun0, route to that address
NXDOMAINan authority answered and does not know the namezone owner, endpoint region
FQDN works, short name failssearch suffix mismatchresolvectl domain on the tun link
getent works, app failsapp cache or its own resolverrestart the app, check secure DNS

有两行承担了大部分分量。超时是可达性问题:私有 resolver 没有经由隧道应答,所以要检查通往其地址的路由,以及链路上的 DNS 服务器是不是你以为的那一台。NXDOMAIN 则是权威性问题:有东西应答了,但它不认识这个名字,这指向区域、地域,或一台本不该收到该查询的 resolver。

最后一行最容易被忽略,因为两半看起来都像成功。短名与 FQDN 解析结果不一致,就是后缀不匹配,而重连隧道并不会改变客户端推送的后缀。

在编辑配置文件之前先把状态存下来,这样你才能说清改动到底有没有起作用:

resolvectl status > before.txt
getent ahosts service.internal.example >> before.txt

按这个顺序做检查,并在第一处失败处停下。getent 与 resolvectl query 必须一致;如果 getent 失败而 dig 能应答,就留在查找链里,别去动路由。resolvectl status 必须在 tun 接口上显示 DNS 服务器和路由域。nsswitch 不应对你正在测试的后缀在 NOTFOUND 时提前返回。名字必须在你端点所连的网络中具有权威性,并用结尾点验证。应用不能持有自己的缓存或加密 DNS 覆盖。

只修第一处失败的层次,然后重新执行同样的命令。一次改两处,你就说不清是哪一处起了作用,而下一次故障又得从零开始排查。

相关文章