只把一个私有子网放进隧道、其余流量走平时的路径,这是一个小需求,却有很多种做错的方式。决定结果的不是叫 split tunneling 的复选框,而是决策在哪里强制执行:客户端自己的路由表、服务器推送的路由,或者两端协商的策略。选错层级,你要么得到从未想要的全隧道,要么得到一条指向对端拒绝接受的隧道的路由。我在打开任何配置之前先用三条件测试判断,然后从下面几种机制里选一种。

决定设计的三个条件

第一,你需要从网络所有者那里拿到精确的前缀和掩码。10.x 网络不是前缀,10.20.0.0/16 才是。确认它不与你的家庭 LAN、Docker 网桥或你运行的另一套 VPN 重叠,因为重叠会让路由产生歧义,内核会选中你并不想要的那条。

第二,回程流量由谁应答。把数据包路由进隧道只是把它送到 VPN 服务器。如果子网在另一台路由器后面,而它没有回到你客户端地址池的路由,服务器就必须做地址伪装,或者那台路由器必须学到你的地址池。客户端侧的任何路由都修不好缺失的回程路径。

第三,强制执行点。有些客户端安装一条路由,剩下的交给操作系统。另一些则协商策略,而网关不接受的策略根本不会建立隧道:数据包被丢弃,却没有明显报错。动笔之前先弄清你属于哪一种。

WireGuard:AllowedIPs 就是路由表

在客户端上,AllowedIPs 列出允许进入隧道的目标,wg-quick 把每一条变成普通路由。因此这项工作的客户端一半只有一行。

[Peer]
PublicKey = ...
Endpoint = vpn.example.net:51820
AllowedIPs = 10.20.0.0/16

现在对比服务器为同一个客户端写的条目。它描述服务器会向客户端发送什么,通常是客户端的隧道地址,而不是私有子网。

[Peer]
PublicKey = ...
AllowedIPs = 10.99.0.2/32

把这两者调换,是我最常看到的错误。把 10.20.0.0/16 放在服务器的 peer 段下面,等于声明整个子网都在那个客户端后面,这是站点到站点的声称,而不是许可。不匹配任何 peer 的 AllowedIPs 的数据包会被静默丢弃,所以一条写错的条目看起来和防火墙问题一模一样。

如果你自己管理路由,就把 Table = off 设在 [Interface] 下面,并加上你要的路由。那样就不会有任何自动添加,所以路由必须写在那段把接口拉起来的配置里。

OpenVPN:先推送路由,再确认它真的落地

在普通服务器上,改动就是服务器配置里客户端地址池旁边的一行指令。

push "route 10.20.0.0 255.255.0.0"

客户端连接时会加上它。失败模式出现在同一个配置又推送了 redirect-gateway,或者有人已经加了 route-nopull 的时候:前者把会话变成全隧道,后者丢弃所有推送的路由,包括你那条。

面对全隧道服务器却需要窄行为的客户端,应该只忽略自己不想要的部分。在 OpenVPN 2.4 及以后版本里,pull-filter 正是为此存在的。

pull-filter ignore "redirect-gateway"
route 10.20.0.0 255.255.0.0

把地址族分开处理:推送的 IPv4 路由对解析到 IPv6 的内部服务毫无作用,那个服务会从隧道走并失败,而其他一切看起来都正常。

当私有子网不在 VPN 服务器上,而在另一个客户端后面时,必须告诉服务器。通过 client-config-dir 为每个客户端加一个文件,里面写 iroute 10.20.0.0 255.255.0.0,并配上对应的 route,让其他客户端也学到这条路径。没有 iroute,服务器就没有理由把这个前缀从那条特定的隧道送下去。

IPsec:流量选择器必须一致

用 IPsec 就没有路由可推。两端协商隧道里包含哪些地址,落在约定选择器之外的流量既不被加密,也不从隧道通过。在 strongSwan 客户端上,选择器是显式写出的。

leftsubnet=192.168.1.0/24
rightsubnet=10.20.0.0/16

如果网关两端都配置了 0.0.0.0/0,IKEv2 的收窄通常会把子 SA 压到两份提议的交集,这正是想要的结果。也有些网关会拒绝与自己配置的选择器不符的提议,日志里显示的是子 SA 失败,而不是路由方面的抱怨。遇到这种情况,改动应该落在网关上,因为客户端路由无法为一个对端不接受的选择器建立隧道。

在 Windows 上,内置客户端把同样的决策藏在两个设置后面。必须打开隧道拆分,并且必须把前缀注册到配置里。

Set-VpnConnection -Name Office -SplitTunneling $true
Add-VpnConnectionRoute -ConnectionName Office -DestinationPrefix 10.20.0.0/16

正是注册前缀,才让它在重连之后依然存在,手工添加的路由做不到这一点。

把端点与 DNS 排除在这个决策之外

VPN 端点必须在不走隧道的情况下保持可达,否则隧道会把自己压垮。当只有 10.20.0.0/16 走路由时这是自动的,因为端点是公网地址。在全隧道配置里就不是这样,客户端在隧道起来之前需要 一条指向端点的显式主机路由。

DNS 是第二个决策,推送路由并不会替你做。内部名称通常只能由私有网络能访问的解析器来解析。如果服务器推送了 DNS 服务器,每个名字都走它;如果不推,内部名称要么解析失败,要么被公开解析到你并不想要的地方。分离 DNS 会在内部解析器上保留一个搜索域,但这取决于客户端的支持,所以要把它当成独立任务,而不是路由的副作用。

先验证,再重启,然后再验证一次

三项检查覆盖路由问题。问内核一个远端地址会走哪里,从子网内部取一点东西,然后确认普通公网请求仍然从你平时的连接出去。

ip route get 10.20.5.7
curl -sS http://10.20.5.7/health
curl -sS https://ifconfig.me

第一行会给出接口名。如果它给出的是你的物理接口,说明路由根本没进去。在 Windows 上 Find-NetRoute -RemoteIPAddress 10.20.5.7 回答同一个问题,最后一项检查仍应显示你平时的地址,而不是 VPN 服务器的地址。

然后重启。手工加的路由会消失,而来自配置、来自推送配置或来自 AllowedIPs 的路由会回来。如果窄访问在重启前能用、重启后失效,问题出在隧道如何启动,而不是路由本身。

在宣布完成之前,确认四件事:前缀来自网络所有者,且不与任何本地网段重叠;回程路径存在,或者由伪装规则替代;你配置的机制正是你所在平台上真正强制执行路由的那一个;同样这几项检查在干净重启之后仍然通过。如果回程路径就是反复出问题的那部分,strongSwan 的转发笔记也值得一读。

相关文章