Skip to content

实验 15:多租户隔离 ​

实验 14 建了一条隧道,一个 VNI,一个虚拟网络。这一章让多个互相隔离的虚拟网络跑在同一条物理链路上。

这是公有云网络的核心场景:几万个租户共用同一套物理设施,彼此完全看不见。

起环境 ​

bash
cd labs/learn-ovs/12-multitenant
./start.sh
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/30

目标:

通信期望
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. 不同网桥 —— OVS 网桥之间天然不通,MAC 表也各自独立
  2. 不同 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=200

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  # 跨租户,跨机
text
h1 → h3:  0% packet loss       ✓ 通
h2 → h4:  0% packet loss       ✓ 通
h1 → h2:  100% packet loss     ✓ 隔离
h1 → h4:  100% packet loss     ✓ 隔离

两个 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.4
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 200

同一对 IP、同一个 UDP 端口、同一条物理链路,靠 VNI 区分租户。 物理网络完全不知道上面跑着两个虚拟网络。

MAC 表各自独立 ​

bash
$N1 ovs-appctl fdb/show br-t100
$N1 ovs-appctl fdb/show br-t200
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,从隧道学到

两张完全独立的表。租户A 的交换机从来没见过租户B 的 MAC 地址 —— 这是比"看得见但拦住"强得多的隔离。

datapath 里仍然只有一个隧道设备 ​

bash
$N1 ovs-dpctl show
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: eth2

两个租户、两个隧道端口,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=flow

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"
text
Flow: in_port=1,vlan_tci=0x0000, ...        ← 注意这里 vlan_tci 是 0
 0. priority 0
    NORMAL
Datapath actions: push_vlan(vid=100,pcp=0),1,...

关键发现: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 标签│                          │
   └──────────────────────────┘        └──────────────────────────┘

帧穿过 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>"

验证一下,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,...)),5

underlay 上依然是按租户分开的 VNI:

text
      8 vni 100
      8 vni 200

这就是 OpenStack Neutron 的架构

br-int + br-tun 这两个名字不是我编的,就是 OpenStack 计算节点上真实的网桥名。你现在知道它们为什么要分开了。

本地用 VLAN 做租户标识(每台机器内部有效),跨机器时映射成 VNI。 VLAN 只有 4094 个不要紧 —— 它只在单台宿主机内部使用,不需要全局唯一。

两种方案对比 ​

一租户一网桥key=flow + br-int/br-tun
网桥数量每租户一个固定两个
隧道端口每租户每对端一个每对端一个
配置复杂度低高(需要流表)
规模上限几十个租户几万个租户
适合学习、小规模固定场景生产环境

先用一租户一网桥理解隔离机制,再看懂 key=flow 的必要性。 顺序反了很难理解为什么要搞那么复杂。

自动检查 ​

bash
./verify.sh
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. h1 和 h2 在同一台交换机、同一个网段,为什么不通?
  2. 一租户一网桥的方案,隔离由哪两层保证?
  3. 两个租户的流量在 underlay 上靠什么区分?
  4. 两个租户的隧道在 datapath 里是两个设备还是一个?
  5. options:key=flow 是什么意思?解决了什么问题?
  6. 为什么单个网桥里 dl_vlan=100 的规则匹配不上?
  7. 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-t200

最常见的是两端 VNI 写错一个,或者主机接错了网桥。

租户之间居然能通 ​

检查是不是把两个租户的口接进了同一个网桥:

bash
ovs-vsctl list-ports br-t100      # 应该只有本租户的口 + 隧道口

也可能是残留的旧网桥还在。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,..."

看 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。