用 UID 规则把单个 Linux 应用固定到 VPN
UID 规则只能看到本地套接字,而常见的 loopback 例外会杀死隧道。先为账号安排路由,再逐步过滤并验证。
把单个程序固定到 VPN 在 Linux 上主要是一个路由问题,防火墙规则只决定选好路由之后发生什么。如果你在隧道掌握默认路由之前就在 OUTPUT 里匹配程序的 UID,规则会丢掉你本想保护的那部分流量。同一条规则也看不到应答程序 DNS 查询的解析进程。把顺序理对,一个无特权账号就能成为可靠的分流选择器。
先给账号一条路由,再下结论
所有者匹配只回答一个问题:这个 UID 名下的本地套接字是否构造了这个包?它不选接口,也无法搬动主表已经发往物理网卡的流量。因此没有匹配路由的 UID 规则只会拦住程序,而不会把它改道。
先让账号走它自己的表,再让防火墙确认结果。这在一台 iproute2 支持 uidrange 的主机上有效,ip rule help 会用一行报告出来。表 100 保留一条经隧道的默认路由,规则只把 UID 1000 送进该表。其余流量留在主表,继续走物理链路。
ip rule add uidrange 1000-1000 table 100 ip route add default dev tun0 table 100
需要另一条隧道的第二个账号会拿到表 101 和自己的规则,因为两个配置不能同时拥有同一张表里的默认路由。NetworkManager 配置是系统级的,所以这部分仍需手工处理;重启后规则会消失,除非有东西把它们恢复。把表写进隧道定义旁边的启动脚本,这样重新连接就不会让某个账号留在错误的路径上。
为什么 loopback 例外会杀死隧道
大多数照抄的规则版本会丢掉每一个不从 loopback 出去的包。隧道不是 loopback,所以这种写法连隧道流量也一起丢掉,结果看起来像 VPN 挂了,而不是防火墙出错。
等到 OUTPUT 运行时,内核已经选好了出口接口,所以 -o 是与路由的答案比较,而不是与包头比较。所有者匹配 只在 OUTPUT 和 POSTROUTING 中被查询,转发的包不携带可供它读取的套接字。这个组合意味着例外必须点名隧道接口并否定那项匹配,而不是否定 loopback。
loopback 仍然值得在 DROP 之上拥有自己的 ACCEPT,因为程序的第一个 DNS 包会经 lo 去往本地 stub。把两条规则按这个顺序放好,账号就能到达 stub,然后到达隧道,别的什么也到不了。
iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP
IPv6 是另一套规则,只做 v4 的那一对会让 ip6tables 保持原样。偏好 AAAA 记录的程序就会经物理链路出去,永远到不了 DROP。把两条规则都镜像到 ip6tables,或者在隧道不携带任何 v6 地址时在主机关掉 IPv6。手工添加的规则在重启时同样会消失,所以要么用 netfilter-persistent save 保存,要么把同样的逻辑重建为由启动单元加载的 nftables 表。
DNS 应答来自另一个进程
程序发往 127.0.0.53 的查询会通过 loopback 的 ACCEPT,而 UID 匹配正是在那里止步。stub 解析器用自己的服务账号向上游转发,所以绑定到 vpnapp 的规则永远看不到第二个查询。如果解析器为这条链路挑了局域网服务器,应答会经物理网卡返回,而程序自己的包仍留在隧道里。可见的结果是隧道能通但域名坏了,或者域名由你并未选择的服务器来回答。
修域名解析要动隧道链路,而不是动应用。给隧道配置一个 DNS 服务器和一个路由域,然后用 resolvectl query 和 resolvectl status tun0 确认解析器对重要的名字确实用了它。在网络命名空间里问题会消失,因为该空间的解析器只认识隧道。
从账号本身检查规则
问内核某个 UID 拿到哪条路由,然后以该账号发一个请求,把出口地址与你自己的账号比较。如果两者报告同一个地址,说明规则匹配的并不是你测试的流量。下面的第一条命令在不发包的情况下显示路由查找,这能把路由问题和防火墙问题分开。
ip route get 1.1.1.1 uid 1000 sudo -u vpnapp curl -s https://ifconfig.co
计数器显示规则到底有没有命中。运行 iptables -vnL OUTPUT,读取每条规则旁的包数,把真实流量之后 DROP 旁边的零当作匹配错误。
被转发的流量不在这个设计范围内。网桥后面的容器或虚拟机没有属于 vpnapp 的本地套接字,所以所有者匹配永远不会触发,那些包改走 FORWARD 策略。如果必须覆盖这类负载,请单独测试,并写一条绑定到网桥或容器地址的策略。只有当表、接口例外、解析器和 v6 规则都指向同一个方向时,这个设计才成立;漏掉其中任何一个,账号要么泄漏,要么断网。