为什么全隧道路由会切断承载它的服务器
经由隧道的默认路由也会捕获承载隧道的封装包。用主机路由固定端点,并让服务器回包绕开它。
开启全隧道后,每个目的地都归 VPN 接口所有,连终止隧道的机器也一样。承载隧道的外层包正是发给那台机器的,于是路由最终吃掉了自己所依赖的传输。再加一条具体的路由就能解决,而其中的道理值得弄懂。不懂这个道理,下一次重建就会把同样的故障带回来。
一条吃掉自己传输的路由
封装会给每个包套上第二层包头。外层包从本机物理地址发往服务器的公网地址,必须在任何隧道建立之前穿过物理链路。
现在让那条公网地址的查找选中隧道。要发出这个包,机器必须先封装它,而这又需要一条通往同一地址的路由,那条路由返回的仍是隧道。客户端有两种处理方式。谨慎的客户端拒绝安装默认路由并报错,而不那么谨慎的客户端报告成功,随后却什么也没搬动。真正耗掉半天的是那种无声的失败。
最长前缀匹配在任何度量被比较之前就决定了查找结果,因此一条单地址路由胜过机器上的所有默认路由。修复背后的全部机制就在这里。
先装例外,再装默认路由
在默认路由移动之前,先为服务器建一条单地址路由,下一跳指向物理网关。顺序很重要:事后再装,就等于一边打字一边忍受断掉的隧道。
大多数客户端不用提醒就会这么做。OpenVPN会先查出远端地址并在改写默认网关之前把它固定下来,它的 route-delay 选项正是为这类顺序会打架的平台准备的。wg-quick 走的是完全不同的路,给包打标记并查询策略规则而不是新增一条例外。机制不同,要求相同:外层包必须逃离它自己创建的隧道。
ip route add 203.0.113.10/32 via 192.168.1.1 dev eth0
ip route get 203.0.113.10
查找的输出就是验收测试。它必须报出物理设备和物理下一跳。如果答案里出现隧道设备,说明例外缺失,或者被放在了会覆盖它的东西之后。
服务器上的镜像
同一个陷阱在另一侧反着出现。给服务器一条经由另一条隧道的默认路由,或者给它推一条覆盖客户端公网地址的路由,它的回包就会养成走错方向的习惯。
设想一个客户端连上来并发出请求。回包被送往该客户端的公网地址,而这个地址如今匹配上了隧道路由,于是回包从另一个接口离开,带着客户端从未拨打过的源地址。客户端可能收到这个包却仍然丢弃它,因为没有任何东西把它和当初发出的请求绑在一起。排查要从服务器的路由决策入手,而不是从客户端的超时入手,因为只有前者能显示真正的选择。
ip route get 198.51.100.44 from 10.90.0.1
ip rule show
sudo tcpdump -ni any host 198.51.100.44
修复有三种,而且并不等价。把服务绑在物理地址上,或者做源地址转换,两者都是把路由决策藏起来而不是改正它。让隧道地址池的回包走一张专用表,才是修正了根因,另外两种只在路由设计被冻结期间可以接受。
隧道不断偷走的例外
有几项工作即使在隧道占着默认路由时也需要开放互联网,每一项都得有自己的例外。动态 DNS 更新程序通常是第一个坏掉的。
它解析服务商、建立连接,然后发现自己的包走在一条几秒前刚换过地址的隧道里,于是更新失败,或者发布了错误的地址。出路有三条:用经物理网关的主机路由把服务商固定住,在隧道升起之前先跑更新,或者给更新程序一张隧道永远不会碰的策略表。时间同步在没有解析器的设备上同样暴露,证书校验端点也出于同样的原因被列进清单。
没人写下来的例外,就是下一次重建会悄悄丢掉的例外。
要在重连之后验证,而不是之前
测三个目的地,而不是一个:端点、隧道后面的私有地址,以及更远处的公网地址。一次成功的查找几乎说明不了另外两个。
ip route get 203.0.113.10
traceroute -n 203.0.113.10
ip route get 10.90.0.5
ip route get 1.1.1.1
然后把连接断开重连,因为重新建表的客户端会把你手敲的路由扔掉。如果启用了 kill switch,要确认端点是被放行的,否则网络一变隧道就回不来,症状看起来像断网。把例外存进配置文件或 unit 文件,而不是 shell 历史里,这样重连时它会重建而不是被抹掉。