NET_ADMIN 是 Linux 的一项能力,允许进程执行网络管理,而在容器里,它就在进程所处的那一个网络命名空间内完成这件事。它不是打开套接字的许可证,也不能替代设备或路由。真正有趣的问题从来不是能力有没有被授予,而是哪个命名空间收到了这次改动。

这项能力覆盖什么

在自己的命名空间内,持有 NET_ADMIN 的进程可以启停接口、分配地址、增删路由并写入防火墙规则,这正是 capabilities(7) 归入该项能力的那组操作。它还能调整接口级设置,控制内核如何排队和过滤流量。VPN 客户端通常在启动时需要其中几项,之后就不再需要别的。因此多数镜像申请的权限都比实际用到的更大。

该命名空间之外的操作不在覆盖范围内,相邻特权各自独立。原始数据包的读写属于 NET_RAW,而创建 tun 设备既需要该能力,也需要 对设备节点的访问。缺少设备的进程会得到看起来像网络问题、实际却不是的权限错误。

命名空间决定影响范围

位于自身桥接网络上的容器只能触达自己的 loopback、自己的接口和自己的路由表。在那里授予 NET_ADMIN,VPN 客户端就能在不触碰主机路由决策的情况下建立隧道,这也是多数容器镜像假定的安排。

把这个容器切换到主机网络,同样的授权就会改动主机的路由和主机的防火墙,因为不再有独立的命名空间。原本只负责一条隧道的客户端,现在可能改写整台机器的流量走向,而它拆除脚本中的一处失误会在主机上留下没人写过的规则。能力本身没有任何变化,变化的是上下文。同一个 compose 文件在两台主机上表现不同,原因就在这里。

被合并成一项的三个前提

VPN 容器启动失败通常涉及三件彼此独立的事:能力、隧道设备和内核对该协议的支持。一次把三样都给足确实能跑通,但也让下一次失败无从归因。一份最小的 compose 片段会授予能力并显式传入设备节点:

services:
  vpn:
    image: example/vpn-client:latest
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1

该镜像需要哪些 sysctl,请阅读镜像自身的文档,因为容器里的 sysctl 可能需要额外特权或主机级设置。在镜像只支持纯用户空间模式的地方,根本不需要设备,能力列表也相应缩短。

授予窄操作,而不是授予模式

特权模式会交出全部能力,同时扩大设备访问,于是掩盖了三个前提中究竟缺哪一个。它还让容器的失败方式与主机改动难以区分。即使写起来更长,也应优先选择显式列表。

无根和用户命名空间部署会改变能力的实际含义,因为进程可能只在映射后的命名空间内持有它。把能力列表从有根示例复制到无根运行时并期待同样的权限,是常见的浪费时间的方式。

机密属于另一个决定。持有 NET_ADMIN 并不说明客户端出示哪种对端身份、加载哪些凭据,也不说明它们如何轮换。网络管理并不提供信任。

核实改动并记录授权的理由

在排查隧道之前,先确认容器实际拿到了什么。下面的检查会报告新增的能力和网络模式,然后展示从内部看到的接口列表:

docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.NetworkMode}} {{.HostConfig.Privileged}}' vpn
docker exec vpn sh -c 'grep CapEff /proc/self/status'
docker exec vpn ip -br addr
docker exec vpn ip route

如果能力存在而接口列表仍为空,说明客户端还没来得及创建任何东西就失败了,接下来该看容器日志。如果隧道存在但流量不通,容器内部的路由表会回答下一个问题。

也要测试拆除路径,因为写入防火墙规则的客户端可能把它们留在身后。停掉容器,如果使用主机网络,再检查一次主机的规则集。只在启动时需要两秒的授权,不应在没有说明的情况下被 compose 文件写成永久要求。

在每项能力旁边写下理由:哪个操作需要它,没有它会怎样失败。这条注释能挺过镜像更新,也正是经过审阅的特权清单与靠复制粘贴长出来的清单之间的区别。

相关文章