scutil --dns 打印的是 macOS 真正会查询的解析器配置,而不是你在设置面板里填的那一台 DNS 服务器。输出比大多数人预想的要长,因为系统会为默认路径、带作用域的接口以及按域名转发分别维护解析器区块。输出最上面的区块未必就是回答查询的那一个。如果某个内网名称在办公室能解析,一接上 VPN 就失败,问题出在匹配规则和路由上,而不是服务器地址。先读顺序,再用一次真实查询来证明答案。

先看区块顺序,再看地址

每个条目以 resolver #N 开头,并列出 nameserver[0]、有时还有 if_index、一行 flags、reach,以及当该区块不是普通默认值时出现的 domain 和 search domain 列表。不带域名的 1 号解析器就是处理公共名称的默认解析器。凡是在标志里带 Scoped 的区块,都绑定到 if_index 指定的接口;凡是被标记为 Supplemental 的,只对列在其下的域名生效。

选择靠后缀匹配。对于 intranet.corp.example 这样的名称,macOS 偏好 domain 条目为最长匹配后缀的解析器,所以带有 domain : corp.example 的区块即便排在输出很靠后的位置,也能胜过默认值。长度相同的域名之间由 order 决定优先级。第一遍通常不需要更多:想完整读完一个区块就看全部输出,想先弄清区块顺序就看开头几行,想看字段行旁边的区块标题就用过滤输出。带作用域的区块出现在自己的标题之下,读起来像是上方列表的延续,所以引用输出时务必让标题留在视野里。

scutil --dns
scutil --dns | sed -n '1,45p'
scutil --dns | grep -nE '^resolver #|^DNS configuration'
scutil --dns | grep -nE 'resolver #[0-9]+|if_index|domain|flags|nameserver'

把 if_index 对应到隧道接口

索引只有在你能说出它背后的接口时才有用。scutil --nwi 列出系统保存的网络信息,包括主接口和可达性;ifconfig -l 则给出名称,其中 utun0 到 utun9 是客户端通常创建的隧道候选。Apple 把 scutil --dns 当作自己的检查示例,写在 部署指南 里,并说明了 Network Extension 如何把选定域名路由到选定解析器,而这些带作用域的区块暴露的正是这一机制。

真正定论的那项检查是:解析器是否位于隧道装上的那条路径上:

scutil --nwi
ifconfig utun4
route -n get 10.20.0.53
netstat -rn | grep -m5 utun

如果 route -n get 返回的网关和接口与解析器条目对不上,地址可能完全正确却依然不可达。只承载公司子网的拆分隧道会丢弃指向其外 DNS 服务器的查询,故障表现为超时,所以配置输出看起来一直很干净。

先查系统解析器,再查服务器

三个工具回答三个不同的问题,把它们混为一谈正是这一领域大多数错误结论的来源。dscacheutil -q host -a name 走的是普通应用所用的同一套系统解析器,因此带作用域和补充区块都会生效。不带服务器参数的 dig 读取 /etc/resolv.conf,而后者并不是 macOS 上带作用域解析的真实来源,它会完全跳过按域名的规则。dig @address 只到达一台固定的服务器,对选择过程一无所知。

dscacheutil -q host -a name intranet.corp.example
dig intranet.corp.example
dig @10.20.0.53 intranet.corp.example
nslookup intranet.corp.example

当第一条命令有应答、第二条返回 NXDOMAIN 时,你找到了一直在找的那种拆分,而且什么都没坏。当第一条超时、而 dig @10.20.0.53 却有应答时,说明解析器本身正常,只是在隧道选定的路径上不可达,所以下一步是 route -n get,而不是去改 DNS 设置。不要把内网名称写进任何要粘贴到工单或共享笔记里的内容。

输出无法告诉你的部分

解析器列表之上或旁边还叠着好几层,每一层都可能把查询发到输出从未提到的地方。开启了安全 DNS 的浏览器可能走自己的加密通道解析,所以 dscacheutil 超时的时候页面仍能加载并不矛盾。安全软件安装的 DNS 代理以 Network Extension 提供程序的方式运行,由它自己转发查询,而 scutil --dns 显示的是配置了什么,而不是代理最终使用的上游地址。搜索域名又添了第三重麻烦:对于不完整的名称,会按顺序追加列出的每一个后缀,于是应答的那个区块可能是你从未想到的。

过期应答最省事就能排除,也最常被跳过:

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
scutil --dns | head -20

如果刷新之后输出变了,说明状态曾被缓存,之前的输出描述的是一个已不存在的隧道。如果没变,配置就是这样,问题就该转向路由和应用策略。

一份用于前后对比的简短手册

只在策略允许的地方采集基线,并把文件放在任何同步文件夹之外。下面这套顺序就是我在客户机上执行的:采集基线、连接、再采集一次,然后比对并查询。

scutil --dns > ~/dns-before.txt
# connect the VPN, then:
scutil --dns > ~/dns-after.txt
diff -u ~/dns-before.txt ~/dns-after.txt
dscacheutil -q host -a name intranet.corp.example

按区块而不是按行看差异。新出现的带作用域解析器带有隧道接口索引,并为内网后缀带一条 domain 条目,这就是预期形态。新出现的默认解析器指向公网地址,则是值得向客户端配置文件所有者上报的发现。然后在真正失败的那个应用里重做查询,而不只是在外壳里查,并把 route -n get 得到的接口记在结果旁边。

要撤销改动,就断开连接并重做基线采集。如果带作用域的区块不见了,说明客户端自己清理干净了。如果该区块仍列在一个已不存在的接口上,状态就已过期,在任何人动配置文件之前,刷新并重新连接是最温和的修法。

按这个顺序做四项检查:是否有解析器区块匹配名称最长的后缀;该区块是否绑定在隧道装上的接口上;到其名称服务器的路由是否走隧道;以及返回答案的是系统解析器而不是 dig。第一项失败的检查会告诉你该改 DNS 策略、隧道路由还是应用。如果四项都通过而名称仍然失败,问题就在 DNS 之上,接下来该查的是在它前面应答 53 端口的所有东西。

相关文章