在一台你通过 SSH 连接的机器上开启 OpenVPN 全隧道,可能在一秒内切断会话,而机器本身没有任何过错。隧道改变的不是数据包如何到达服务器,而是回应如何离开。默认路由一指向 tun0,发给你 SSH 客户端的回应就进入服务商的网络,而那里没有回到你地址的路由。连接挂起,下一次尝试超时,守护进程全程仍在监听 22 端口。

在读 VPN 日志之前,先读路由表。日志会说隧道已经建立,这句话没错。问题在于每个回应被交给哪个接口,而那是对刚被隧道改写的路由表逐包查询的结果。

为什么回应从错的门出去

入站数据包仍然从物理接口到达,因为 VPN 客户端无法改变互联网如何到达你服务器的公网地址。回应是另一个决定:对每个数据包,内核查询目标地址,而在 --redirect-gateway 之后,默认条目写着隧道。服务商的出口看到一个自己并不服务的目标,于是丢弃该包。

OpenVPN 自身的保护只覆盖一个目标。 2.6 手册把 --redirect-gateway 描述为一个序列:先通过隧道之前使用的网关为 VPN 对端添加一条主机路由,使加密流量不会绕回自身,然后替换默认路由。那条主机路由只针对对端。你的 SSH 客户端只是另一个互联网地址,不会得到任何例外。

有一个前缀错误值得尽早排除。不带长度的 ip route add -net 0.0.0.0 gw 10.8.0.1 dev tun0 只匹配 0.0.0.0 这个单一地址,而不是整个互联网;必须写成 0.0.0.0/0。而如果走默认路由会破坏隧道自身的传输,就把对端地址作为主机路由加在物理接口上,并使用服务商的真实地址。

固定客户端地址以立即修复

最快的修法是通过隧道之前服务器用的网关,为你的客户端地址加一条主机路由。先在你这边查到服务器会看到的地址,再读服务器的路由表确认旧网关:

curl -s https://ifconfig.co
ip route

在服务器上,趁客户端还没启动就加上这条例外。这里的地址只是占位,形式才是重点:

sudo ip route add 80.128.240.55 via 192.168.178.1

这就是报告案例里用的修法,而且立刻生效。它也在两个方面可预期地脆弱:重启后路由消失,服务商一换地址它就失效。把它当成应急措施,并有意识地做成持久配置,通过网络单元或 dispatcher 钩子;或者承认它是临时的,并把恢复步骤写下来。

别让客户端拿走默认路由

更干净的做法是不让隧道替换默认路由。 route-nopull 与 --client 或 --pull 一起使用时,会接受推送的选项,但路由、block-outside-dns 以及 DNS 服务器这类 DHCP 风格选项除外,因此你只添加自己需要的前缀。更窄的工具是拉取过滤器,它丢掉一条有问题的指令,保留其余部分:

route-nopull
pull-filter ignore "redirect-gateway"

两者只取其一,不要都用,并且清楚会改变什么。没有经隧道的默认路由后,服务器不再通过服务商浏览互联网,只能到达你显式路由的目标。如果全隧道正是这套配置的全部目的,那就是用错了开关,上面的主机路由才是对的。

按源地址或按连接拆分回程

当入站地址稳定时,源地址规则能让回应保持本地,而不必触碰隧道自身的路由。给物理接口地址单独建一张表,放入 LAN 前缀和物理默认路由,然后对来自它的流量选择这张表:

ip rule add from 192.168.1.11 table 128
ip route add table 128 to 192.168.1.0/24 dev eth0
ip route add table 128 default via 192.168.1.1

这之所以有效,是因为入站 SSH 连接的回应来自客户端所连的那个地址。报告案例中每个入站服务都必须继续工作,用的正是这种形式。当入站地址漂移或路径是动态的时,就改为标记连接:在从物理接口进来的第一个包上打 conntrack 标记,在 OUTPUT 中把它复制到回应包上,再让带标记的包走指向物理网关的表。

iptables -t mangle -A PREROUTING -i eth0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1234
iptables -t mangle -A OUTPUT -m connmark --mark 1234 -j MARK --set-mark 4321
ip route add default via 192.168.1.1 dev eth0 table 3412
ip rule add fwmark 4321 table 3412

测试时把连接标记、防火墙标记和表号分开。一个编号兼任两种用途,会产生看起来正确、却匹配错误数据包的规则。路由表的默认条目在普通以太网链路上还需要带 LAN 网关的 via;只有接口是点对点,或者下一跳用别的方式解析时,裸的 dev eth0 才可行。报告中的 Jessie 主机恰好位于裸写法可用的链路,那不是可以照抄的发行版规则。

在不会把自己关在外面的前提下验证

不要用断开来测试。保持第一个会话开着,把隧道拉起来,然后问内核你自己的地址会走哪条路:

ip route get 80.128.240.55

答案应当指出物理接口和旧网关,而不是隧道。然后开第二个 SSH 连接;这才是真正的测试,因为旧会话可能靠新连接无法复现的状态存活。如果第二个挂起而第一个仍有回应,说明规则从未匹配到真正重要的流量,该看的是标记或源地址。

整个过程中都留一个带外控制台。唯一通路就是你刚弄断的那条隧道的服务器,无法再通过这条隧道修复;把添加的表号和前缀记下来,回滚会容易得多。

相关文章