隧道连上之后,接入链路上的一次租约续期把它弄断,日志却把责任推给超时而不是路由。 VPN 配置里什么都没改,这正是线索:变的是它之外的东西。 一次 DHCP 事件重写了地址或网关,原本让隧道端点可达的那条主机路由,现在指向一个已不存在的下一跳。 这是归属问题,然后才是路由问题。

拿症状去对照半默认路由

使用 redirect-gateway def1 时,OpenVPN 会安装 0.0.0.0/1 和 128.0.0.0/1,而不是替换默认路由。 这两条合起来覆盖整个地址空间,而且仅凭前缀长度就压过 0.0.0.0/0。 因此任何没有更具体匹配的目的地都会进入隧道。

端点需要一条例外。 在半默认路由写入之前,OpenVPN 会先经由原接入网关为服务器地址添加一条主机路由,好让封装后的报文能出去。 当这条例外消失,或者仍指向一个已不存在的下一跳时,隧道自己的外层报文就会匹配到半默认路由,被送回隧道里。 结果是一条在下次 keepalive 或握手时死掉的连接,日志里却没有任何像路由错误的东西。

在动别的之前,先确认路由表的样子。 ip route show 应当列出两个半边,再加上服务器地址的主机路由;主机路由缺失时,后续排查就有了明显的起点。

为什么半默认路由不改度量值也能赢

更低的度量值赢不了更长的前缀。 /1 路由之所以赢,是因为内核先匹配最具体的路由,所以在默认路由上提高度量值,完全不会改变去往隧道的流量走哪里。 追度量值是这套诊断里最常见的错误转向。

用 ip route get 203.0.113.10 检查选路,把服务器地址换成真实值。 它会说出内核实际会用的接口和下一跳,比读表再推理更有信息量。

在隧道启动前、运行中、租约事件之后各跑一次这条命令。 一条命令给出三个答案,分叉点就看得见了。

取三份快照,而不是一份

在三个时刻收集接入地址、接入网关、端点目的地、被选中的路由以及任何策略规则:接入链路配置完成后、VPN 连接后、租约或运营商事件之后。 每份快照只是几条命令,只有放在一起才有意义。 故障之后的一份快照记录的是状态,而不是过程。 把这些命令写进一个小脚本,让每份快照都以同样的方式采集。

把快照放在一个文件里并带上时间戳,因为后面真正有意思的问题是顺序。 DHCP 事件是在 OpenVPN 刷新路由之前还是之后到来的? 答案决定了该由谁来修。 错误时钟的时间戳比没有更糟,所以要从拥有路由表的那台主机上取。

当端点配置成名字而不是地址时,每份快照里也要解析一次。 接入链路上的 DNS 变更可以在完全不碰路由的情况下挪走端点。

决定由哪个组件刷新这条例外

在 dhcpcd 系统上,钩子既从钩子目录运行,也从退出钩子运行,而管理器模式与逐接口模式的作用范围不同。 钩子无论做什么都必须幂等,因为续租可能成批到来。

可靠的归属者是那个本来就了解两端的集成:管理 VPN 连接的网络管理器,或者在接入链路起来之后才排序的 unit。 钩子添加的静态路由只能用下一次网关变更之前,之后就会以完全相同的方式失败。 如果确实要用钩子,就让它运行时读取当前网关,而不是把你今天看到的那个写死。

把归属者写进配置里。 下一个排查重连的人需要知道:负责这条例外路由的是钩子,不是 VPN。

测的是生命周期,而不只是重连

用 dhcpcd -n eth0 或者释放并重新获取租约来强制续租,然后再用 ip route get 检查端点。 做两遍,因为第二遍能抓住那些追加重复路由的钩子。 然后重启,因为持久性是另一种属性,和正确性不是一回事。

能扛过续租和重启的隧道,才有归属者和可用的例外。 只扛过手动重启的隧道两者都没有,并且会在下一个你排不了期的事件上失败。

相关文章