加密从一条边界开始,而这条边界是端点在任何一个数据包被封装之前做出的路由决定。一个从未见过你密钥的网络,仍然可以给你的机器一条它更喜欢、胜过隧道自身路由的路径。这里有两种失败被混为一谈:读取一个已经进入隧道的数据包,以及引导一个从未被放进隧道的数据包。

这个区分之所以重要,是因为第二种失败更容易布置,也更容易被忽略。它表现为一个从错误接口发出的请求,而客户端仍在报告连接健康。

网络影响你的两种方式

到达隧道的数据包,其载荷对经过的每一跳都是隐藏的;即使路径出乎意料,这层保护依然成立。从未进入隧道的数据包则是来自你地址的普通请求,被送往路由表所指的地方。第一种失败需要被攻破的密码或已被控制的对端,而第二种只需要一条你的机器选择信任的配置消息。正是这种不对称,让路由这一半值得单独审查。

路径上的设备不需要你的密钥就能影响第二种情况。它只需要你的端点接受一条路由、一个解析器,或一条能产生这种结果的策略规则。

expected:  app -> tunnel device -> encapsulation -> internet
observed:  app -> wlan0        -> plaintext     -> internet

隧道建立之前,路由可能来自哪里

DHCP 分发的不只是地址租约。无类静态路由选项可以携带目标前缀和下一跳,而 RFC 3442 讨论了错误路由器地址的安全后果:流量被错误引导、拒绝服务,或一名停在路径上的观察者。在隧道建立之前就接受这些路由的客户端,最终可能走上一条与你的服务商毫无关系的首选路径。

这些都不使路由注入成为普遍现象。客户端必须接受该选项,前缀必须覆盖你在意的流量,选定的下一跳必须可达,执行策略必须允许数据包从那条路出去。去掉其中任何一条,同样的响应就无害了。这也是同一网络上的两台机器表现可能不同的原因:它们运行的客户端、策略,甚至地址族都不相同。

IPv6 值得单独过一遍,因为路由器通告通过不同于 DHCPv4 选项的代码携带默认路由。只检查 v4 表的测试并没有覆盖它。

检查内核实际选择的路径

路由查询是可以回答的,所以去问它们,而不是从状态灯推断。下面的命令显示某个目标被选中的路由和源地址,以及可能覆盖主表的路由规则。请在连接时运行一次,重连之后再运行一次,因为租约续期可能改变答案。

ip route get 203.0.113.10
ip route show table all | grep -E '^(default|203\.0\.113)'
ip rule show
resolvectl query example.com

从输出里看两件事:路由中指定的接口,以及被选中的源地址。隧道路由会写明隧道设备,泄漏的路由会写明物理设备,这个差别无需解密任何东西就能看出来。如果答案在两次运行之间发生变化,保留两份输出,并比较产生它们的那几条规则。

不要止步于默认路由。某个应用例外、一个分离隧道路由前缀,或者第二条策略规则,都可能在默认路由仍指向隧道内部时,把特定目标带离隧道。

决定执行点应该放在哪里

有三个条件决定客户端侧的执行是否足够,还是需要客户端之外的东西。如果客户端为你使用的每个地址族安装并守护路由,那么一条拥有默认路由的隧道通常就够。如果机器必须访问本地资源,而其他一切都要留在隧道里,这些例外就必须被逐一列出并加以管理,而不是交给接入网络通告什么就是什么。如果平台能把流量按应用绑定到某个接口,这种绑定比任何东西都能覆盖的路由是更强的保证。

无论选哪一种,把决定写在配置所在的地方。一条因为有人在事故中敲进去而存在的规则,熬不过下一次重启,所以把本地例外和引导例外保持得很窄,并记录每一条为什么存在。

每次客户端升级和每次网络变更之后,重新运行查询命令,并与上一次记录的输出比较。路由表就是执行的边界,所以要把它当作边界来对待。

相关文章