让一个 LAN 子网走 OpenVPN,并保留默认路由
让一个子网走隧道,同时保留 LAN 的默认路由。比较策略表与改动主表的做法,然后选择 NAT 或服务器侧路由。
既为 LAN 做路由、又要拨 VPN 的机器会陷入自相矛盾:隧道想占据默认路由,而 LAN 需要把默认路由要回来。OpenVPN 连接时可能安装一条经由隧道的默认路由,或一对半默认路由;这对笔记本是对的,对邻居指望从其 LAN 地址收到回包的网关却是错的。答案不是放弃 VPN。而是让这两个决定使用不同的路由表。
一条默认路由,两个主人
转发流量和本机产生的流量不必走同一条路。LAN 客户端只需要为选定的目的地走隧道,而主机自身的管理会话及其对这些客户端的回包需要 LAN 默认路由。把两者混在主表里,等于用一个答案去回答两个不同的问题。
OpenVPN 有一个开关正是为此而设。route-noexec 让客户端完全不碰主表,而它原本要用的值会导出到 route-up 脚本,形式为 route_vpn_gateway、dev 以及接口地址。之后由你决定哪张表接收哪条路由,写在以后还能读懂的代码里。
策略表与改写主表的对比
| Approach | What it buys | What it costs |
|---|---|---|
| Separate table plus source rule | LAN traffic and host traffic keep independent defaults | Every lookup now depends on rule order |
| Editing the main table only | Fewer moving parts on a single-purpose host | One mistake takes the gateway offline entirely |
| Two half-default routes | Works for a full-tunnel client | Ambiguous 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 客户端向应当走隧道的目的地发一次真实请求。一条存在却从未被选中的路由,在你读计数器之前,表现得和完全没有路由一模一样。