云计算数据中心里,一台物理服务器上可能同时运行数百个虚拟机或容器,分属不同租户。这些租户需要各自独立的 L2 网络,互相不可见——但物理网络的 VLAN 标签只有 12 位,最多 4096 个网络,远远不够用。Overlay 网络因此而生:把虚拟的 L2 帧封装在物理网络的 UDP/IP 包里传输,让虚拟网络的数量不再受物理硬件限制。
GENEVE 是目前最新、最具扩展性的 Overlay 封装协议,被 OVN、AWS Nitro、Cilium 等主流项目采用。本文从它诞生的历史背景讲起,逐层剖析其协议细节,最后在单台机器上做完整实验。
在 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)正是为了终结这种碎片化而设计:提供一个统一的、可扩展的封装框架,让各方在同一个协议头部里携带任意元数据,同时对硬件解析友好。
一个携带以太网内层帧的 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 等价多路径负载均衡能正确分流。
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 |
这是 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(Open Virtual Network)是使用 GENEVE 元数据最典型的案例。它在 Options 中携带两个关键信息:
- Logical Datapath:标识数据包当前所在的逻辑网络(相当于租户 VPC)
- Logical Port:标识数据包的来源或目的逻辑端口
这让 Hypervisor 上的 OVS 在收到隧道包时,不需要查外部数据库,直接从 GENEVE Options 里读出包的逻辑身份,决定转发策略。这是 GENEVE Options 让 SDN 控制面简化的典型体现。
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 |
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。
用两个 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
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
|
在 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 28082 和 id 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
|
GENEVE 解决的核心问题是:如何在一个统一的 Overlay 封装协议里,既做到 24 位 VNI 的大规模租户隔离,又能携带任意扩展元数据,同时对硬件解析友好。
它没有在 VXLAN 上打补丁,而是从头设计了可变长 TLV Options 机制——定义了一个"元数据总线",让不同的 SDN 系统(OVN、AWS、Cilium 等)各自在 Option Class 命名空间下写入自己需要的元数据,而其他不认识这些选项的设备可以安全跳过。
从实验可以直观看到:两套 GENEVE 隧道使用完全相同的 overlay IP,各自走各自的 VNI 封装,在 underlay 上共用同一条 UDP 通道,在接收端由内核按 VNI 值分发到对应接口——这正是云计算多租户网络隔离的数据平面基础。