上一篇我们拆了 netfilter 的五链四表,搞清楚了”一个包能不能走”的问题。但还有一个更基本的问题没回答:一个包应该往哪走?
你在容器里 curl google.com,这个 SYN 包从
veth 出来之后,经过
bridge、经过宿主机的协议栈,最终要从某个物理网卡发出去。Linux
怎么知道应该把它从 eth0 发出去而不是
eth1?下一跳是谁?
答案是路由表。
但在 Kubernetes 的世界里,事情要复杂得多。Pod 分布在不同节点上,节点可能分布在不同的 L2 域甚至不同的数据中心。直接路由走不通的时候,就需要隧道 — 把一个完整的内层数据包塞进另一个外层包里,通过 underlay 网络透明地传输。
这篇文章分两大部分:
- 路由:Linux 路由表的结构、FIB 查找算法、策略路由、ECMP 多路径
- 隧道:IPIP、GRE、VXLAN、Geneve、WireGuard 五种隧道技术的原理和对比
本文的实验环境:Linux 6.x, x86_64。所有命令需要 root 权限。
一、Linux 路由表:不只是一张表
大多数人对路由表的印象就是 ip route show
输出的那张表。但 Linux 路由子系统远比这复杂。
三张内置路由表
Linux 内核维护了多张路由表,通过数字 ID 区分。默认有三张:
| 表 ID | 名称 | 用途 |
|---|---|---|
| 255 | local |
本机地址、广播地址、环回地址。内核自动维护,用户一般不需要改 |
| 254 | main |
标准路由表。ip route
不指定表时操作的就是它 |
| 253 | default |
默认路由表。通常是空的,大多数发行版不使用 |
查看它们:
# 查看 local 表 — 本机地址都在这
$ ip route show table local
local 10.0.0.1 dev eth0 proto kernel scope host src 10.0.0.1
broadcast 10.0.0.0 dev eth0 proto kernel scope link src 10.0.0.1
broadcast 10.0.0.255 dev eth0 proto kernel scope link src 10.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
# 查看 main 表 — 日常打交道的路由
$ ip route show table main
default via 10.0.0.1 dev eth0
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1当一个包需要路由时,内核按照策略路由规则依次查找这些表。默认的查找顺序是:
$ ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default优先级数字越小越先匹配。所以查找顺序是 local
-> main -> default。先查
local 表是因为内核要先确认”这个包是不是发给自己的”。
策略路由:ip rule
如果你只有一个默认网关,三张表就够了。但在多网卡、多出口、VPN 分流等场景下,你需要策略路由(Policy-Based Routing)。
策略路由的核心是 ip rule —
它根据包的属性(源地址、目的地址、fwmark、入接口等)来决定查哪张路由表。
# 创建一张自定义路由表(编号 100,别名 custom)
echo "100 custom" >> /etc/iproute2/rt_tables
# 在 custom 表里添加路由
ip route add default via 192.168.1.1 dev eth1 table custom
# 添加策略:从 10.10.0.0/16 来的包走 custom 表
ip rule add from 10.10.0.0/16 table custom priority 100这在 Kubernetes 里非常常见。比如 Calico 会创建自定义路由表来处理 Pod 的流量,Cilium 用 fwmark 标记包然后通过策略路由分流:
# Cilium 典型的策略路由规则
$ ip rule show
0: from all lookup local
100: from all fwmark 0x200/0xf00 lookup 2004
32766: from all lookup main
32767: from all lookup defaultfwmark 0x200/0xf00 的意思是:取 fwmark 的第
8-11 位(掩码 0xf00),如果值为
0x200,就查表 2004。eBPF
程序在处理包的时候会设置这个
fwmark,从而把特定流量导向特定的路由表。
路由条目的结构
一条路由条目包含这些关键字段:
# 详细查看路由条目
$ ip -d route show
10.244.1.0/24 via 10.0.0.2 dev eth0 proto bird metric 100各字段含义:
- 目的前缀(
10.244.1.0/24):匹配的目的地址范围 - via(
10.0.0.2):下一跳网关的 IP - dev(
eth0):出接口 - proto(
bird):谁添加的这条路由。kernel表示内核自动添加,bird表示 BGP 守护进程添加 - metric(
100):路由优先级,数字越小优先级越高。多条路由匹配时选 metric 最小的 - scope:路由作用域。
link表示直连网段,global表示可以被转发
路由类型也有好几种:
# unicast — 普通路由(默认类型)
10.0.0.0/24 dev eth0
# blackhole — 静默丢弃
ip route add blackhole 192.168.99.0/24
# unreachable — 丢弃并返回 ICMP unreachable
ip route add unreachable 10.99.0.0/16
# prohibit — 丢弃并返回 ICMP prohibited
ip route add prohibit 172.16.0.0/12
# throw — 在当前表中查找失败,继续查下一张表
ip route add throw 10.0.0.0/8 table customthrow 在策略路由里特别有用 —
它让你可以在自定义表里处理部分流量,处理不了的”抛回去”让主表接手。
二、FIB:内核怎么快速查路由
路由表可能有几百甚至上万条目(想想一个大规模 Kubernetes 集群,每个 Pod CIDR 都是一条路由)。每个包都要查一次路由表,这个查找必须极快。
最长前缀匹配
路由查找的核心算法是最长前缀匹配(Longest Prefix Match, LPM)。当多条路由都能匹配目的地址时,选前缀最长(最具体)的那条。
比如目的地址是
10.244.1.5,路由表有这几条:
default via 10.0.0.1 # /0 匹配
10.0.0.0/8 via 10.0.0.1 # /8 匹配
10.244.0.0/16 via 10.0.0.2 # /16 匹配
10.244.1.0/24 via 10.0.0.3 # /24 匹配 <-- 选这条
/24 是最长的匹配前缀,所以选它。
FIB 与 LC-trie
Linux 内核把路由表组织成 FIB(Forwarding Information Base)数据结构。从 2.6.13 开始,Linux 使用 LC-trie(Level Compressed trie)算法来实现 FIB 查找。
传统的 trie(前缀树)把 IP 地址的每一位作为树的一层,32 位地址需要最多 32 层。LC-trie 通过两种压缩手段大幅减少层数:
- Path
compression:如果一段路径上没有分叉,就压缩成一步跳过。比如
10.244.1.0/24和10.244.2.0/24共享前 16 位,这 16 位可以一步跳过 - Level compression:如果某个节点的所有子节点都存在(完全二叉),就把多层合并成一层,用多位索引一次跳转
这两种压缩让查找复杂度从 O(W)(W=地址位数)降到接近 O(log n),其中 n 是路由条目数。
在内核代码里,FIB 查找的入口是
fib_lookup():
// net/ipv4/fib_rules.c
int fib_lookup(struct net *net, struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
// 遍历 ip rule 链
// 对每条 rule 匹配的表,调 fib_table_lookup()
// fib_table_lookup() 内部走 LC-trie
}你可以通过 /proc/net/fib_triestat 查看
LC-trie 的统计信息:
$ cat /proc/net/fib_triestat
Basic info: size of leaf: 48 bytes, size of tnode: 40 bytes.
Main:
Aver depth: 2.60
Max depth: 4
Leaves: 15
Prefixes: 17
Internal nodes: 6
1: 3 2: 2 3: 1
Pointers: 24
Null ptrs: 8
Total size: 2 kBAver depth: 2.60 意味着平均只需要 2.6
次内存访问就能找到路由 — 对于 15
条路由来说已经非常高效了。在大规模场景下(上万条路由),LC-trie
的深度通常也不会超过 6-8 层。
路由缓存的演变
早期 Linux(2.6.38 之前)有一个独立的路由缓存(route cache),缓存最近查过的路由结果。但这个缓存有几个严重问题:
- DDoS 攻击向量:攻击者发大量随机目的 IP 的包,撑爆路由缓存
- 内存消耗不可控:缓存条目数量没有上限
- GC 开销:垃圾回收要遍历整个哈希表
从 3.6 开始,独立的路由缓存被移除了。现在的做法是把缓存信息(nexthop 等)挂在 FIB 条目的下一跳结构上,直接在 trie 查找的结果里返回,不需要单独维护缓存。
三、ECMP:多条路走哪条
如果到同一个目的地有多条等价路由,Linux 支持 ECMP(Equal-Cost Multi-Path)— 把流量分散到多条路径上。
配置 ECMP
# 添加两条等价路由
ip route add 10.244.0.0/16 \
nexthop via 10.0.0.2 dev eth0 weight 1 \
nexthop via 10.0.0.3 dev eth1 weight 1查看效果:
$ ip route show 10.244.0.0/16
10.244.0.0/16
nexthop via 10.0.0.2 dev eth0 weight 1
nexthop via 10.0.0.3 dev eth1 weight 1负载均衡算法
ECMP 的流量分配基于哈希。内核对包的五元组(源IP、目的IP、协议、源端口、目的端口)计算哈希,然后对路径数取模,决定走哪条路径。
这样做的好处是:同一条流的所有包走同一条路径,不会乱序。不同的流则可能走不同的路径。
# 查看和配置哈希模式
$ sysctl net.ipv4.fib_multipath_hash_policy
net.ipv4.fib_multipath_hash_policy = 0
# 0: 基于 L3(源IP、目的IP)
# 1: 基于 L4(加上端口号)— 推荐
# 2: 基于 L3 + 使用 inner header(用于隧道场景)在 Kubernetes 场景中,hash_policy=1(L4
哈希)通常是更好的选择,因为很多连接可能源自同一个 Pod
IP(相同 L3),只有端口不同,L3
哈希会把它们全部导向同一条路径。
权重分流
ECMP 不要求所有路径权重相等。你可以给不同路径设置不同的权重:
# 60% 的流量走 eth0,40% 走 eth1
ip route add 10.244.0.0/16 \
nexthop via 10.0.0.2 dev eth0 weight 3 \
nexthop via 10.0.0.3 dev eth1 weight 2内核把权重转化成哈希桶的数量比例。weight 3 : weight 2 意味着 60% 的哈希值映射到第一条路径,40% 映射到第二条。
在 Calico 里,BGP 通告的多条等价路由会自动形成 ECMP。当一个 Service 的 Endpoint 分布在多个节点上时,发往 Service ClusterIP 的流量就通过 ECMP 分散到各个节点。
四、隧道技术全景
当两个节点不在同一个 L2 网络时,直接路由可能走不通 — 中间的路由器不知道怎么转发你的 Pod CIDR。这时候就需要隧道(tunnel),也叫 overlay network。
隧道的基本思想非常简单:在原始包的外面再套一层头,让中间网络只看到外层头。外层源地址和目的地址是两个节点的物理 IP,中间路由器按正常方式转发。到了对端节点,剥掉外层头,恢复出原始包。
Linux 内核原生支持多种隧道协议。下面我们从最简单的开始,逐步深入。
IPIP
IPIP 是最简单的隧道 — 就是在一个 IP 包外面再套一层 IP 头。
[Outer IP Header][Inner IP Header][TCP/UDP + Payload]
外层 IP 的 protocol 字段设为
4(IPIP),告诉对端”里面还有一个 IP 包”。
# 在 Host A(10.0.0.1)上创建 IPIP 隧道
ip tunnel add tun0 mode ipip local 10.0.0.1 remote 10.0.0.2
ip addr add 10.244.0.1/24 dev tun0
ip link set tun0 up
# 在 Host B(10.0.0.2)上
ip tunnel add tun0 mode ipip local 10.0.0.2 remote 10.0.0.1
ip addr add 10.244.1.1/24 dev tun0
ip link set tun0 up优点:开销最小(只加 20 字节 IP 头),内核支持最成熟。 缺点:只能封装 IP 包(不是二层帧),没有多租户标识,不加密。
Calico 在 IPIPMode: Always 时使用 IPIP
隧道。它的开销比 VXLAN 小 30 字节,但只能做 L3 overlay。
GRE
GRE(Generic Routing Encapsulation)比 IPIP 灵活一些。它在外层 IP 和内层 IP 之间加了一个 GRE 头:
[Outer IP Header][GRE Header][Inner IP Header][TCP/UDP + Payload]
GRE 头最小 4 字节,可以扩展到 16 字节(加上 checksum、key、sequence number 等可选字段)。
# 创建 GRE 隧道
ip tunnel add gre1 mode gre local 10.0.0.1 remote 10.0.0.2 key 42
ip addr add 10.244.0.1/24 dev gre1
ip link set gre1 upkey 字段让你可以在同一对 IP
之间建立多条逻辑隧道。不过 GRE 在 Kubernetes 网络中很少使用
— 它不提供多租户隔离(key 只有 32 位),也不太适合大规模的
overlay 网络。
为什么需要更强的隧道?
IPIP 和 GRE 都是 L3 隧道 — 它们封装的是 IP 包,不是以太网帧。这意味着:
- 不能传输 ARP(没有 L2 头)
- 不能做 L2 多播
- 无法实现跨节点的同一个 L2 域
在云计算和 SDN 场景下,你经常需要让不同物理位置的虚拟机/容器看起来在同一个 L2 网络里 — 这样它们可以用 ARP 发现彼此,用 L2 广播通信。这就需要 L2 over L3 的隧道:VXLAN 和 Geneve。
五、VXLAN 深度拆解
VXLAN(Virtual Extensible LAN,RFC 7348)是目前最广泛使用的 overlay 隧道协议。Flannel 的默认后端是 VXLAN,Cilium 和 Calico 也支持 VXLAN 模式。
封装结构
VXLAN 的封装结构是这样的:
从外到内:
- Outer Ethernet Header(14 字节):外层以太网帧头,目的 MAC 是对端 VTEP 的 MAC
- Outer IP Header(20 字节):外层 IP 头,源/目的是两个节点的物理 IP
- UDP Header(8 字节):目的端口 4789(IANA 标准),源端口是内层包哈希值(利于 ECMP)
- VXLAN Header(8 字节):包含 VNI(VXLAN Network Identifier)
- Inner Ethernet Frame:完整的原始二层帧
总开销是 50 字节(14 + 20 + 8 + 8)。如果 underlay MTU 是 1500,那么 VXLAN 内部的有效 MTU 是 1450。这就是为什么 Kubernetes 集群里经常要调 MTU。
关键概念
VNI(VXLAN Network Identifier):24 位,可以标识 2^24 = 16,777,216 个逻辑网络。对比传统 VLAN 只有 12 位(4096 个)。在 Kubernetes 里,通常整个集群用一个 VNI。
VTEP(VXLAN Tunnel
Endpoint):隧道的端点。每个节点上有一个 VTEP
设备(通常叫 vxlan.calico 或
flannel.1),负责 VXLAN 的封装和解封装。
FDB(Forwarding Database)表:VTEP 需要知道”内层 MAC 地址对应哪个外层 IP”。这个映射存储在 FDB 表里,类似于交换机的 MAC 地址表。
# 查看 VXLAN 设备的 FDB 表
$ bridge fdb show dev vxlan0
aa:bb:cc:dd:ee:01 dst 10.0.0.2 self permanent
aa:bb:cc:dd:ee:02 dst 10.0.0.3 self permanent
00:00:00:00:00:00 dst 10.0.0.2 self permanent
00:00:00:00:00:00 dst 10.0.0.3 self permanent00:00:00:00:00:00 是默认条目,处理未知目的
MAC 的流量(BUM 流量)。
BUM 流量处理
BUM(Broadcast, Unknown unicast, Multicast)流量是 VXLAN 的一个经典难题。当 VTEP 不知道目的 MAC 在哪个节点上时,它必须把包发给所有可能的 VTEP — 这叫泛洪(flooding)。
三种处理方式:
1. 组播(Multicast):把 BUM 流量发到一个组播组,所有 VTEP 都加入这个组。
# 创建使用组播的 VXLAN
ip link add vxlan0 type vxlan id 42 \
group 239.1.1.1 dstport 4789 dev eth0优点是简单自然,缺点是很多数据中心和云环境不支持组播。
2. 头端复制(Head-End Replication):VTEP 自己把包复制多份,分别单播发给每个对端 VTEP。
# 用 FDB 默认条目实现头端复制
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.0.0.2
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.0.0.3
bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 10.0.0.4不需要组播支持,但 VTEP 数量多时复制开销大。
3. 控制平面学习:使用外部控制平面(比如 BGP EVPN、或者 Flannel/Calico 的 agent)直接把 MAC-to-VTEP 的映射注入 FDB 表,避免泛洪。
# Flannel agent 直接写入 FDB 条目
bridge fdb add aa:bb:cc:dd:ee:01 dev flannel.1 dst 10.0.0.2这是 Kubernetes CNI 的主流做法。Flannel 用 etcd/API Server 存储节点信息,每个节点的 agent 维护本地的 FDB 表和 ARP 表,完全避免了 BUM 泛洪。
VXLAN 发包流程
从 Pod A(Node 1)发一个包到 Pod B(Node 2),完整流程是:
- Pod A 发出内层帧,目的 MAC 是网关(
cali*或flannel.1) - 包到达 VTEP 设备
- VTEP 查 FDB 表,找到目的 MAC 对应的远端 VTEP IP
- VTEP 构造外层 UDP/IP/Ethernet 头,VNI 填入 VXLAN 头
- 外层包通过宿主机路由表正常转发到对端节点
- 对端节点的 UDP 端口 4789 收到包,内核识别为 VXLAN
- 解封装,恢复出内层帧
- 内层帧根据内层目的 MAC 转发给 Pod B
为什么 VXLAN 用 UDP?
你可能好奇:为什么不直接用一个新的 IP 协议号?为什么要套一层 UDP?
两个关键原因:
- NAT 穿透:很多中间网络设备只认 TCP 和 UDP。如果用自定义 IP 协议号,NAT 设备可能直接丢弃。UDP 几乎可以穿透任何网络。这和 QUIC 选择在 UDP 上构建的道理一样 — 中间设备对 UDP 最友好
- ECMP 友好:很多交换机做 ECMP 时只看 L4 端口号。VXLAN 的外层源端口是内层包的哈希值,不同的内层流会得到不同的外层源端口,从而被 ECMP 分散到不同的路径上
六、Geneve:为什么它是”最终隧道协议”
VXLAN 有一个根本性的限制:它的头格式是固定的。如果你想在隧道头里携带额外的元数据(安全标签、服务链信息、调试追踪 ID),VXLAN 做不到。
Geneve(Generic Network Virtualization Encapsulation,RFC 8926)就是为了解决这个问题而设计的。IETF 的 NVO3 工作组明确将 Geneve 定位为”最终的网络虚拟化封装协议”。
Geneve 的封装结构
Geneve 的基础结构和 VXLAN 很像:
[Outer Ethernet][Outer IP][UDP dst=6081][Geneve Header][Inner Frame]
但 Geneve 头的设计完全不同:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| Opt Len |O|C| Rsvd. | Protocol Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Virtual Network Identifier (VNI) | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Variable Length Options |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
关键区别在 Variable Length Options — 这是一组 TLV(Type-Length-Value)格式的可扩展选项。
TLV 可扩展性
每个 TLV 选项的结构是:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Option Class | Type |R|R|R| Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Variable Option Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- Option Class(16 位):谁定义的这个选项。厂商可以申请自己的 class
- Type(8 位):选项类型
- Length(5 位):选项数据长度(4 字节为单位,最大 124 字节)
这意味着 Geneve 可以在不修改协议本身的情况下,携带任意的元数据。比如:
TLV: Class=0x0102 Type=1 Length=2 Data=[security_label_id]
TLV: Class=0x0103 Type=5 Length=1 Data=[trace_id_high]
Geneve vs VXLAN
| 特性 | VXLAN | Geneve |
|---|---|---|
| RFC | 7348 (2014) | 8926 (2020) |
| 基础开销 | 50 字节 | 50 字节(无 TLV 时相同) |
| 可扩展性 | 固定头 | TLV 可扩展 |
| 元数据 | 无 | 任意 TLV 选项 |
| VNI | 24-bit | 24-bit |
| UDP 端口 | 4789 | 6081 |
| 内核支持 | 3.7+ | 3.18+ |
| 硬件卸载 | 广泛支持 | 逐步增加 |
| OVS 支持 | 完善 | 原生支持 |
为什么说 Geneve 是”最终隧道协议”?因为它把数据平面和控制平面的语义解耦了。VXLAN 的头格式写死了能携带什么信息,如果未来需要新的元数据,就得定义新的隧道协议。Geneve 不需要 — 加一个 TLV 就行了。
在实际项目中:
- OVN(Open Virtual Network)从一开始就使用 Geneve
- Cilium 从 1.8 开始支持 Geneve,并利用 TLV 携带安全身份信息
- AWS 内部的 overlay 网络也使用 Geneve
# 创建 Geneve 隧道
ip link add geneve0 type geneve id 42 remote 10.0.0.2 dstport 6081
ip addr add 10.244.0.1/24 dev geneve0
ip link set geneve0 up七、WireGuard:加密隧道
前面说的隧道都不加密。在零信任网络和跨数据中心场景下,你需要对 overlay 流量加密。传统方案是 IPsec,但 IPsec 的配置复杂到令人绝望。
WireGuard 是一个现代的加密隧道协议,从 Linux 5.6 开始进入内核主线。它的设计哲学是简单:
- 代码量约 4000 行(对比 IPsec 的几万行)
- 只使用一套密码学原语:Curve25519、ChaCha20-Poly1305、BLAKE2s
- 配置极其简洁
架构
WireGuard 工作在 L3 层,创建一个虚拟网络接口(比如
wg0)。发往这个接口的 IP 包会被加密,封装在 UDP
里发送到对端。
[Outer IP][UDP][WireGuard Header][Encrypted Inner IP Packet]
WireGuard 的开销约 60 字节(20 IP + 8 UDP + 32 WireGuard header,其中包含 16 字节的 Poly1305 MAC)。
基本配置
# 生成密钥对
wg genkey | tee privatekey | wg pubkey > publickey
# 在 Host A(10.0.0.1)上
ip link add wg0 type wireguard
ip addr add 10.244.0.1/24 dev wg0
wg set wg0 listen-port 51820 private-key ./privatekey \
peer <HOST_B_PUBKEY> allowed-ips 10.244.1.0/24 endpoint 10.0.0.2:51820
ip link set wg0 up
# 在 Host B(10.0.0.2)上
ip link add wg0 type wireguard
ip addr add 10.244.1.1/24 dev wg0
wg set wg0 listen-port 51820 private-key ./privatekey \
peer <HOST_A_PUBKEY> allowed-ips 10.244.0.0/24 endpoint 10.0.0.1:51820
ip link set wg0 upallowed-ips 既是路由又是 ACL — 它指定”哪些
IP 范围可以通过这个 peer 发送/接收”。
在 Kubernetes 中的应用
Calico 从 3.14 开始支持 WireGuard 加密 Pod 间流量:
# 一行命令开启全集群加密
kubectl patch felixconfiguration default --type='merge' \
-p '{"spec":{"wireguardEnabled":true}}'开启后,Calico 会自动在每个节点创建
wireguard.cali 接口,节点之间的 Pod
流量全部经过 WireGuard 加密。密钥轮换和 peer
管理完全自动化。
性能方面,WireGuard 的内核实现使用了 SIMD 指令优化加密运算。在现代 CPU 上,ChaCha20-Poly1305 的吞吐量可以达到 10 Gbps 以上,延迟增加只有几微秒。相比 IPsec 的 AES-GCM 模式,性能相当,但配置和维护成本低了一个数量级。
八、实验:手工建立 VXLAN 隧道
理论说够了。下面我们在一台机器上,用两个 network namespace 模拟两个 host,手工建立 VXLAN 隧道,然后用 tcpdump 抓封装后的包。
实验拓扑
ns-host-a (netns) ns-host-b (netns)
+-----------------+ +-----------------+
| 10.244.0.1/24 | | 10.244.1.1/24 |
| vxlan100 | | vxlan100 |
| | | |
| 172.16.0.1/24 | | 172.16.0.2/24 |
| veth-a | | veth-b |
+------+----------+ +----------+------+
| |
| Host (root netns) |
+---------- veth-a-br ---+--- veth-b-br -------+
|
br-underlay
(172.16.0.0/24)
两个 namespace 各有一个 veth 对连到宿主机的 bridge,模拟 underlay 网络。然后在每个 namespace 里建 VXLAN 设备,走 underlay 互通。
搭建步骤
#!/bin/bash
set -e
# 清理旧环境
ip netns del ns-host-a 2>/dev/null || true
ip netns del ns-host-b 2>/dev/null || true
ip link del br-underlay 2>/dev/null || true
# 创建 underlay bridge
ip link add br-underlay type bridge
ip link set br-underlay up
# 创建两个 namespace
ip netns add ns-host-a
ip netns add ns-host-b
# 创建 veth pair for host A
ip link add veth-a type veth peer name veth-a-br
ip link set veth-a netns ns-host-a
ip link set veth-a-br master br-underlay
ip link set veth-a-br up
# 创建 veth pair for host B
ip link add veth-b type veth peer name veth-b-br
ip link set veth-b netns ns-host-b
ip link set veth-b-br master br-underlay
ip link set veth-b-br up
# 配置 host A 的 underlay 地址
ip netns exec ns-host-a bash -c "
ip link set lo up
ip addr add 172.16.0.1/24 dev veth-a
ip link set veth-a up
"
# 配置 host B 的 underlay 地址
ip netns exec ns-host-b bash -c "
ip link set lo up
ip addr add 172.16.0.2/24 dev veth-b
ip link set veth-b up
"
echo "=== 验证 underlay 连通性 ==="
ip netns exec ns-host-a ping -c 2 172.16.0.2
# 在 host A 里创建 VXLAN 隧道
ip netns exec ns-host-a bash -c "
ip link add vxlan100 type vxlan \
id 100 \
local 172.16.0.1 \
remote 172.16.0.2 \
dstport 4789 \
dev veth-a
ip addr add 10.244.0.1/24 dev vxlan100
ip link set vxlan100 up
"
# 在 host B 里创建 VXLAN 隧道
ip netns exec ns-host-b bash -c "
ip link add vxlan100 type vxlan \
id 100 \
local 172.16.0.2 \
remote 172.16.0.1 \
dstport 4789 \
dev veth-b
ip addr add 10.244.1.1/24 dev vxlan100
ip link set vxlan100 up
"
# 添加路由让两个 overlay 网段互通
ip netns exec ns-host-a ip route add 10.244.1.0/24 dev vxlan100
ip netns exec ns-host-b ip route add 10.244.0.0/24 dev vxlan100
echo "=== 验证 VXLAN 隧道连通性 ==="
ip netns exec ns-host-a ping -c 3 10.244.1.1抓取封装后的包
在一个终端里启动 tcpdump,抓 br-underlay 上的流量:
# 抓 underlay bridge 上的 UDP 4789 流量
tcpdump -i br-underlay -nn -e udp port 4789 -vv在另一个终端里 ping:
ip netns exec ns-host-a ping -c 1 10.244.1.1tcpdump 输出类似这样:
14:32:01.123456 a2:b3:c4:d5:e6:01 > a2:b3:c4:d5:e6:02, ethertype IPv4 (0x0800),
length 148: (tos 0x0, ttl 64, id 12345, offset 0, flags [none],
proto UDP (17), length 134)
172.16.0.1.52876 > 172.16.0.2.4789: VXLAN, flags [I] (0x08), vni 100
a2:b3:c4:00:00:01 > a2:b3:c4:00:00:02, ethertype IPv4 (0x0800),
length 98: (tos 0x0, ttl 64, id 54321, offset 0, flags [DF],
proto ICMP (1), length 84)
10.244.0.1 > 10.244.1.1: ICMP echo request, id 1234, seq 1, length 64
逐层拆解:
- 外层
Ethernet:
a2:b3:c4:d5:e6:01 > a2:b3:c4:d5:e6:02— veth 的 MAC 地址 - 外层
IP:
172.16.0.1 > 172.16.0.2— underlay 地址 - 外层 UDP:源端口 52876(内层包哈希),目的端口 4789
- VXLAN Header:
flags [I]表示 VNI 有效,vni 100 - 内层 Ethernet:内层 MAC 地址
- 内层
IP:
10.244.0.1 > 10.244.1.1— overlay 地址 - ICMP:实际的 ping 请求
注意外层包长度是 148 字节,内层 ICMP 包是 98 字节,差值正好是 50 字节(VXLAN 封装开销)。
查看 FDB 表和 ARP 表
# 查看 host A 的 VXLAN FDB
$ ip netns exec ns-host-a bridge fdb show dev vxlan100
00:00:00:00:00:00 dst 172.16.0.2 self permanent
a2:b3:c4:00:00:02 dst 172.16.0.2 self
# 查看 host A 的 ARP 表
$ ip netns exec ns-host-a ip neigh show dev vxlan100
10.244.1.1 lladdr a2:b3:c4:00:00:02 REACHABLEFDB 表的第一条(全零 MAC)是创建 VXLAN 时
remote
参数自动添加的默认条目。第二条是通过数据平面学习到的 — host
B 的 vxlan100 的 MAC 地址对应 underlay IP 172.16.0.2。
清理
ip netns del ns-host-a
ip netns del ns-host-b
ip link del br-underlay九、隧道性能考量
选择隧道协议不只是功能对比,性能影响同样重要。
MTU 问题
VXLAN 的 50 字节开销意味着内层 MTU 只有 1450(假设 underlay MTU 是 1500)。如果内层应用发了一个 1500 字节的包:
- 如果设了
DF(Don’t Fragment)标志,包被丢弃,返回 ICMP “需要分片” - 如果没设
DF,内核在外层做分片 — 一个内层包变成两个外层包,性能直接腰斩
正确的做法是在 VXLAN 设备上设置正确的 MTU:
# 设置 VXLAN 内层 MTU
ip link set vxlan100 mtu 1450
# 或者在 underlay 上用 jumbo frame
ip link set eth0 mtu 9000
# 这样 VXLAN 内层 MTU 可以设为 8950大多数 CNI 插件会自动处理 MTU,但跨云环境下经常出问题。排查网络问题时,MTU 不匹配是最常见的原因之一。
硬件卸载
现代网卡支持 VXLAN 的硬件卸载(offload):
# 查看网卡的卸载能力
$ ethtool -k eth0 | grep vxlan
tx-udp_tnl-segmentation: on
tx-udp_tnl-csum-segmentation: on
rx-vxlan-port-offload: on有了硬件卸载,VXLAN 的封装/解封装在网卡上完成,CPU 开销几乎为零。没有硬件卸载的话,每个包都要在软件层做封装,CPU 开销明显增加。
Checksum 策略
VXLAN 外层 UDP 的 checksum 是否开启也影响性能:
# 关闭外层 UDP checksum(适用于可靠的数据中心网络)
ip link add vxlan0 type vxlan id 42 remote 10.0.0.2 \
dstport 4789 dev eth0 udp6zerocsumtx udp6zerocsumrx在可靠的数据中心网络里,关闭外层 checksum 可以减少 CPU 开销。但在不可靠的广域网上,最好保留 checksum。
十、总结与对比
回顾一下这篇文章的核心内容:
路由: - Linux 有
local、main、default
三张内置路由表 -
策略路由(ip rule)让你根据源地址、fwmark
等属性选择路由表 - FIB 使用 LC-trie
算法实现高效的最长前缀匹配 - ECMP
支持多路径负载均衡,基于五元组哈希保证同一流不乱序
隧道:
| 隧道 | 层次 | 开销 | 加密 | 典型使用者 |
|---|---|---|---|---|
| IPIP | L3 | 20 B | 无 | Calico |
| GRE | L3 | 24-28 B | 无 | 传统 VPN |
| VXLAN | L2 over L3 | 50 B | 无 | Flannel, Cilium, Calico |
| Geneve | L2 over L3 | 50+ B | TLV 可扩 | OVN, Cilium |
| WireGuard | L3 | 60 B | ChaCha20 | Calico 加密模式 |
选择指南:
- 同 L2 网络、追求性能:直接路由(不用隧道),或 IPIP(开销最小)
- 跨 L3 网络、需要 L2 overlay:VXLAN(最成熟)或 Geneve(可扩展)
- 需要加密:WireGuard(简单高效)
- 需要携带元数据:Geneve(TLV 可扩展)
下一篇我们会聊 BGP — 在不使用隧道的情况下,怎么通过路由协议让整个数据中心知道你的 Pod 在哪。Calico 的核心就是 BGP。
参考资料
- Linux 路由子系统
man ip-route,man ip-rule- Robert Olsson, “LC-trie: Lookups for IP”, 2004
- 内核源码:
net/ipv4/fib_trie.c,net/ipv4/fib_rules.c
- VXLAN
- RFC 7348: “Virtual eXtensible Local Area Network”
- 内核源码:
drivers/net/vxlan/vxlan_core.c - IETF NVO3 工作组文档
- Geneve
- RFC 8926: “Geneve: Generic Network Virtualization Encapsulation”
- J. Gross et al., “Geneve: It’s Time to Retire VXLAN”, 2020
- WireGuard
- Jason Donenfeld, “WireGuard: Next Generation Kernel Network Tunnel”, NDSS 2017
- 内核源码:
drivers/net/wireguard/
- Kubernetes 网络
- Calico 文档:IPIP, VXLAN, WireGuard 模式
- Flannel 文档:VXLAN 后端
- Cilium 文档:Native Routing vs Overlay