Apple VPN On Demand 的域名规则如何触发隧道
域名规则只决定隧道何时打开。connect-if-needed 的 DNS 探测如何工作、为什么路由仍在选择流量,以及如何测试描述文件。
当你请求一个内网名称时,隧道会打开;而下一个应用同样需要时,它却保持关闭。这不是应用的 bug,而是域名规则的形态。Apple 的 on-demand 配置描述的是建立连接的条件,而这些条件的域名形态会在任何一个数据包发出之前向系统解析器提出一个问题。有两个问题一直被混为一谈:隧道应该在何时拉起,以及隧道存在之后承载什么。前者是触发器,后者是路由与解析器策略,而同一个描述文件通常把两者放进同一个 payload,所以一条正确的域名规则仍然可能让你待在错误的网络里。
触发器与流量选择位于不同的层
on-demand 规则存在于 VPN payload 的 on-demand 字典中:一个有序的规则列表,每条规则都有一个动作和一组匹配条件。系统在连接状态变化时、或有东西试图访问网络时对它们求值,动作决定隧道是被拉起、被评估、被断开还是被忽略。这一切都发生在平台的网络层,早于应用拿到套接字。
接下来是另一组独立的键:隧道的 IPv4 与 IPv6 设置、它包含与排除的路由、DNS 服务器地址,以及在 iOS 和 iPadOS 上决定哪些应用能使用隧道的按应用映射。一个描述文件可以把宽泛的触发器配窄路由,也可以把窄触发器配默认路由。只读一半,另一半看起来就像故障。
我有一个实验用描述文件:只要请求内网域名下的任何东西它就会连接,但只把一条 /16 送进隧道。它完全按自己的 payload 行事,而这也正是最容易产生工单的那类描述文件:人们以为隧道连上了,流量就在隧道里。
域名规则究竟向解析器问了什么
规则的域名形态常被称为 connect-if-needed,它不检查数据包 payload,也不监视 URL。当一个名称被解析时,系统把它与规则中的域名比对,命中后询问这个名字能否通过隧道提供的 DNS 服务器解析。如果回答来自隧道的解析器,这个名字就算内网名称,连接随即开始。如果回答来自本地解析器,这个名字就被当作外部名称,隧道不会出现。Apple 的部署概览把这一过程描述为网络探测加上随后的连接评估。
由此产生两个后果。描述文件必须在隧道内指定 DNS 服务器;没有隧道解析器的域名规则没有可比对的对象,评估也就无法给出你期待的命中。而且查询必须到达系统解析器。一个运行自家加密 DNS 的应用或浏览器、一个在本地应答的扩展,或者直连 IP 地址的连接,都永远不会产生规则所等待的名称解析结果。我经历过的最漫长的一个下午,最后发现隧道前面的客户端用的是 DNS over HTTPS。
缓存同样会消掉触发器。一个已经在缓存里的名称可能不经探测就被解析,于是昨天还好用的规则在重启之后、或隧道挂了一段时间之后失效。在 macOS 上用 sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder 重置状态;在 iOS 上切换飞行模式或重启设备能得到相当的干净起点。
匹配范围:名称、应用与网络
域名匹配只是若干类型之一。规则还可以匹配接口类型(Wi-Fi、蜂窝、以太网)和特定 SSID,而按应用或 Safari 域名的条目把隧道绑定到某一条工作流。这种绑定解释了一种奇怪体验:只有一个应用和其他所有应用表现不同——其他应用在隧道范围之外,所以即使隧道图标显示已连接,它们仍然走本地路径。
顺序比人们预期的更重要。规则按顺序求值,第一条命中的给出动作,所以放在窄域名规则上方的宽泛“连上任何 Wi-Fi”规则会悄悄胜出,让域名规则变得无法测试。在重写域名列表之前,先确认没有更早的规则已经命中。
这也是为什么 always-on 并不只是一条更严格的 on-demand 规则。强制执行 always-on 是独立的部署模式,有自己的监管与管理要求;它让隧道保持打开,而不是对名称作出反应。如果你需要的是一条永不掉线的隧道,域名规则就是错的工具。
伪装成规则损坏的故障模式
关于域名规则不生效的报告,大多落在五个地方之一,而只有最后一个才是规则文本本身。
| What you see | Layer | First check |
|---|---|---|
| Tunnel comes up, internal site still fails | route or resolver after connect | netstat -rn, scutil --dns |
| Tunnel never comes up for that name | trigger path | fresh lookup, cache flush, private DNS |
| Tunnel comes up for every app | scope, or an earlier rule | rule order, per-app mapping |
| Tunnel drops and will not return on the same Wi-Fi | interface or SSID match, admission | captive portal, SSID string, auth prompt |
| Tunnel returns after you disconnect it | profile ownership | profiles show |
强制门户值得单独一行,因为酒店和机场的 Wi-Fi 拦截的正是规则所依赖的那次查询。在门户放行设备之前,隧道的 DNS 服务器不可达,探测失败,而失败看起来像一个拼错的域名。认证是另一个安静的情况:规则可能触发,隧道可能开始连接,连接却死在过期证书上,或死在另一个屏幕上弹出的提示上。
IPv6 制造了一种很难正确诊断的变体。一个同时拥有两类地址的名称可能先从 AAAA 记录得到应答,而隧道的路由列表只包含 IPv4。一个浏览器能用,另一个不能用,域名列表却完全无辜。
用真实请求按这个顺序测试
先读策略再猜。在 macOS 上,sudo profiles show -type configuration 打印已安装的描述文件,包括 VPN payload;scutil --nc list 显示服务状态;scutil --nc show <service> 打印系统实际使用的 on-demand 字典。在受管理的 iPhone 或 iPad 上,同一份 payload 来自管理控制台或 MDM 报告。网络日志在 macOS 上可用 log stream --predicate 'subsystem == "com.apple.networkextension"',手机连接时也可通过设备控制台获取。
然后用本次会话中尚未查询过的主机名测试。在隧道的 DNS 区域里挑一个你确知会内网解析的名称,请求一次,看隧道是否出现。把同一次查询跑两遍:第二次走缓存路径,两者之间的差别会告诉你触发器是解析还是路由。
隧道拉起后,分别验证两层。netstat -rn -f inet 和 netstat -rn -f inet6 显示每个前缀指向哪里,scutil --dns 显示解析器顺序以及每个解析器属于哪个接口,nettop -m route 显示哪个接口承载了字节。如果隧道已拉起、解析器也正确,资源却仍然失败,就别再改规则了;去查服务器侧授权和回程路由。
当规则属于别人
如果你断开之后隧道又出现,规则通常属于某个描述文件而不是某个应用:通过 MDM 下发的配置、残留的个人描述文件,或另一个客户端安装的描述文件。sudo profiles show -type configuration 加上搜索 on-demand 键能把它缩小,而 移除的路径是描述文件,不是菜单栏里的开关。删掉服务条目或应用会留下规则,下一次安装又会把它继承回来。
一个测试就能判定归属:断开、等待、再次请求内网名称。如果隧道回来了,触发器就是按设计工作,剩下的问题是它在谁的管辖之下,以及那个范围是不是你想要的。
在把它报成 bug 之前,先走同样的五项检查:payload 里存在隧道的解析器地址、名称没有被缓存、没有更早的规则已经命中、AAAA 路径被路由列表覆盖、应用在隧道范围之内。五项都成立时,规则在做它该做的事,而故障在路径更远的地方。