两个同时运行的 VPN 客户端并不会形成嵌套隧道;它们形成的是两条隧道和一场关于路由的争执。当内层客户端到自身服务器的连接本身由外层隧道承载时,才存在嵌套隧道,而这是一种刻意搭建的布置。做对了,内层服务器看到的是外层服务器的地址;做错了,其中一个客户端会悄悄绕开另一个重新连接。三条条件决定了嵌套是真实的还是巧合。

三条条件决定嵌套

第一条是哪个端点需要哪条路径。说出内层服务器的地址,说出外层服务器的地址,并明确声明内层服务器只能通过外层隧道到达。其余一切(包括哪些路由属于哪张表)都由这句话推出来。

第二条是能否在每一层看到路径。如果你无法向内核询问某个目标会用哪条路由,你就是在根据最终的出口 IP 推断这条链条,而这恰恰是无法区分嵌套与巧合的证据。

第三条是外层隧道断开时会怎样。内层隧道可以随之停止,可以改走普通网络重连,也可以一直阻塞到外层路径恢复,这三种都是站得住脚的策略。它们并不是同一种策略,而多数客户端的默认行为是第二种。客户端一边显示连接正常、一边通过物理网络重连其端点,就是原本设想的链条悄悄变成两条互不相关隧道的常见方式。

条件一:哪个端点需要哪条路径

ip route get 203.0.113.10 直接回答了这个问题:它打印出路由、源地址以及内核此刻为该目标会使用的接口。

在内层客户端启动之前对内层服务器运行一次,启动之后再运行一次,然后对比。如果答案从物理接口变为外层隧道的接口,那么此刻嵌套是真实的。如果答案没变,那么无论面板上怎么说,内层客户端都是直连的。对外层服务器的地址重复一次;它仍然必须通过物理接口解析,因为 一条递归地把两个端点都捕获进来的路由 正是让整台机器彻底断网的那种故障。

条件二:在每一层检查路径

在现代客户端上,路由选择远不止主表。策略规则可以把带标记的报文送到另一张表,客户端可以把套接字绑定到某个接口,容器或命名空间更是有自己的表。ip rule 和 ip route show table all 就是查清其中哪种机制在起作用的方法。

把自己的套接字打上 SO_MARK、或被配置为绑定到特定设备的客户端,会绕过你以为已经安排好的默认路由。这是特性而不是缺陷,它意味着唯一可靠的检查是与客户端处在同一命名空间里的检查。如果客户端跑在容器里,就在那些容器里做检查,而不是在主机上做。

条件三:外层隧道断开时会发生什么

故障行为需要的是一个决定,而不是默认值。写下外层隧道消失时内层隧道应当如何,然后让客户端照此执行。

选项很具体。外层路径消失时停掉内层客户端,得到的是干净的链条和明显的故障。允许它直接重连,得到的是可用性,同时悄悄去掉了链条当初要保证的那个性质。阻塞到外层路径恢复是最严格、运维上也最烦人的做法,因为外层隧道的每一次抖动都会变成一起看得见的事故。

用无害流量和一次刻意的外层故障去测这三种行为,因为读配置不会告诉你实际是哪一种。杀掉外层隧道,观察内层客户端做了什么,并在它断开期间再查一次路由。然后把外层隧道恢复,确认链条重新形成,而不是把内层客户端留在它这期间选中的直连路径上。

链条不能给你的东西

额外的层次会增加端点、密钥和故障模式;它不会增加任何匿名性保证,也不会让外层运营者看不见隧道的存在。任何关于嵌套保护了什么的主张,都必须点明它防范的是哪个观察者。

交付物是一份简短文档,而不是一条可用的连接:哪个端点走哪条路径、外层故障时会发生什么,以及三条显示当前状态的命令。strongSwan 关于转发的文档 是了解 Linux 主机上隧道流量如何在接口之间转发的有用参考。每次客户端升级后都要重跑检查,因为造就你那条链条的默认值并不是承诺。一条你无法检查的嵌套隧道,只是带着状态灯的传言。

相关文章