用于 VPN 的 Linux 网络命名空间:它们没有隔离什么
命名空间移动的是接口、路由和套接字,而不是文件或账户。以下是独立 netns 中的 VPN 覆盖了什么,以及流量仍然从哪里泄漏出去。
在它自己的命名空间里启动 VPN,在里面运行一个应用,流量仍然走宿主机的路由出去。什么都没坏:命名空间只管辖进入它的进程,而这次泄漏来自一个从未进入的进程。接口、地址、路由、防火墙规则和套接字才是命名空间所拥有的。文件、账户和已在运行的服务则不是,大多数意外正来自这里。
命名空间拥有什么,套接字在哪里诞生
把一个接口移进命名空间,宿主机就失去它;在里面建一条隧道,宿主机就看不到它。这就是受命名空间限制的 VPN 的机制:设备和指向它的路由存在于只有被选定的进程才能触及的上下文里。宿主机保留自己的默认路由,所以外面的一切都不受影响。
对 WireGuard 来说,创建的上下文和当前所在位置同样重要:UDP 套接字绑定在创建该接口的命名空间里,即使设备之后搬去了别处。加密数据包走哪张路由表,取决于这个出生地。当套接字留在一个没有通往对端路由的上下文里时,接口会去查表,握手永远出不去。先检查它的出生地,再检查它当前的位置。
命名空间没有隔离什么
这里没有任何东西隔离文件系统、账户或密钥,而且这些边界不会因为创建了命名空间就出现。解析器配置是一个文件,所以内部进程仍然可以读到一份指向宿主机解析器的设置,并让查询沿它隐含的路由发出去。把请求交给已在运行的中间代理或系统服务的应用,牵涉的是完全另一个上下文中的进程。两者都是这道边界从未打算回答的请求。
解决办法是在 /etc/netns/<name>/resolv.conf 放一份按命名空间区分的文件,或 bind mount 一份准备好的文件,指向从内部可达的解析器。在宿主机上编辑 /etc/resolv.conf 不会改变命名空间看到的任何东西。
进程的放置才是控制手段
你得到的隔离,正是你在里面启动的那些进程的隔离。通过命名空间 exec 辅助程序启动的 shell 及其产生的一切都留在里面;从宿主机会话启动的桌面应用则不会。创建上下文,移动设备,然后证明两边:
ip netns add vpn1
ip link set wg0 netns vpn1
ip netns exec vpn1 wg show
wg show
在命名空间内部读第一个 wg show,在宿主机上读第二个,预期只有其中之一会列出对端。若两者都列出,说明设备从未移动;若都不列出,说明它去了别处。当启动方是服务管理器时,要改的是 unit 文件,而不是桌面快捷方式。缺少命名空间设置的 unit 会在宿主机上下文里启动守护进程,悄悄让整套设计失效。
从应用的上下文测试
用应用所用的同一个二进制、同一个用户,在命名空间内部执行检查。要包含 DNS 和两种地址族,因为你从未考虑过的 IPv6 路由是常见的出口。从你自己的 shell 里做的测试,只能证明你的 shell 被隔离了。
然后在宿主机上执行同样的检查,确认答案不同:内部是隧道地址和预期的出口,外部是宿主机路由和办公室出口,以及按名字解析的解析器。命名空间要按部署而不是按协议命名,这样清理脚本不会把两个弄混。
转发、故障策略与清理
对于网关,要有意识地开启转发,并决定隧道断开时应该发生什么。命名空间不会阻止你在里面安装路由,所以如果你建了一条回落到宿主机的路由,它就会被使用。这是策略决定,应该在故障来检验它之前写下来。
记录命名空间的名称、接口以及谁拥有配置,让清理只移除部署所创建的东西。回滚时重置宿主机网络,会把清理变成故障。详见 network_namespaces(7) 一页虽短,在设计这条路径之前值得一读。