你无法通过编辑路由让某个程序免于走 WireGuard 主机的全隧道。路由按目的地选路,而你想豁免的是进程,内核没有可比对的东西。可行的做法是给那个进程自己的网络上下文:一个带有自己的接口、地址和默认路由的命名空间,通过 veth 对连接到主机。我在 Ubuntu 24.04 上用 wireguard-tools 1.0.20210914 运行过,命名空间从来不是问题——问题出在主机转发策略和隧道的标记规则上。

先选择你需要的上下文类型

实践中会出现两种形态。如果主机运行全隧道,而某个程序必须走 ISP 路径出去,你就建立第二个命名空间,把它连接到主机,让主机为它转发并转换流量。如果命名空间已经带有 VPN 接口,你只需要在其中启动一条命令,那就是常见的ip netns exec情形,大部分路由工作已经完成。

两种形态都不会给你一条只限于单个进程 ID 的规则。Netfilter 匹配地址、端口、标记和接口;没有按进程的路由表。标记可以引导流量,但标记必须打在你已经识别出的数据包上,而这是低一层的同一个问题。命名空间把那棵进程树的整个栈隔离开,所以这才是通常的答案。

如果要求真的是不让任何东西离开隧道,那就停掉隧道。命名空间是为混合场景存在的:一个程序出去,其余全部进来,同一台机器,同一时刻。

建立第二个上下文并把它连接到主机

一个示例布局:主机有一个承载默认路由的 WireGuard 接口,还有一个名为clearnet的网络命名空间,通过 veth 对连接到主机。下面的地址只是示例。

ip netns add clearnet
ip link add veth-clear-out type veth peer name veth-clear-in
ip addr add 192.168.100.1/24 dev veth-clear-out
ip link set veth-clear-out up
ip link set veth-clear-in netns clearnet
ip netns exec clearnet ip addr add 192.168.100.2/24 dev veth-clear-in
ip netns exec clearnet ip link set veth-clear-in up
ip netns exec clearnet ip route add default via 192.168.100.1

这个命名空间现在能到达主机,别的什么也到不了。主机必须转发流量并改写源地址,因为 192.168.100.0/24 是私有地址,上级路由器没有回到它的路由:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o <wan-interface> -j MASQUERADE

继续之前先确认命名空间这一侧。ip netns exec clearnet ip route应当显示 veth 路由和经由 192.168.100.1 的默认路由,发往该地址的 ping 应当有回应。如果 ping 主机地址失败,说明地址或链路状态不对,任何防火墙规则都帮不上忙。

当名字能解析但什么都不回来时,去读 FORWARD

典型故障看起来像路由 bug:命名空间解析出名字,数据包发出去了,回应却始终不来。在 FORWARD 策略为 DROP 的主机上——好几个发行版和大多数加固镜像都是这样——数据包在路上被丢弃,而应用打印的错误对过滤只字不提。

在添加任何东西之前先读链:

iptables -S FORWARD
nft list chain inet filter forward

然后放行两个方向的流量,带上计数器,这样你才能看出规则是否曾经命中:

iptables -A FORWARD -i veth-clear-out -o <wan-interface> -j ACCEPT
iptables -A FORWARD -i <wan-interface> -o veth-clear-out -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
watch -n1 'iptables -L FORWARD -v -n'

计数器为零的规则不在路径上。要么接口名和你以为的不同,要么链中更早的规则先接受、丢弃或返回了数据包。修正顺序,而不是规则文本。

回应流量是人们忘记的那一半。返回规则需要同样的 veth 名称加上连接状态匹配,或者如果你让策略保持开放,就用简单的接口接受。只添加出站规则,原始症状就会回来,而你的新规则立在那里看起来完全正确。

让隧道的标记规则远离新路径

这一步会把一个能用的命名空间变成一个漏的命名空间。WireGuard 配置通常连同自己的路由规则和独立表一起安装防火墙标记策略,因此未标记的数据包回落到主表,而已标记的使用隧道表。从命名空间转发出去的流量在出口处会按策略路由评估,取决于那些规则是怎么写的,它可能落进隧道表。

两个补充把路径分开。在 mangle 链中为命名空间的子网提前返回,并添加一条基于源的规则,把该子网放进主表:

iptables -t mangle -I PREROUTING -s 192.168.100.0/24 -j RETURN
ip rule add from 192.168.100.0/24 lookup main priority 100

优先级 100 在我的主机上排在隧道规则之前,所以对该源由普通表决定。不要盲目照抄这个数字:wg-quick安装的优先级在不同版本以及 IPv4 与 IPv6 配置之间都不一样,所以请运行ip rule show并选择一个在你的机器上排在隧道规则之前的优先级。

不要相信规则列表,用针对源的查询来验证:

ip route get 1.1.1.1 from 192.168.100.2
ip netns exec clearnet ip route get 1.1.1.1

如果第一条命令报告的是 WireGuard 接口,新规则还不在隧道策略之前。如果它报告的是 WAN 接口,而第二条仍然显示 veth 默认路由,说明主机和命名空间并不一致,你应该重新读两张路由表。

启动打开连接的那个进程

启动命令只有一行,但请启动真正创建套接字的进程。ip netns exec clearnet curl -s https://ifconfig.me是最小的诚实测试:它显示命名空间向世界呈现的地址,而不是主机会用的地址。

对于服务,通过路径把命名空间挂上去,让进程整个生命周期都待在里面:

[Service]
NetworkNamespacePath=/run/netns/clearnet

图形应用正是单行做法悄悄失败的地方。从菜单启动的浏览器往往把请求交给已经在命名空间之外运行的实例,于是新窗口出现了,流量仍然走隧道出去。给它一个单独的配置文件,让新实例真的被创建,然后检查进程的上下文而不是猜:

readlink /proc/$(pgrep -f clearnet-profile | head -1)/ns/net
readlink /run/netns/clearnet

两个输出必须一致。同样的检查适用于任何长期运行的程序,值得对每个应用做一次,而不是假设启动器做了你想做的事。

名字解析需要自己的决定。创建命名空间不会给它解析器配置;每个命名空间的文件通过/etc/netns/<name>/resolv.conf支持,没有它你就会继承一种容易被误读的行为。IPv6 也需要同样的处理:如果命名空间没有 v6 默认路由,而应用偏好 v6,请求可能在回退之前就卡住,看起来像隧道坏了。

检查框架,而不只是出口地址

能用的公网地址证明路由,不证明全部要求。按这个顺序走完剩下的部分:

  • ip netns exec clearnet ip route get 1.1.1.1显示的是 veth 默认路由,而不是 WireGuard 接口。
  • 请求进行时,两条规则的 FORWARD 计数都会增长。
  • 应用进程报告的是你预期的命名空间 inode。
  • 重启后命名空间在服务启动之前就存在;更早运行的 unit 会失败,或者更糟,运行在命名空间之外。

然后决定隧道掉线时应该发生什么。命名空间继续工作,这既是这个练习的目的,也是它的风险:被豁免的程序保持连接,而其他一切都停住。把它记录为一项策略选择,并清楚说明豁免不涵盖什么——账户身份、保存的浏览器状态和你的 DNS 提供商都位于网络命名空间的范围之外,把豁免当成匿名正是那个比配置活得更久的错误。

相关文章