两个 OpenVPN 守护进程都可以报告隧道建立成功,而网关却把每个客户端都送进同一条隧道。握手阶段没有任何东西决定客户端走哪条隧道;真正决定的是内核,它逐个数据包地遍历策略规则,再查每条规则指向的表。一旦这样看,配置就不再是关于 OpenVPN 选项,而是一个有两个归属方的路由矩阵。

决定的是内核,不是守护进程

被转发的数据包到达 LAN 接口,路由代码按优先级顺序评估规则列表。第一条选择器匹配的规则给出一张表,由那张表里的路由决定出接口。如果表里没有匹配的路由,查找会继续落到下一条规则,正是这个细节让半成品的配置悄无声息地失败,而不是大声报错。

在那趟遍历中,隧道进程没有发言权。它创建接口,也能安装路由,但某个客户端的数据包会不会进入那个接口,完全取决于它被送到了哪张表。因此,导入路由完全相同的两条隧道会互相竞争,最后写入的一方为所有人赢下。这就是为什么配置在文件里看起来平衡,却仍然把全部客户端送进同一个守护进程。

给每个实例一套自己的资源

从名字开始,因为重名正是两个实例意外共享状态的途径。分配各自独立的隧道设备、独立的配置文件、独立的凭据和独立的服务单元。把每个实例的外层端点与隧道地址记进策略矩阵所在的那张表,这样以后改动只有一处可查。

dev tun-office
route-nopull
pull-filter ignore "redirect-gateway"

dev tun-lab
route-nopull

route-nopull 把服务器推送的路由挡在本地表之外,当策略表本来就该显式时,这正是你要的效果。某个服务器是否还会推送 redirect-gateway 选项、你的客户端版本是否支持过滤器写法,值得翻手册确认,而不是想当然。

写出矩阵:来源、表、设备

对每个客户端组,加一条按源地址选表的规则,然后把打算走的路径放进那张表:

ip rule add from 10.8.1.0/24 lookup 100 priority 1000
ip rule add from 10.9.1.0/24 lookup 200 priority 1000
ip route add default dev tun-office table 100
ip route add default dev tun-lab table 200

表里需要该组应有的目的路由,还需要让网关保持可用的本地或管理例外。只含默认路由的一张表,就是该组的全隧道策略,这也许完全正确,但它应当是一个决定,而不是一处遗漏。

隧道自身的传输不属于客户端组

每个守护进程的加密包都发源于网关自身,发往外层对端。只匹配客户端源前缀的规则会放过这些包,所以按源限定的规则比匹配一切的规则更安全。没有选择器的规则会捕获隧道自身的传输并把它送回隧道,连接于是以一种看起来像服务商故障的方式中断。

把本地管理会话和底层路径明确挡在策略表之外。把这些例外写在矩阵旁边;它们是设计的一部分,不是出事后才重新发现的临时绕法。

选中路径并不等于放行

转发许可是路径上稍后做出的第二个决定。接受从第二条隧道离开的数据包的防火墙规则,并不会让路由代码去选它;而正确的表也不会授予转发的许可。两者必须同时存在,只证明其中一个的测试会让你误解另一个。当一个组能通、另一个不能时,分别检查这两个步骤,而不是重写整张表。

源地址转换是第三个决定,它应当只限定在源组和真正承载它的隧道设备上。为两个实例加专用链,而不是清空 postrouting 链,因为不相关的服务可能在那里有条目,清空会把它们悄无声息地删掉。

一条隧道断开时会发生什么

提前决定失败时的行为。当某张隧道表失去路由,查找会落到下一条规则,该组的流量会毫无报错地继续走另一条隧道或普通上行链路。如果这不可接受,就在更低的优先级编号上加一条终止规则,让该来源得到不可达的结果,使丢失可见,而不是被吸收掉。

从每个源组各发一次新请求,并记录四件事:选中的表、出接口、转换后的源地址,以及回包是否走同一条路径。然后把一条隧道断开重做。接口列表里出现两个 TUN 设备,只能证明两个守护进程都起来了,并不能说明某个客户端用了哪一个。少了那条隧道的第二轮,才看得出回退规则是否按你的本意工作。

相关文章