ᕕ( ᐛ )ᕗ Jimyag's Blog

GENEVE 协议简介与 VNI 多租户隔离实验

· 1748 words · ~ 9 min read

云计算数据中心里,一台物理服务器上可能同时运行数百个虚拟机或容器,分属不同租户。这些租户需要各自独立的 L2 网络,互相不可见——但物理网络的 VLAN 标签只有 12 位,最多 4096 个网络,远远不够用。Overlay 网络因此而生:把虚拟的 L2 帧封装在物理网络的 UDP/IP 包里传输,让虚拟网络的数量不再受物理硬件限制。

GENEVE 是目前最新、最具扩展性的 Overlay 封装协议,被 OVN、AWS Nitro、Cilium 等主流项目采用。本文从它诞生的历史背景讲起,逐层剖析其协议细节,最后在单台机器上做完整实验。


1. 历史背景:封装协议的碎片化

在 GENEVE 之前,业界已经有多种 Overlay 封装协议,但各自为政,互不兼容:

协议 提出者 RFC 特点
NVGRE Microsoft RFC 7637 GRE 封装,Key 字段做租户 ID
STT Nicira (VMware) 草案 TCP-like 封装,利用硬件 TSO
VXLAN VMware/Cisco RFC 7348 UDP 封装,24 位 VNI,最流行
GENEVE VMware/MS/Red Hat/Intel RFC 8926 UDP 封装,可变长 Options

VXLAN 解决了 VLAN 数量不足的问题,但它的 8 字节固定头部没有扩展空间——一旦需要携带额外的元数据(如安全策略 ID、服务链标签、QoS 标记),就只能在内层帧外再套一层封装,或者依赖带外信道。

不同 SDN 控制器(NSX、OpenStack OVN、AWS 等)为了传递各自需要的元数据,纷纷自定义私有格式,导致设备互通困难,硬件卸载芯片无法统一支持。

GENEVE(Generic Network Virtualization Encapsulation)正是为了终结这种碎片化而设计:提供一个统一的、可扩展的封装框架,让各方在同一个协议头部里携带任意元数据,同时对硬件解析友好。


2. GENEVE 协议详解

完整数据包结构

一个携带以太网内层帧的 GENEVE 数据包,从外到内的层次如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌──────────────────────────────────────┐
│  外层以太网头  (14 字节)              │  物理网络寻址
├──────────────────────────────────────┤
│  外层 IP 头    (20/40 字节)          │  VTEP 间路由
├──────────────────────────────────────┤
│  外层 UDP 头   (8 字节, 目的端口 6081)│  多路复用 + ECMP 哈希
├──────────────────────────────────────┤
│  GENEVE 头     (8 字节固定 + Options) │  隧道标识 + 元数据
├──────────────────────────────────────┤
│  内层以太网帧  (≥14 字节)            │  租户虚拟网络
├──────────────────────────────────────┤
│  内层 IP / TCP / 载荷                │
└──────────────────────────────────────┘

外层 UDP 使用目的端口 6081(VXLAN 是 4789),源端口由发送方基于内层五元组哈希生成,确保 ECMP 等价多路径负载均衡能正确分流。

GENEVE 固定头部(8 字节)

1
2
3
4
5
6
7
 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver|  Opt Len  |O|C|   Rsvd.   |         Protocol Type         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       Virtual Network Identifier (VNI)        |    Reserved   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段 位宽 说明
Ver 2 协议版本,当前固定为 0
Opt Len 6 Options 长度,单位为 4 字节,取值 0–63,即最多 252 字节 Options
O 1 OAM 控制包标志,置 1 时内层帧为管理报文而非数据
C 1 Critical Options 标志,置 1 时接收方必须理解所有 Critical 选项,否则丢弃
Rsvd 6 保留,发送方置 0
Protocol Type 16 内层协议类型:0x6558 = 透明以太网桥接(最常用),0x0800 = IPv4,0x86DD = IPv6
VNI 24 Virtual Network Identifier,24 位 = 1677 万个虚拟网络
Reserved 8 保留,置 0

TLV Options(0–252 字节)

这是 GENEVE 相对 VXLAN 最核心的差异点。Options 区域由一到多个 TLV(Type-Length-Value)条目组成:

1
2
3
4
5
6
7
 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Option Class          |      Type     |R|R|R|  Length  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Option Data (变长)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段 位宽 说明
Option Class 16 定义选项的组织,类似以太网 OUI。0x0100 = Linux 内核,0x0101 = Open vSwitch,0xFFFE = 实验用
Type 8 在该 Class 内的选项编号,最高位为 1 时表示 Critical
R 3 保留
Length 5 Option Data 的长度,单位为 4 字节,最多 124 字节
Option Data 变长 实际携带的元数据内容

硬件友好性:不认识某个 Option Class/Type 的设备可以根据 Length 跳过该选项(除非 Critical 位为 1),而不需要解析选项内容。这使得新版本的选项格式不会导致旧设备彻底无法处理数据包,只是跳过未知部分。

OVN 如何使用 GENEVE Options

OVN(Open Virtual Network)是使用 GENEVE 元数据最典型的案例。它在 Options 中携带两个关键信息:

  • Logical Datapath:标识数据包当前所在的逻辑网络(相当于租户 VPC)
  • Logical Port:标识数据包的来源或目的逻辑端口

这让 Hypervisor 上的 OVS 在收到隧道包时,不需要查外部数据库,直接从 GENEVE Options 里读出包的逻辑身份,决定转发策略。这是 GENEVE Options 让 SDN 控制面简化的典型体现。


3. GENEVE 相对 VXLAN 的核心优势

VXLAN 的 8 字节固定头部里,除了 24 位 VNI,只有一个 I 标志位(表示 VNI 有效),没有任何扩展空间。需要携带元数据时只能走以下绕路方案:

  • 在内层帧外再套一层封装(增加头部开销)
  • 把元数据编码进 VNI 本身(牺牲 VNI 空间,且语义不清)
  • 依赖带外信道(控制平面额外通信)

GENEVE 通过 TLV Options 机制,将这些需求内化到协议本身:

能力 VXLAN GENEVE
VNI 位宽 24 位 24 位
最大虚拟网络数 1677 万 1677 万
携带元数据 不支持 最多 252 字节 TLV
头部长度 固定 8 字节 8 字节 + Options
硬件跳过未知选项 N/A 支持
Critical 选项标记 不支持 支持
UDP 目的端口 4789 6081
主要使用者 Kubernetes Flannel/Calico OVN、AWS Nitro、Cilium

4. 主流项目中的 GENEVE

OVN:默认隧道协议就是 GENEVE,所有 Hypervisor 之间的流量都走 GENEVE 封装,并在 Options 中携带逻辑数据路径和端口信息。

AWS Nitro:AWS 的 Nitro 卡(Smart NIC)使用 GENEVE 封装实例之间的网络流量,并通过 Options 传递安全组策略标签,实现硬件卸载时的策略执行。

Cilium:CNI 插件支持 GENEVE 模式(--tunnel=geneve),作为 VXLAN 的替代,在需要携带 eBPF 元数据时更有优势。

Open vSwitch(OVS):OVS 2.5+ 支持 GENEVE,可以通过 ovs-vsctl 创建 GENEVE 类型的 Port,并读写 Options。


5. 实验:单台物理机验证 VNI 隔离

用两个 network namespace 模拟两台物理主机,通过 veth pair + Linux bridge 构建 underlay,在上面配置两套 GENEVE 隧道(VNI 100 和 VNI 200)。两套隧道使用完全相同的 overlay IP,用 VRF 让内核路由表按 VNI 查各自的路由,演示不同租户 IP 地址空间完全隔离。

拓扑

  flowchart TB
    subgraph H1["ns-h1(模拟主机 1,underlay: 192.168.100.1)"]
        direction TB
        V100_H1["vrf100\n路由表 100"]
        V200_H1["vrf200\n路由表 200"]
        G0_H1["geneve0\nVNI=100\n10.0.0.1/24"]
        G1_H1["geneve1\nVNI=200\n10.0.0.1/24"]
        V100_H1 --> G0_H1
        V200_H1 --> G1_H1
    end

    BR["br-underlay\nLinux bridge(模拟物理交换机)\nunderlay: 192.168.100.0/24"]

    subgraph H2["ns-h2(模拟主机 2,underlay: 192.168.100.2)"]
        direction TB
        V100_H2["vrf100\n路由表 100"]
        V200_H2["vrf200\n路由表 200"]
        G0_H2["geneve0\nVNI=100\n10.0.0.2/24"]
        G1_H2["geneve1\nVNI=200\n10.0.0.2/24"]
        V100_H2 --> G0_H2
        V200_H2 --> G1_H2
    end

    G0_H1 -- "GENEVE VNI=100\nUDP 6081" --- BR
    G1_H1 -- "GENEVE VNI=200\nUDP 6081" --- BR
    BR -- "GENEVE VNI=100\nUDP 6081" --- G0_H2
    BR -- "GENEVE VNI=200\nUDP 6081" --- G1_H2

    style G0_H1 fill:#4a90d9,color:#fff
    style G0_H2 fill:#4a90d9,color:#fff
    style G1_H1 fill:#e67e22,color:#fff
    style G1_H2 fill:#e67e22,color:#fff
    style BR fill:#27ae60,color:#fff

搭建 Underlay 网络

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 创建两个 namespace,分别模拟两台物理主机
ip netns add ns-h1
ip netns add ns-h2

# 创建两对 veth,每对一端留在根 namespace,另一端放入对应的 namespace
# veth1 <-> veth1-br:连接 ns-h1 和 bridge
# veth2 <-> veth2-br:连接 ns-h2 和 bridge
ip link add veth1 type veth peer name veth1-br
ip link add veth2 type veth peer name veth2-br

# 将 veth1/veth2 移入各自的 namespace(模拟主机的物理网卡)
ip link set veth1 netns ns-h1
ip link set veth2 netns ns-h2

# 创建 Linux bridge,模拟连接两台主机的物理交换机
ip link add br-underlay type bridge

# 将根 namespace 侧的 veth 端口接入 bridge
ip link set veth1-br master br-underlay
ip link set veth2-br master br-underlay

# 启动 bridge 及其两个端口
ip link set veth1-br up
ip link set veth2-br up
ip link set br-underlay up

# 启动 ns-h1 内的回环和网卡,并配置 underlay IP
ip netns exec ns-h1 ip link set lo up
ip netns exec ns-h1 ip link set veth1 up
ip netns exec ns-h1 ip addr add 192.168.100.1/24 dev veth1

# 启动 ns-h2 内的回环和网卡,并配置 underlay IP
ip netns exec ns-h2 ip link set lo up
ip netns exec ns-h2 ip link set veth2 up
ip netns exec ns-h2 ip addr add 192.168.100.2/24 dev veth2

# 验证 underlay 连通(两台"主机"能互相 ping 通)
ip netns exec ns-h1 ping -c2 192.168.100.2

配置 VRF + GENEVE

在 ns-h1 和 ns-h2 上各建两个 VRF,每个 VRF 绑定一张独立路由表,再将对应 VNI 的 GENEVE 接口加入该 VRF:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ── ns-h1 ────────────────────────────────────────────────────

# 创建 VRF vrf100,使用路由表 100(隔离 VNI 100 租户的路由)
ip netns exec ns-h1 ip link add vrf100 type vrf table 100
# 创建 VRF vrf200,使用路由表 200(隔离 VNI 200 租户的路由)
ip netns exec ns-h1 ip link add vrf200 type vrf table 200
# 启动两个 VRF 接口
ip netns exec ns-h1 ip link set vrf100 up
ip netns exec ns-h1 ip link set vrf200 up

# 创建 GENEVE 隧道接口 geneve0:VNI=100,对端 underlay IP 为 ns-h2(192.168.100.2)
ip netns exec ns-h1 ip link add geneve0 type geneve id 100 remote 192.168.100.2
# 将 geneve0 加入 vrf100,此后 geneve0 的路由只在路由表 100 中查找
ip netns exec ns-h1 ip link set geneve0 master vrf100
# 分配 VNI 100 租户的 overlay IP(10.0.0.1/24)
ip netns exec ns-h1 ip addr add 10.0.0.1/24 dev geneve0
ip netns exec ns-h1 ip link set geneve0 up

# 创建 GENEVE 隧道接口 geneve1:VNI=200,同样指向 ns-h2
ip netns exec ns-h1 ip link add geneve1 type geneve id 200 remote 192.168.100.2
# 将 geneve1 加入 vrf200,路由完全独立于 vrf100
ip netns exec ns-h1 ip link set geneve1 master vrf200
# 分配与 VNI 100 完全相同的 overlay IP(10.0.0.1/24),VRF 保证不冲突
ip netns exec ns-h1 ip addr add 10.0.0.1/24 dev geneve1
ip netns exec ns-h1 ip link set geneve1 up

# ── ns-h2(对称配置)────────────────────────────────────────

ip netns exec ns-h2 ip link add vrf100 type vrf table 100
ip netns exec ns-h2 ip link add vrf200 type vrf table 200
ip netns exec ns-h2 ip link set vrf100 up
ip netns exec ns-h2 ip link set vrf200 up

# VNI=100 隧道,对端 underlay IP 为 ns-h1(192.168.100.1)
ip netns exec ns-h2 ip link add geneve0 type geneve id 100 remote 192.168.100.1
ip netns exec ns-h2 ip link set geneve0 master vrf100
ip netns exec ns-h2 ip addr add 10.0.0.2/24 dev geneve0
ip netns exec ns-h2 ip link set geneve0 up

# VNI=200 隧道,同样指向 ns-h1
ip netns exec ns-h2 ip link add geneve1 type geneve id 200 remote 192.168.100.1
ip netns exec ns-h2 ip link set geneve1 master vrf200
ip netns exec ns-h2 ip addr add 10.0.0.2/24 dev geneve1
ip netns exec ns-h2 ip link set geneve1 up

验证隔离

ip vrf exec <vrf名> 在指定 VRF 的路由上下文中执行命令,相当于切换到对应租户视角:

1
2
3
4
5
# 以 VNI 100 租户身份 ping 10.0.0.2:查 vrf100 路由表 → 走 geneve0(VNI=100)
ip netns exec ns-h1 ip vrf exec vrf100 ping -c3 10.0.0.2 &

# 以 VNI 200 租户身份 ping 同一个 10.0.0.2:查 vrf200 路由表 → 走 geneve1(VNI=200)
ip netns exec ns-h1 ip vrf exec vrf200 ping -c3 10.0.0.2 &

同时在另一个终端抓 underlay 接口上的 GENEVE 包:

1
2
# 抓 bridge 端口上的 UDP 6081 流量,过滤出 Geneve 行和内层 IP 行
tcpdump -i veth1-br -nn udp port 6081 -v 2>/dev/null | grep -E "Geneve|10\.0\.0"

实际输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tcpdump -i veth1-br -nn udp port 6081 -v 2>/dev/null | grep -E "Geneve|10\.0\.0"
    192.168.100.2.51913 > 192.168.100.1.6081: Geneve, Flags [none], vni 0xc8
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0xc8
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28083, seq 1, length 64
    192.168.100.2.6677 > 192.168.100.1.6081: Geneve, Flags [none], vni 0xc8
    10.0.0.2 > 10.0.0.1: ICMP echo reply, id 28083, seq 1, length 64
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0x64
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28082, seq 1, length 64
    192.168.100.2.6677 > 192.168.100.1.6081: Geneve, Flags [none], vni 0x64
    10.0.0.2 > 10.0.0.1: ICMP echo reply, id 28082, seq 1, length 64
    192.168.100.1.61607 > 192.168.100.2.6081: Geneve, Flags [none], vni 0x64
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0xc8
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28083, seq 2, length 64
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0x64
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28082, seq 2, length 64
    192.168.100.2.6677 > 192.168.100.1.6081: Geneve, Flags [none], vni 0xc8
    10.0.0.2 > 10.0.0.1: ICMP echo reply, id 28083, seq 2, length 64
    192.168.100.2.6677 > 192.168.100.1.6081: Geneve, Flags [none], vni 0x64
    10.0.0.2 > 10.0.0.1: ICMP echo reply, id 28082, seq 2, length 64
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0xc8
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28083, seq 3, length 64
    192.168.100.1.6677 > 192.168.100.2.6081: Geneve, Flags [none], vni 0x64
    10.0.0.1 > 10.0.0.2: ICMP echo request, id 28082, seq 3, length 64

三点可以直接从输出读出:

  • vni 0x64(十进制 100)和 vni 0xc8(十进制 200)交替出现,两条流量并行跑在各自的隧道里,互不干扰。
  • 两个 VNI 的内层 IP 完全相同(10.0.0.1 → 10.0.0.2),证明不同租户可以复用同一套地址空间。
  • id 28082id 28083 是两条 ping 各自的 ICMP 标识符,接收端按 VNI 分别解封装到 geneve0(vrf100)或 geneve1(vrf200),不会混淆。

清理实验环境

1
2
3
4
5
6
# 删除 namespace 时,其中所有接口(含 VRF 和 GENEVE 接口)自动销毁
ip netns del ns-h1
ip netns del ns-h2

# 删除 bridge(根 namespace 侧的 veth 端口随 bridge 一起消失)
ip link del br-underlay

7. 总结

GENEVE 解决的核心问题是:如何在一个统一的 Overlay 封装协议里,既做到 24 位 VNI 的大规模租户隔离,又能携带任意扩展元数据,同时对硬件解析友好

它没有在 VXLAN 上打补丁,而是从头设计了可变长 TLV Options 机制——定义了一个"元数据总线",让不同的 SDN 系统(OVN、AWS、Cilium 等)各自在 Option Class 命名空间下写入自己需要的元数据,而其他不认识这些选项的设备可以安全跳过。

从实验可以直观看到:两套 GENEVE 隧道使用完全相同的 overlay IP,各自走各自的 VNI 封装,在 underlay 上共用同一条 UDP 通道,在接收端由内核按 VNI 值分发到对应接口——这正是云计算多租户网络隔离的数据平面基础。

#Linux #GENEVE #VXLAN #Overlay #网络虚拟化 #OVN #Kernel