你通过 SSH 登录一台构建主机,在上面启动 VPN 客户端,然后会话就不再响应。没有任何东西崩溃,也没有任何东西在通常意义上看起来配置错误。客户端安装了一条默认路由,于是发往你工作站的回包现在会走隧道,而你工作站的地址在隧道里不可路由。有趣的是,这个结果是可预测的,修复办法就是在客户端启动前写好一条路由。假设一台只有一块网卡的服务器,网关是 192.0.2.254,管理员从 203.0.113.9 连接。

启动之前先把路径记下来

ip route show default 会显示当前网关和接口,而 ip route get 203.0.113.9 会显示内核对你的会话将使用哪个接口和源地址。把两个输出都保留下来。隧道起来之后,这两条命令仍应描述物理路径;如果它们描述的是隧道,那这个会话已经在借来的时间里运行了。

真正重要的是已经建立的连接,所以不要只用一次新的登录来验证。新会话可能凭旧会话已经不具备的状态短暂成功。

在客户端自己的配置里固定地址

手工添加的路由会在重启后消失,还会和客户端自己的路由表更新发生竞争,所以要把例外放在管理路由表的地方。在 OpenVPN 中,net_gateway 关键字会解析为隧道建立之前的网关,因此 route 203.0.113.9 255.255.255.255 net_gateway 恰好把一个地址留在隧道之外。WireGuard 需要另一种做法,因为 wg-quick 自己管理路由:设置 Table = off,并在 PostUp 中加入例外,同时配一条对应的 PostDown 把它删掉。两种写法表达同样的意图,而且都只把例外限定在一个地址上,而不是削弱整条隧道。

对于 IPv6,同样的思路是取一个单主机前缀,而封装传输的情形值得记住:如果隧道本身的外层连接位于固定地址上,这个地址就必须留在它所承载的隧道之外,否则客户端会把自身的传输路由进自己内部。

或者把主路由表从问题里拿掉

当主机上有多个服务必须保持正常路径时,就不要再改主路由表,而是给客户端一张自己的表。带一对 veth 的网络命名空间可以干净地做到这一点:客户端的默认路由存在于另一个协议栈中,只有在该命名空间内启动的进程才会使用隧道。策略路由也能为单个目的地达到类似效果:把匹配的流量送入一张专用表,而它的默认路由仍指向物理网关。两种做法都不会给主机的主路由表添加新的默认路由,正是这一点让 SSH 继续有回应。

两者都比单独一条主机路由需要更多机制,而当机器上有不止一个东西必须挺过隧道时,两者都值得做。

假定固定本身可能失效

一条绑定在你当前公网地址上的路由,会在运营商重新分配地址的那一刻失效。此时远程服务器就需要一条不依赖该地址的路径:控制台、第二个管理接口,或者一台留在隧道之外的跳板主机。

两项防护只需要十分钟。一个看门狗会在几分钟内没有出现确认登录时停掉客户端,把被锁在外面变成一次延迟;而在变更期间保持打开的第二条 SSH 会话,可以证明隧道处于连通状态时例外仍然有效。单靠 keepalive 在这里没有帮助,因为路径是消失了,而不是空闲;它只是让死亡更容易被察觉。

按真正重要的顺序验证

连接隧道,确认通往你地址的路由仍然从物理网关出去,然后打开一个新会话。如果新会话能用,而旧会话仍然死掉,那原因不是路由,而是连接跟踪或有状态防火墙,这是另一种修法。

把固定下来的地址和证明它的命令写进变更记录。下一个在该主机上启动客户端的人会同时需要这两样,而到那时地址可能已经变了。

相关文章