既为 LAN 做路由、又要拨 VPN 的机器会陷入自相矛盾:隧道想占据默认路由,而 LAN 需要把默认路由要回来。OpenVPN 连接时可能安装一条经由隧道的默认路由,或一对半默认路由;这对笔记本是对的,对邻居指望从其 LAN 地址收到回包的网关却是错的。答案不是放弃 VPN。而是让这两个决定使用不同的路由表。

一条默认路由,两个主人

转发流量和本机产生的流量不必走同一条路。LAN 客户端只需要为选定的目的地走隧道,而主机自身的管理会话及其对这些客户端的回包需要 LAN 默认路由。把两者混在主表里,等于用一个答案去回答两个不同的问题。

OpenVPN 有一个开关正是为此而设。route-noexec 让客户端完全不碰主表,而它原本要用的值会导出到 route-up 脚本,形式为 route_vpn_gateway、dev 以及接口地址。之后由你决定哪张表接收哪条路由,写在以后还能读懂的代码里。

策略表与改写主表的对比

ApproachWhat it buysWhat it costs
Separate table plus source ruleLAN traffic and host traffic keep independent defaultsEvery lookup now depends on rule order
Editing the main table onlyFewer moving parts on a single-purpose hostOne mistake takes the gateway offline entirely
Two half-default routesWorks for a full-tunnel clientAmbiguous on a host that must answer on its LAN address

独立路由表是唯一能扩展到一个子网之外的做法。给它起一次名字,之后按名字引用,这样谁都不必记住数字:

echo "10 vpn" >> /etc/iproute2/rt_tables
ip rule add from 192.168.50.0/24 table vpn
ip route add default via 10.8.0.1 dev tun0 table vpn

这里的地址都是占位符。动手之前,请换成你自己部署中的 LAN 前缀、隧道网关和设备名。

规则也必须匹配主机本身

只匹配 LAN 前缀的规则覆盖转发报文,却不覆盖网关自身发起的流量。它也不覆盖回给 LAN 客户端的应答,这些应答以网关的 LAN 地址为源。 这些查找会落到 main,仍然可能从隧道出去。

为网关自己的 LAN 地址再加一条规则,或把第一条规则扩展为同时包含两者的集合。顺序很重要:把更具体的规则放在前面,并用明确写出源的查找来验证结果。

ip route get 203.0.113.9 from 192.168.50.1

如果对应当地访问的目的地却报出了隧道设备,那么无论表里有什么,规则集都是错的。

NAT,还是对端的路由

VPN 服务器只认识隧道子网,别的一概不知。以 LAN 源地址到达的报文没有回程,除非有人提供回程,而提供回程恰好只有两种办法。在服务器上为 LAN 前缀加一条路由,可以让原始源地址端到端可见,这通常是经过审计的设计想要的。在网关上做伪装则把源改写成隧道地址,不需要服务器配合。

iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o tun0 -j MASQUERADE

不掌控服务器、也无法在服务器上加路由时,选 NAT。当日志、访问控制或回程对称性重要时,选服务器侧路由,因为伪装会把原始地址从对端记录的一切中抹掉。

无论哪种做法,都必须开启转发;当 main 不会为该源选择隧道时,严格的逆向路径过滤可能丢掉回包。先查有效值,再怪隧道。

让这套拆分在重连后依然成立

手工添加的规则和路由会在接口重建时消失,而一条过期的表项可能比当初使它合理的那个会话活得更久。把表规则、隧道路由和伪装项放进客户端的 route-up 脚本,并在脚本开头清空该表,免得旧网关残留到新会话里。

从两端验证:在网关上用 LAN 源做一次查找,再从 LAN 客户端向应当走隧道的目的地发一次真实请求。一条存在却从未被选中的路由,在你读计数器之前,表现得和完全没有路由一模一样。

相关文章