Mac 上额外的 VPN 路由需要所有者,而不是登录脚本
登录时添加一次的路由不是重连策略。给远端前缀一个所有者,并决定它们如何随那条连接出现、变化和消失。
通过 VPN 到达一个私有前缀,是一项有期限的要求:它在那条连接可用期间成立,并在连接结束时结束。登录脚本无法表达这一点,因为登录和隧道建立是两个不同的时刻,而登录时存在的隧道未必是一分钟后承载流量的那一条。首先要决定的不是运行哪条命令,而是哪个组件拥有这个前缀。
前缀需要所有者,而不是某个时刻
先把要求写成带主语的句子:在办公 VPN 处于活动状态时,经由 10.77.0.0/24 到达。这句话点明了前缀、连接和条件,并立刻排除了两种常见的替代方案:登录挂钩,以及像 ppp0 或 utun2。
接口名是运行时分配的,在重连之间并不稳定。匹配上周所见名称的挂钩,要么根本不触发,要么作用到别人的接口上。两种结果都表现为路由故障,而不是它们真正是的过时假设。同样的反对理由适用于任何依赖某个只在特定隧道活动期间才存在的名称的脚本。
使用受支持的路由意图来源
网关、受管理配置文件或拥有方客户端有文档记载的路由机制,才是这件事该在的地方——只要该机制能够表达这项要求。适用哪一种取决于协议和提供商,而键值不会在两者之间通用,所以一个用来配置 OpenVPN 配置文件的值,丝毫说明不了原生 L2TP 或 IKEv2 客户端的情况。
| Mechanism | Can carry extra prefixes | Who owns the lifetime |
|---|---|---|
| Gateway-pushed routes | Often, if the gateway is configured for it | The connection |
| Managed profile settings | Depends on the client and profile format | The profile |
| Provider tunnel settings | Yes, as included and excluded routes | The provider process |
| Manual command | Yes, once | Nobody |
对数据包隧道开发者来说,Apple 的 IPv4 隧道设置明确公开了包含路由和排除路由,IPv6 也有对应设置。这些是提供商使用的 API,并不能证明每个原生 VPN 面板都会向管理员提供任意路由列表。如果当前组合无法表达你需要的前缀,下一步应当是运营商支持的配置途径,而不是抱着它会表现更好的希望去安装另一个应用。
生命周期包含清理
有时确实需要外部辅助程序,而这时「添加路由」只是它工作中最小的一部分。它必须知道自己服务于哪条连接、那条连接正在使用哪个接口,等到相关网络状态稳定下来, 在连续运行两次时保持幂等,并且不清理任何不是它自己安装的东西。
最后一条才是最痛的。一个删除所有匹配办公前缀的路由的辅助程序,迟早会删掉一条它没有添加的路由。由此造成的故障会被归咎于 VPN 客户端,因为客户端是用户能看到的那部分。在写辅助程序之前先决定它拥有什么,比事后解释为什么某个本地子网不再响应要便宜。
重连竞态
设想一次快速重连。会话 A 断开,会话 B 建立并安装了自己的路由,然后 A 延迟的清理终于运行。如果那次清理按前缀删除路由,它会把 B 的有效状态一并带走,一秒前还能用的连接现在被路由到空处。
这是所有权竞态,而给脚本加一段睡眠只会改变哪一方获胜。修复方法是让清理以身份为条件:把辅助程序安装的接口和会话与仍然有效的那些作比较,只在匹配时删除。
测试什么,以及路由做不到什么
验收必须覆盖四种转换:首次连接、重连到不同接口、失败的连接尝试,以及最终断开。每一次之后,读取系统实际为远端某个地址选择的路由,而不是你期望它选择的那条。
route -n get 10.77.0.5
netstat -rn -f inet
把棘手情况也放进同一轮:需要时的 IPv6、与远端前缀重叠的本地前缀,以及通往 VPN 公网端点本身的路由——它在隧道活动期间必须保持可用。然后写下路由不是什么。它只选择数据包;它不在远端授予权限,也不创建返回路径,而这些仍是各有其所有者的独立要求。
你想要的结果是一个在每次转换中都跟随其受支持连接所有者的前缀,而不是一条恰好在写它的那台机器上成功过一次的命令。