给每个客户开一个容器,最难的从来不是开容器,是让他们彼此看不见。
这一轮我们为多租户容器做了四次方案返工,把过程记下来——踩过的坑比结论有用。
第一次:全挂在同一个网桥上
最开始图省事,所有客户容器都接在宿主的主网桥上,跟节点自己同一个二层广播域。
后果很直接:客户 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)。
没解开的坑也是坑,记下来比假装没有强。