为什么内部主机名在 dig 中能解析,在 VPN 上的 curl 中却失败
dig 能应答,curl 却报告无法解析该主机。比较两条查询链,然后修复 nsswitch、按链路划分的 DNS 或错误的搜索后缀。
你的笔记本电脑已连上 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 一侧的做法。
判断该修哪一层
从症状走向层次,而不是按顺序试各种修法:
| Symptom | What it means | Check first |
|---|---|---|
| dig answers, getent fails | lookup chain, not the packet path | nsswitch line and resolvectl status |
| Timeout rather than NXDOMAIN | private resolver unreachable through the tunnel | resolvectl dns tun0, route to that address |
| NXDOMAIN | an authority answered and does not know the name | zone owner, endpoint region |
| FQDN works, short name fails | search suffix mismatch | resolvectl domain on the tun link |
| getent works, app fails | app cache or its own resolver | restart 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 覆盖。
只修第一处失败的层次,然后重新执行同样的命令。一次改两处,你就说不清是哪一处起了作用,而下一次故障又得从零开始排查。