添加一条经隧道的默认路由,隧道自身的报文也会走它。此时路由表里就有了一条自我否定的规则。通往服务器的加密连接,试图走在那条它正要建立的连接里面。

有两种机制可以避免这个循环。一种在路由表中看得见,另一种藏在应用内部。

内部流量与外部流量是两个不同的决定

应用报文进入虚拟接口后被封装。结果是一个发往服务器公网地址的新报文。这个报文需要在物理网络上有路由,因为它任何部分都不能进入隧道。

两者都是路由决策,而只有前者属于隧道。当客户端安装覆盖服务器的默认路由时,外部报文同样会被捕获。

症状立刻出现且是彻底的。握手完成后什么都不通,或者路由一装上连接就断。两种情况下,keepalive 都再也发不出去。

方案一:为端点加一条主机路由

直接的做法是经由原网关为服务器地址添加一条路由。只需要一行。任何查看路由表的人都能看见它。在变更记录里也容易解释。

它的弱点会在底层路径变化时显现。保存下来的路由写明了旧网络的网关和接口。切换 Wi-Fi 之后,那个网关可能已不存在,而路由仍留在原处,看起来正确,实际什么也不做。

# before the tunnel: which path reaches the endpoint?
ip route get 198.51.100.7

# pin it through the current gateway
ip route add 198.51.100.7/32 via 192.168.1.1 dev wlan0

# after the tunnel is up, confirm the pin survived
ip route get 198.51.100.7

主机名会让这件事更难。如果端点是一个解析到多个地址的名字,排除规则就必须覆盖真正在用的那个地址。之后的解析可能返回路由从未覆盖过的地址。

方案二:保护套接字

另一种做法完全不碰路由表。客户端给承载隧道的套接字打标记或加以保护,于是操作系统按策略把该套接字的流量发到隧道之外。Android 直接把这一机制提供给 VPN 应用。桌面客户端用防火墙标记或绑定到设备的调用来实现等价方案。

好处是例外跟随连接,而不是跟随一条保存的网关。网络变化会重写底层路径,而受保护的套接字随之移动,因为它从未被钉在某个地址上。路由表保持干净。

代价是可见性。路由表里没有任何东西显示这个例外,于是寻找主机路由的评审者什么也找不到,可能会断定设计不安全。保护存在于客户端代码以及该代码安装的策略规则之中。

如何在两者之间选择

客户端支持时,优先选择套接字层面的机制,因为它能扛住那些会破坏固定路由的网络变化。当例外必须对其他工具可见,或客户端没有更好的选择时,就用主机路由。

两种机制都无法免除一个前提:服务器地址本身必须可达。私有网关有一个公网监听地址,必须留在隧道之外的正是这个地址。它背后的内部网络要等隧道建立之后才可达。

无论选哪种,都要把它交给拥有隧道的那个组件。手工添加的野路由只能撑到下一次换网络,然后就变成明天那桩神秘的故障。

相关文章