两条上行链路、一条隧道,流量却从你没选的那条出去。私有子网可以访问,日志报告连接成功,承载它的正是那条计费链路。OpenVPN 的输出里没有提到任何网关,因为客户端从未选过网关。它只是向远端地址打开了一个套接字,然后让内核决定如何到达那个地址。

隧道地址不是端点

每个会话里都有两条不同的路径。外层路径把加密传输送到服务器的公网地址。内层路径承载服务器推入隧道的一切,两者的路由规则互不影响。

配置文件里的 remote 条目指定的是外层目的地址,所以要固定的就是它。当地址名解析结果变化,或配置列出多个 remote 时,变化的也正是这个值。

找出客户端真正拨号的地址

读取活动套接字,而不是配置文件。套接字表显示进程实际连上的对端,例外规则必须与这个值匹配。会话开始后配置仍可能被修改,套接字是唯一能证明实际拨号对象的证据:

ss -tunp | grep -i openvpn
journalctl -u openvpn-client@work --no-pager | grep -i "Peer Connection Initiated"

日志行会给出连接时的对端地址和端口。如果它与配置不符,说明客户端前方有东西在连接开始前改写了目的地址。

还要检查地址族。双栈配置可能通过 IPv6 连接,而你的例外只覆盖 IPv4,流量于是遵循另一条规则。代端点应答的代理也一样。这两种情况都会让隧道看起来正常,而你的路由毫无作用。

固定端点并读取策略顺序

主机路由比任何默认路由都更具体。因此对端地址的 /32 在包含它的表内胜出。常见写法会明确写出网关和设备:

ip route add 203.0.113.40/32 via 192.168.10.1 dev eth0

只有当查找到达那张表时,这条路由才有用。策略规则按顺序求值,第一个匹配者决定结果,所以 把所有流量发往第二张表的规则会绕过 main 表中的主机路由。在认定这条路由权威之前,先读取规则:

ip rule show
ip route get 203.0.113.40

策略依赖源地址或标记时,用客户端所用的同一源地址或标记去问第二条命令。它会报告内核会选的路由,这只是查找过程的证据,并不证明活动套接字正在走那条路。

当端点移动时

一个解析到多个地址的主机名,会在应答变化时立刻让固定的 /32 失效。含多条 remote 的配置有同样的问题,因为客户端可能落到其中任何一个。应当由客户端自身的生命周期钩子刷新路由,而不是手工添加。up 脚本读取协商对端并安装主机路由,让例外与现实保持一致,配套的 down 脚本则把它移除。

把刷新记录下来,放在主机其余路由配置所在的位置。只活到下次重启的路由只是测试,不是修复。

把内层策略独立出来

移动外层路径并不会开放新的私有子网,也不会改变服务器推入隧道的目的地。底层路由决定走哪条线路,推入的路由决定什么进入隧道。把这两个决定分开,分割才可预测。

如果目的只是让加密传输避开计费链路,就保持默认路由不动,只添加单条主机例外。为移动一条 VPN 连接而替换整机默认路由,会破坏主机上的其他所有应用。

用两个相互独立的观察来确认结果:套接字的本地地址与设备,以及私有目的地址解析到的路由。两者都与你的意图一致时,分割才成立。OpenVPN 2.6 手册介绍了决定客户端拨号地址的 remote 与本地绑定选项。

相关文章