让 OpenVPN 默认不走 VPN

LUG VPN 的一些用户希望仅对某些特定 IP 使用 VPN,而 OpenVPN 默认是全部走 VPN。也许是我的搜索能力太差,竟然没有 Google 到靠谱的答案。没有耐心的读者可直接看我的解决方案:

$ echo “script-security 2” >>/etc/openvpn/client.conf
$ echo “up /usr/local/bin/remove-ovpn-defroute” >>/etc/openvpn/client.conf

$ cat /usr/local/bin/remove-ovpn-defroute

#!/bin/sh
(
sleep 2 # wait for routing table to be flushed
ip route del 0.0.0.0/1 dev tun0
ip route del 128.0.0.0/1 dev tun0
) &
exit 0

OpenVPN 是如何让全部流量走 VPN 的呢?

$ ip route
10.8.0.37 dev tun0 proto kernel scope link src 10.8.0.38
202.141.160.99 via 202.141.162.126 dev eth0
202.141.162.0/25 dev eth0 proto kernel scope link src 202.141.162.123
10.8.0.0/16 via 10.8.0.37 dev tun0
0.0.0.0/1 via 10.8.0.37 dev tun0
128.0.0.0/1 via 10.8.0.37 dev tun0
default via 202.141.162.126 dev eth0

上面的示例中,

  • 10.8.0.37 是 OpenVPN server 分配的 IP
  • 10.8.0.0/16 是 OpenVPN 子网(这个 VPN 网络比较大,/24 可能不够用)
  • 202.141.160.99 是 OpenVPN server 的 IP
  • 202.141.162.126 是 OpenVPN client 的默认网关
    可以看到,0.0.0.0/1 和 128.0.0.0/1 这两条路由表项的优先级高于 default route,而且涵盖了所有的IP。这就是 OpenVPN client 在连接成功后添加的。如果大家在 console 下启动 OpenVPN,就能在输出日志中发现。

为什么 OpenVPN 不直接覆盖默认路由,而是添加了这么奇怪的两项呢?因为 OpenVPN 连接断开时还要把路由表恢复到初始状态,采用这种 0.0.0.0/1 和 128.0.0.0/1 的办法,只需在 VPN 连接断开时删除这两条路由表项即可,无需存储原来的默认路由。

如何让 OpenVPN 在连接建立的时候不添加这两条规则呢?网上有一种说法是在 /etc/openvpn/client.conf(你的 client 配置文件)中加入一行 route no-pull,就不会添加路由表项了。不过,这样也不会添加 10.8.0.0/16 的路由表项,VPN 网络中 client 间无法通信(注:OpenVPN server 要配置 client-to-client 才能使 client 间互相通信),我们再也无从得知 VPN 子网的IP范围。

另一种办法是在连接建立后,删除这两条新加的路由表项。OpenVPN client 提供了 up 参数,可以在连接建立时执行指定命令。如果执行的命令是外部脚本,还需要指定 script-security 2 参数以允许执行外部脚本。在外部脚本里这么写行不行呢?

#!/bin/sh
ip route del 0.0.0.0/1 dev tun0
ip route del 128.0.0.0/1 dev tun0

No. 因为 up 参数指定的脚本是在修改路由表之前执行,所以尽管此时 VPN 连接已经建立,这两条命令删除的事实上是不存在的规则,还会以错误码退出,OpenVPN client 检测到错误码会中断连接。

在脚本最后加个 exit 0 呢?也不行,脚本执行完了,什么效果也没有,然后 0.0.0.0/1 和 128.0.0.0/1 的路由表项被加上了。这个脚本不再会被调用,因此达不到清除默认路由的目的。

因此我采用了变通的方案:fork 出一个脚本进程,主进程正常退出,子进程等待2秒后(估计路由规则已经设置完毕),再删除这两条规则。事实上设置路由规则用不了2秒,这只是一个保险的设置。

#!/bin/sh
(
sleep 2 # wait for routing table to be flushed
ip route del 0.0.0.0/1 dev tun0
ip route del 128.0.0.0/1 dev tun0

# insert your routes here

) &
exit 0
如果用户希望添加某些特定的 IP 走 VPN,可以写在两条 ip route add 命令之后。VPN 网关的 IP 可以从 ip addr show dev tun0 的输出中 grep/sed/awk 出来,如下例中的 10.8.0.41。

$ ip addr show dev tun0
3: tun0: mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 100
link/none
inet 10.8.0.42 peer 10.8.0.41/32 scope global tun0

$ ip route
default via 192.168.0.1 dev eth0
10.8.0.0/16 via 10.8.0.41 dev tun0
10.8.0.41 dev tun0 proto kernel scope link src 10.8.0.42
192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.101
202.141.160.99 via 192.168.0.1 dev eth0
这种做法的最大缺陷是所有流量会走 VPN 至多2秒,一些 TCP 连接可能因此中断。除了 route no-pull 以外,有没有更和谐的解决方案呢?