多租户容器网络的四次返工:n² 条规则、SDN、和一个没解开的 vlan 网关

给每个客户开一个容器,最难的从来不是开容器,是让他们彼此看不见。

这一轮我们为多租户容器做了四次方案返工,把过程记下来——踩过的坑比结论有用。

第一次:全挂在同一个网桥上

最开始图省事,所有客户容器都接在宿主的主网桥上,跟节点自己同一个二层广播域。

后果很直接:客户 A 的容器能扫到客户 B 的端口,还能扫到宿主的 SSH。这在托管业务里不是性能问题,是事故级的安全问题。

第二次:每客户一个裸网桥

方案是给每个客户建一个独立网桥,不接任何物理口,配自己的网段和网关地址,再挂一小组 iptables 规则做双向隔离。

核心几条:


# 建独立网桥,不接物理口
ip link add vmbrN type bridge
ip link set vmbrN up
ip addr add 10.NN.0.1/24 dev vmbrN

# 出网:只放行「出去」和「回程」
iptables -t nat -A POSTROUTING -s 10.NN.0.0/24 -o vmbr0 -j MASQUERADE
iptables -A FORWARD -i vmbrN -o vmbr0 -j ACCEPT
iptables -A FORWARD -i vmbr0 -o vmbrN -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# 同组互通,跨组一律丢弃
iptables -A FORWARD -i vmbrN -o vmbrM -j DROP

实测下来五条都对:同组容器互通、跨组不通、访问宿主不通、访问同网段的别家容器不通、公网正常。

但账单在后头。上面那条「跨组丢弃」我写成了双层循环——每两个网桥之间配一条规则,n 个客户就是 n² 条:


for i in $(seq 1 $N); do
  for j in $(seq 1 $N); do
    [ "$i" = "$j" ] && continue
    iptables -A LU-FWD -i vmbr$i -o vmbr$j -j DROP
  done
done

算一下规模就明白问题在哪:

客户数 DROP 规则数 转发路径要过的规则
10 90 可忽略
20 380 开始有感
50 2450 每个包顺着走一遍

iptables 是线性匹配。这不是「以后优化」的问题,这是规模天花板。

第三次:上 PVE 原生 SDN

既然宿主是 PVE,第一反应是——让宿主自己管。PVE 从 6.x 起就带 SDN,能定义 zone 和 vnet。

先试 simple zone,直接报错:


unexpected property 'bridge'

翻源码才明白,simple zone 的属性只有 dns / reversedns / dnszone / dhcp 四个,压根不吃 bridge 参数。它的定位是「每个 vnet 一个裸网桥,不做 VLAN」——本质上就是第二次那套方案,只是换成 PVE 来管。

它同样不配网关 IP,同样不做 NAT。

真正能省规则的是 vlan zone:只用一个物理网桥,每个客户打一个 VLAN tag,隔离交给网桥的 vlan filter 完成。这条路上规则数是 O(n),不是 n²。

第四次:开 vlan-aware,然后卡在最后一步

vlan zone 要求底层网桥支持 VLAN 过滤,得先给主网桥加一行:


iface vmbr0 inet static
	address 192.168.2.6/24
	gateway 192.168.2.1
	bridge-ports nic1
	bridge-stp off
	bridge-fd 0
	bridge-vlan-aware yes

然后 ifreload -a。这一步有断网风险——网桥要重建。实测结果还算温和:重载完 bridge/vlan_filtering 读到 1,节点网络正常,跑在上面的容器没有掉线。

接着建 zone 和两个 vnet:


pvesh create /cluster/sdn/zones --zone lu --type vlan --bridge vmbr0
pvesh create /cluster/sdn/vnets --vnet lu0101 --zone lu --tag 101
pvesh create /cluster/sdn/vnets --vnet lu0102 --zone lu --tag 102
pvesh set /cluster/sdn

PVE 自动生成配置,落盘长这样:


auto lu0101
iface lu0101
	bridge_ports vmbr0.101
	bridge_stp off
	bridge_fd 0

到这一步,容器加 bridge=lu0101 就能挂上,隔离由 VLAN 完成,规则数降到 O(n)。

然后卡在最后一步:给 vmbr0.101 配网关地址。

ifup vmbr0.101 不报错、不输出、也不配。ifdown 再来一遍,一样。原因不难猜——ifupdown2 认为这个设备已经由 SDN 创建、并且作为 lu0101 的 port 存在,于是跳过 inet 段。

试过走 SDN 的 subnet 接口:


pvesh create /cluster/sdn/vnets/lu0101/subnets \
  --subnet 10.10.0.0/24 --type subnet --gateway 10.10.0.1

它只是写进 IPAM 记录,不会碰宿主接口。

四种方案放一起

方案 规则复杂度 隔离强度 网关 IP NAT 出网 判断
全在一个网桥 无 无 现成 现成 否决,安全不合格
每客户裸网桥 n² 强 手配 手配 可跑,20 户以内
SDN simple zone n² 强 手配(待验) 手配 只是换了个管家
SDN vlan zone n 强 卡住 手配 待解

两个结论

一,SDN 只管拓扑和隔离,不管网关 IP 和 NAT。

这两件事它没有开关。所谓「全交给 PVE」是个幻觉——你能省掉的是网桥和规则的维护,省不掉的是地址规划和出网策略。想清楚这一点,就不会指望某一层能替你兜底。

二,规则数才是规模的天花板。

十户的时候 n² 无所谓,五十户就是两千多条,每个包都要顺着走一遍。要往上走,只有三条路:iptables 加 ipset 做集合匹配、换 nftables 用原生 set、或者干脆交给 VLAN。

下一步:再试一次 simple zone——那种 vnet 是裸网桥,IP 应该能直配。如果也不通,就退回裸网桥加 ipset,至少先把规则数压到 O(n)。

没解开的坑也是坑,记下来比假装没有强。

💬 留下你的爪印