在单台 Linux 主机上叠放第二个 OpenVPN 客户端,会在两处之一失败。要么内层客户端无法到达自己的服务器,因为外层隧道已经截走了那部分传输;要么第二个客户端改写路由表,第一个隧道随之失效。两个问题都出在路由而非加密,而且都要在新隧道承载任何应用流量之前解决。这里的地址仅为示例:tun0 是外层隧道,203.0.113.9 是内层服务器,TCP 9999 是必须走内层路径的流。

两种失败模式,一张路由表

内层客户端向自己的服务器打开一个 UDP 套接字,这个套接字和其他套接字一样遵循当前路由表。如果外层隧道已经占据默认路由,握手包就会进入外层隧道。只有当外层服务器能到达内层服务器、且对端同意承载该流时才有效;否则握手停滞,客户端重试。

第二种失败来自服务器的 push。常见的 push 是 redirect-gateway def1,它安装 0.0.0.0/1 和 128.0.0.0/1,而不是替换默认路由。因此,各自安装自己 /1 对的两个客户端会相互冲突,较新的那一对几乎对每个目的地都胜出。当第二个连接后隧道失效时,去表里找这两个前缀,因为默认路由本身可能从未改变。

标记该流并给它一张路由表

在 mangle 表中标记选定的流量,并在策略规则中匹配该标记,从而引导流量。规则指向一张专用表,其默认路由是内层设备,因此只有被标记的包才走内层路径。

iptables -t mangle -A PREROUTING -p tcp -m multiport --dport 9999 -j MARK --set-mark 0x9999
ip rule add fwmark 0x9999 table 200 pref 1000
ip route add default dev tun1 table 200

这套规则里的失败大多来自两个错误。防火墙设置的标记值与规则匹配的值必须完全相同,因为不一致既不会报错也不会生效。而 PREROUTING 只能看到从外部到达的包,所以在这台主机上发起的连接,在被标记之前需要一条匹配条件相同的 OUTPUT 规则。

从第二个客户端手里收回路由所有权

有两种做法能阻止第二个客户端改动路由表,它们作用于不同环节。route-nopull 忽略推送来的路由选项,因此什么都不安装。route-noexec 接受推送的选项进入客户端环境,但把表改动留给 hook,当你要看清服务器提供了什么时,这正是需要的。

hook 运行时环境里带着推送的值,它只应添加你批准的条目。让它保持幂等:重建之前先清空它自己拥有的那张表,不要假设上一次运行留下了干净状态。重连会再次调用 hook,而只追加不清空的 hook 会不断累积重复路由。

选一个所有者并写下来。同时设置两个选项的配置,或者与手工脚本争抢的 hook,最终会留下一张谁也解释不清的表。

把两个服务器地址都固定住

内层客户端连接之前,内层服务器需要一条经由外层设备的主机路由。正是这条条目把内层传输留在外层隧道里,而不是让它走默认路由。

ip route add 203.0.113.9/32 dev tun0
ip route get 203.0.113.9

也要在主表里为外层服务器加一条对应的主机路由,否则重连可能把外层客户端自己的传输送进它自己的隧道。顺序不是可选项:这些条目要在相关接口存在之后再添加,也就是由连接后运行的 hook 或脚本来做。指向已下线设备的路由会被删除或忽略,所以开机时写入的条目撑不过你设想的时序。

检查查询结果,而不是通读整张表。路由查询的输出会指出内核将使用的设备和源地址,这正是客户端打开套接字时得到的。

测试整条链,并让失败可见

在内层客户端连接时于外层接口抓包。内层握手应当以加密载荷的形式出现在外层隧道内部;如果它表现为发往 203.0.113.9 的明文 UDP,说明主机路由缺失,这条链从未建成。

悄悄退回物理上行链路是最糟的结果,因为接口状态什么都不会宣告,内层流量会从错误的源地址发出。如果外层隧道断开时第二个客户端能直接重连,就禁用那条路径,或把它绑定到外层接口,让失败可见。

最后用两条流和一次规则转储收尾:被标记和未被标记的连接应当走不同路径,ip rule show 应当列出你预期的优先级。如果两条流走同一条路径,说明标记没有作用在你以为的位置。

相关文章