使用已建立的 Linux VPN 的浏览器不会打开 /dev/net/tun。它打开一个套接字,而该套接字的报文从哪个接口发出,由内核决定。创建隧道的进程所做的工作,与承载其流量的进程不同。把这两个角色分开,大多数权限问题在被提出之前就有了答案。

追踪一个报文从套接字到隧道的旅程

普通的应用调用 connect 或 send,内核为目的地查找路由。如果选中的路由指向隧道接口,报文就会排入 VPN 守护进程读取的设备队列。守护进程将它加密,再通过物理接口发出去。那段描述里的每一个箭头,都是可以回答一个权限问题的地方。

这条路径上没有任何环节要求应用了解该设备。守护进程持有文件描述符并执行接口操作,而应用只看到一个普通的网络 API。

把两半各画一次,权限问题就更容易安放。上半部分是应用的世界,下半部分属于守护进程。

判断谁需要设备,谁不需要

创建或接入 TUN 接口需要设备访问权限,此外还需要 配置它的能力,因此受支持的 VPN 服务以网络管理权限启动,或使用一个特权辅助程序。这项要求属于守护进程。它对浏览器、数据库客户端或文件传输工具没有任何要求。需要这项能力的服务,是为自己的配置步骤而需要它,不是为其客户端。

一旦路由和策略就位,普通进程就用平常的套接字调用走这条路径。它不需要设备节点,也不需要 root。

修改设备权限并不是给应用授予目的地访问权的方式。它只是把设备边界挪到了别处。

检查应用实际运行在哪个命名空间

容器或第二个网络命名空间会为其进程提供独立的接口集合和路由表。主机上的隧道不会自动出现在那里,任何设备权限的改动都改变不了这一点。应用只会经由该命名空间提供的东西进行路由。这就是容器镜像不可能意外继承主机隧道的原因。

用 ip netns list 和 ip -n NAME route 检查,或者查看该进程的命名空间链接。答案通常一条命令就能看到。

同一台主机上的两个用户也可以被策略赋予不同的路径。这是网络决策,而不是设备归属的证据。

把能力与网络策略分开

有些工具需要的不止是套接字。抓包需要原始套接字,更改路由需要管理权限。它们是各自独立的操作,各有各的授权,授予其中一项不应被理解为使用 VPN 的一揽子前提。

自行实现隧道的程序属于第三种情况。它确实会打开 TUN,因为它承担了守护进程的角色,所以才需要守护进程的特权。

先说出操作,再说出特权。两者通常比看上去更容易分开。

用两个账户验证这种分离

以服务身份连接隧道,然后以普通用户运行应用,确认它能访问私有资源。在该用户的 shell 里对目的地运行 ip route get,看看选中的是哪个接口。如果路由正确而连接仍然失败,问题在策略而不是特权。

由此得到的设计是带有明确边界的最小权限:一个组件拥有配置和隧道资源,工作负载保留各自的普通账户,由网络策略决定什么可以通过。把所有东西都以 root 运行,只会让这些角色崩塌,而什么也解释不了。

相关文章