主题
实验 15:多租户隔离
实验 14 建了一条隧道,一个 VNI,一个虚拟网络。这一章让多个互相隔离的虚拟网络跑在同一条物理链路上。
这是公有云网络的核心场景:几万个租户共用同一套物理设施,彼此完全看不见。
起环境
bash
cd labs/learn-ovs/12-multitenant
./start.sh1
2
2
text
租户A h1 .1 ─┐ ┌─ h3 .3 租户A
租户B h2 .2 ─┼─ ovs1 ══ underlay ══ ovs2 ┼─ h4 .4 租户B
eth1,eth2 eth3 eth1 eth2,eth3
四台主机的 IP 全在 192.168.10.0/24
underlay: 10.0.0.1/30 ↔ 10.0.0.2/301
2
3
4
5
6
2
3
4
5
6
目标:
| 通信 | 期望 |
|---|---|
| h1 ↔ h3(租户A,跨交换机) | 通 |
| h2 ↔ h4(租户B,跨交换机) | 通 |
| h1 ↔ h2(不同租户,同一台交换机) | 不通 |
| h1 ↔ h4(不同租户,跨交换机) | 不通 |
注意第三行
h1 和 h2 插在同一台交换机上,IP 在同一网段,却必须不通。这说明隔离和物理位置、IP 规划都没关系,完全由虚拟网络的归属决定。
方案:一租户一网桥一 VNI
最直白的做法,也是本章实验采用的:
text
ovs1 ovs2
┌────────────────────┐ ┌────────────────────┐
│ br-t100 │ │ br-t100 │
│ eth1(h1) │ VNI 100 │ eth2(h3) │
│ vx-t100 ══════════╪════════════════╪══ vx-t100 │
├────────────────────┤ ├────────────────────┤
│ br-t200 │ VNI 200 │ br-t200 │
│ eth2(h2) │ │ eth3(h4) │
│ vx-t200 ══════════╪════════════════╪══ vx-t200 │
└────────────────────┘ └────────────────────┘
└────────── 同一条物理链路 ──────────┘1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
隔离由两层共同保证:
- 不同网桥 —— OVS 网桥之间天然不通,MAC 表也各自独立
- 不同 VNI —— 封装之后在 underlay 上也是分开的
配置
每台交换机上重复一遍相同的模式:
bash
N1="docker exec clab-multitenant-ovs1"
# 租户A
$N1 ovs-vsctl add-br br-t100
$N1 ovs-vsctl add-port br-t100 eth1
$N1 ovs-vsctl add-port br-t100 vx-t100 \
-- set Interface vx-t100 type=vxlan \
options:remote_ip=10.0.0.2 options:key=100
# 租户B
$N1 ovs-vsctl add-br br-t200
$N1 ovs-vsctl add-port br-t200 eth2
$N1 ovs-vsctl add-port br-t200 vx-t200 \
-- set Interface vx-t200 type=vxlan \
options:remote_ip=10.0.0.2 options:key=2001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ovs2 上对称配置(remote_ip 换成 10.0.0.1,接口换成 eth2/eth3)。完整命令见 solve.sh。
验证隔离
bash
docker exec clab-multitenant-h1 ping -c 3 192.168.10.3 # 租户A 跨机
docker exec clab-multitenant-h2 ping -c 3 192.168.10.4 # 租户B 跨机
docker exec clab-multitenant-h1 ping -c 2 -W 1 192.168.10.2 # 跨租户,同机
docker exec clab-multitenant-h1 ping -c 2 -W 1 192.168.10.4 # 跨租户,跨机1
2
3
4
2
3
4
text
h1 → h3: 0% packet loss ✓ 通
h2 → h4: 0% packet loss ✓ 通
h1 → h2: 100% packet loss ✓ 隔离
h1 → h4: 100% packet loss ✓ 隔离1
2
3
4
2
3
4
两个 VNI 在同一条线上并存
抓 underlay:
bash
docker exec -d clab-multitenant-ovs2 \
sh -c 'timeout 10 tcpdump -i eth1 -n -l udp port 4789 > /tmp/cap.txt 2>&1'
# 同时让两个租户都打流量
docker exec clab-multitenant-h1 ping -c 2 192.168.10.3
docker exec clab-multitenant-h2 ping -c 2 192.168.10.41
2
3
4
5
2
3
4
5
text
10.0.0.1.42547 > 10.0.0.2.4789: VXLAN, vni 100
10.0.0.1.37444 > 10.0.0.2.4789: VXLAN, vni 200
10.0.0.2.54954 > 10.0.0.1.4789: VXLAN, vni 100
10.0.0.2.41009 > 10.0.0.1.4789: VXLAN, vni 2001
2
3
4
2
3
4
同一对 IP、同一个 UDP 端口、同一条物理链路,靠 VNI 区分租户。 物理网络完全不知道上面跑着两个虚拟网络。
MAC 表各自独立
bash
$N1 ovs-appctl fdb/show br-t100
$N1 ovs-appctl fdb/show br-t2001
2
2
text
--- br-t100(租户A)---
port VLAN MAC Age
1 0 aa:c1:ab:83:4f:e1 5 ← h1
2 0 aa:c1:ab:31:f9:f7 5 ← h3,从隧道学到
--- br-t200(租户B)---
port VLAN MAC Age
1 0 aa:c1:ab:16:91:f2 5 ← h2
2 0 aa:c1:ab:ec:ad:41 5 ← h4,从隧道学到1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
两张完全独立的表。租户A 的交换机从来没见过租户B 的 MAC 地址 —— 这是比"看得见但拦住"强得多的隔离。
datapath 里仍然只有一个隧道设备
bash
$N1 ovs-dpctl show1
text
port 0: ovs-system (internal)
port 1: br-t100 (internal)
port 2: eth1
port 3: vxlan_sys_4789 (vxlan: packet_type=ptap)
port 4: br-t200 (internal)
port 5: eth21
2
3
4
5
6
2
3
4
5
6
两个租户、两个隧道端口,datapath 里共用一个 vxlan_sys_4789。区分靠的是包携带的隧道元数据(tun_id + 对端 IP),不是设备。
这个方案的局限
一租户一网桥,简单直观,但规模上去就撑不住:
- 1000 个租户 = 1000 个网桥 + 1000 个隧道端口,配置和管理成本极高
- 每加一个宿主机,每个租户都要加一条隧道 —— N 台机器 M 个租户就是 N×M 条隧道端口
生产环境用的是另一套。
生产方案:key=flow + br-int / br-tun
核心是把 VNI 从配置变成流表决定的字段:
bash
ovs-vsctl add-port br-tun vx \
-- set Interface vx type=vxlan \
options:remote_ip=10.0.0.2 options:key=flow1
2
3
2
3
key=flow 表示"VNI 不固定,由流表的 set_field:...->tun_id 动作决定"。一个隧道端口就能承载所有租户。
为什么必须拆成两个网桥
直觉上,在单个网桥里给主机口配 tag,再用流表把 VLAN 映射成 VNI,似乎就够了。但这样是不通的 —— 用 trace 看一下就知道为什么:
bash
ovs-appctl ofproto/trace br-int "in_port=1,dl_src=<h1MAC>,dl_dst=ff:ff:ff:ff:ff:ff"1
text
Flow: in_port=1,vlan_tci=0x0000, ... ← 注意这里 vlan_tci 是 0
0. priority 0
NORMAL
Datapath actions: push_vlan(vid=100,pcp=0),1,...1
2
3
4
2
3
4
关键发现:Port.tag 是在 NORMAL 动作内部生效的,流表在 table 0 看到的 vlan_tci 仍然是 0。
所以 dl_vlan=100 这样的匹配条件永远命中不了。这不是配置错误,是 OVS 的处理顺序决定的。
解法是把职责拆到两个网桥,中间用 patch 连接:
text
┌──────────────────────────┐ ┌──────────────────────────┐
│ br-int │ │ br-tun │
│ eth1 tag=100 (h1) │ │ │
│ eth2 tag=200 (h2) │ │ 流表做 VLAN ↔ VNI 映射 │
│ patch-tun ══════════════╪════════╪══ patch-int │
│ │ 帧带着 │ vx (key=flow) │
│ NORMAL 在这里应用 tag │ VLAN 标签│ │
└──────────────────────────┘ └──────────────────────────┘1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
帧穿过 patch 进入 br-tun 时,已经带着真实的 VLAN 标签了 —— 这时候流表才匹配得上。
br-tun 上的流表:
bash
# 出方向:VLAN → VNI,摘掉 VLAN 标签
add-flow br-tun "in_port=<patch>,dl_vlan=100,actions=strip_vlan,set_field:100->tun_id,output:<vx>"
add-flow br-tun "in_port=<patch>,dl_vlan=200,actions=strip_vlan,set_field:200->tun_id,output:<vx>"
# 入方向:VNI → VLAN,重新打上标签
add-flow br-tun "in_port=<vx>,tun_id=100,actions=push_vlan:0x8100,mod_vlan_vid:100,output:<patch>"
add-flow br-tun "in_port=<vx>,tun_id=200,actions=push_vlan:0x8100,mod_vlan_vid:200,output:<patch>"1
2
3
4
5
6
7
2
3
4
5
6
7
验证一下,trace 把两个网桥的分工显示得清清楚楚:
text
bridge("br-int")
----------------
0. priority 0
NORMAL
-> forwarding to learned port ← tag 在这里生效
bridge("br-tun")
----------------
0. in_port=1,dl_vlan=100, priority 200 ← 帧现在带着 VLAN 100 了
pop_vlan
set_field:0x64->tun_id
output:2
-> output to kernel tunnel
Datapath actions: set(tunnel(tun_id=0x64,dst=10.0.0.2,...)),51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
underlay 上依然是按租户分开的 VNI:
text
8 vni 100
8 vni 2001
2
2
这就是 OpenStack Neutron 的架构
br-int + br-tun 这两个名字不是我编的,就是 OpenStack 计算节点上真实的网桥名。你现在知道它们为什么要分开了。
本地用 VLAN 做租户标识(每台机器内部有效),跨机器时映射成 VNI。 VLAN 只有 4094 个不要紧 —— 它只在单台宿主机内部使用,不需要全局唯一。
两种方案对比
| 一租户一网桥 | key=flow + br-int/br-tun | |
|---|---|---|
| 网桥数量 | 每租户一个 | 固定两个 |
| 隧道端口 | 每租户每对端一个 | 每对端一个 |
| 配置复杂度 | 低 | 高(需要流表) |
| 规模上限 | 几十个租户 | 几万个租户 |
| 适合 | 学习、小规模固定场景 | 生产环境 |
先用一租户一网桥理解隔离机制,再看懂 key=flow 的必要性。 顺序反了很难理解为什么要搞那么复杂。
自动检查
bash
./verify.sh1
text
[通过] ovs1 上有两个租户网桥
[通过] 租户A 的 VNI 是 100
[通过] 租户B 的 VNI 是 200
[通过] 租户A 跨交换机互通:h1 → h3
[通过] 租户B 跨交换机互通:h2 → h4
[通过] 租户间隔离:h1 不能到 h2(同一台交换机)
[通过] 租户间隔离:h1 不能到 h4(跨交换机)
[通过] underlay 上看到 VNI 100 的包
[通过] underlay 上看到 VNI 200 的包1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
自测清单
- h1 和 h2 在同一台交换机、同一个网段,为什么不通?
- 一租户一网桥的方案,隔离由哪两层保证?
- 两个租户的流量在 underlay 上靠什么区分?
- 两个租户的隧道在 datapath 里是两个设备还是一个?
options:key=flow是什么意思?解决了什么问题?- 为什么单个网桥里
dl_vlan=100的规则匹配不上? - OpenStack 的 br-int 和 br-tun 各负责什么?
常见问题
某个租户不通,另一个正常
对比两个租户的配置差异:
bash
ovs-vsctl get Interface vx-t100 options:key
ovs-vsctl get Interface vx-t200 options:key
ovs-vsctl list-ports br-t100
ovs-vsctl list-ports br-t2001
2
3
4
2
3
4
最常见的是两端 VNI 写错一个,或者主机接错了网桥。
租户之间居然能通
检查是不是把两个租户的口接进了同一个网桥:
bash
ovs-vsctl list-ports br-t100 # 应该只有本租户的口 + 隧道口1
也可能是残留的旧网桥还在。ovs-vsctl list-br 看一眼有没有多余的。
key=flow 配了但包发不出去
key=flow 时必须有流表设置 tun_id,否则 VNI 是 0。用 trace 确认:
bash
ovs-appctl ofproto/trace br-tun "in_port=1,dl_vlan=100,..."1
看 Datapath actions 里的 tun_id= 是不是你期望的值。tun_id=0x0 说明流表没生效。
想知道生产环境是怎么下发这些流表的
不是手工敲的。OVN、Neutron 的 agent 会根据数据库里的逻辑网络定义自动生成流表并下发。第 5 部分的外部控制器和 OVN 简介会讲这个链路。
小结
- 多租户隔离和物理位置、IP 规划无关,完全由虚拟网络归属决定
- 一租户一网桥一 VNI 是最直观的方案:网桥隔离 + VNI 隔离双保险
- 多个 VNI 共用同一条物理链路、同一个 datapath 隧道设备,靠隧道元数据区分
- 各租户的 MAC 表完全独立,一方根本不知道另一方的存在
- 这个方案在几十个租户以上就不可维护(网桥数和隧道数爆炸)
- 生产方案是
options:key=flow:VNI 由流表决定,一个隧道端口承载所有租户 Port.tag在NORMAL内部生效,流表在 table 0 看到的vlan_tci是 0 —— 这是必须拆成 br-int / br-tun 的根本原因- OpenStack 的做法是本地用 VLAN 标识租户,跨机映射成 VNI
第 4 部分到此结束。下一部分进入高级特性:有状态防火墙、QoS、外部控制器、用户态 datapath。