一台 Pi 上的两个 VPN 跃点:哪条路由承载哪个对端
一台 Pi 上出现两个隧道地址并不是故障。每个目标都需要自己的承载方、自己的证明,以及外层跃点失效时已声明的回退方案。
一块小板子上出现两个隧道地址看起来像错误,本能反应是删掉一个。这种本能恰恰破坏了它本该整理的东西。承载两个跃点的主机会为每个接入点保留一个地址,而每个地址由不同的路由选中。值得问的问题是:哪条路由为哪个目标选择哪个承载方,而不是存在多少个地址。
需要不同承载方的三类目标
两跳链路是三类目标叠在一起,每一类由不同的接入点承载。外层对端必须经物理链路到达。内层对端必须经外层隧道到达。被覆盖应用的目标必须经内层隧道到达,而内层隧道自身的传输由外层隧道承载。
| Destination class | View that names the carrier | Command that proves it | Symptom when the wrong carrier wins |
|---|---|---|---|
| Outer peer, 198.51.100.10 | the main table, by destination | ip route get 198.51.100.10 | the outer handshake never completes |
| Inner peer, 203.0.113.7 | the main table once the outer tunnel is up | ip route get 203.0.113.7 | the inner handshake leaves over the physical link |
| Application destination, 172.30.9.1 | the table the inner client's traffic is selected into | ip route get 172.30.9.1 from 10.8.0.2 | the exit address looks right and the traffic never crossed the outer hop |
| Parallel tunnels, one per source group | one policy per group, not the main table | ip rule show beside a marked test connection | both tunnels are up and one steals the other's traffic |
在动路由之前,先用读者的真实地址把表格填满,因为只有具体之后表格才有用。一条可用的链路同时满足全部四行,而半可用的链路通常只满足其中一行,且用错了承载方。把错的那一行当成故障来读,正是把路由问题变成一个下午的反复修改的原因。
地址讲的是角色,不是数量
Linux 允许一个接口上配置多个地址、一台主机上运行多条隧道,所以数量并不能说明对错。关键在于哪个地址属于哪个接入点,以及某条对端对应的路由是否选中了你想要的那个接入点。三种视图从三个方向回答这个问题,每次都值得按同样的顺序读一遍。
地址清单说明存在哪些地址、归哪个接入点所有。路由表回答普通目标流量的承载方问题。策略视图则针对按源或标记选中的流量再回答一次,这是两个客户端共用一台主机时的常规安排。
ip -4 addr show
ip -4 route show table all
ip rule show
因为列表看起来长就删掉一个地址,可能删掉路径的一部分;因为看起来多余就删掉一条路由,也会悄悄造成同样的后果。两者都不会报错;对端只是不再在你预想的路径上应答,而从另一条路径仍然可达。应用报告的是内层对端的出口地址,并不能证明流量穿过了外层隧道,因为直连也会得到同样的观察结果。观察与证明之间的这道缺口,正是必须在路由层核对承载方的原因。
激活顺序胜过固定等待
如果内层客户端依赖外层客户端提供的地址或路由,激活就必须等待那个资源,而不是等待一段时间。固定等待在外层隧道变慢的那天就会失效,并且以无人能够复现的方式间歇性失败。基于依赖的排序消除了猜测,代价只是把依赖关系描述一次。接口名同样不是排序机制。
内核按创建顺序分配 tun0 和 tun1 这样的名字,而这个顺序在重启或重连之间可能改变。要把配置绑定到工具提供的身份,或者绑定到你刻意指定的名字。你选的名字是契约;内核选的名字是偶然。
让每个客户端的传输避开另一方的应用路由,并准确理解 OpenVPN 2.6 的路由选项 到底承诺了什么。文档中的默认重定向机制让客户端仍能通过既有网关到达自己的远端对端,这切断了一条特定的递归环路。它不会把一个客户端变成另一个客户端的承载方,所以链路其余部分仍是你的设计。当链路需要的是策略而不是路由时,写一条明确的源或标记规则,并用它应该匹配的流量去测试。
外层跃点死掉时会发生什么
在链路算作完整之前,先决定外层跃点消失时该怎么办。内层客户端的传输会随之死亡,下一步是策略问题而不是技术问题:失败、重试,还是直接经物理链路重连。第三个结果才是值得思考的那个。它在用户以为覆盖仍然完好的那一刻,悄悄把两跳设计变成了一跳设计。
如果两跳是强制要求,那么外层消失时内层客户端必须无法建立直连路径,而验证方法是打断外层跃点,观察内层会怎么做。经过批准的兜底是合法答案,但必须写下来、测试过并告知相关方。没人记录过的兜底,在事故复盘中与泄漏无法区分。
DNS 与另一个地址族属于同一个决定。本机生成的查询遵循主机自己的策略,而不是被转发客户端的策略,所以对转发流量有效的解析器,未必是主机自己使用的那个。
按你搭建的顺序验收每一层
按链路搭建的顺序测试:先单独测外层隧道,再测内层客户端经外层隧道的传输,最后测应用经内层隧道。每次验收都必须是新建立的连接,而不是已经打开的页面。然后分别打断并恢复每一层,观察下一层的反应。
兜底结果才是这里最有价值的观察,也是公网 IP 页面永远不会显示的东西。把对端路径、它们使用的地址以及那个结果保存在配置旁边,因为顺利路径并不是事后被质疑的那部分。