Skip to content

实验 14:打通 VXLAN ​

上一章讲了原理,这一章动手建一条隧道,并且抓包证明封装确实发生了。

起环境 ​

bash
cd labs/learn-ovs/11-vxlan
./start.sh
text
    overlay:h1 和 h2 要能二层互通
    ┌──────────────────────────────────────────────┐
    │  h1                                      h2  │
    │  192.168.10.1/24            192.168.10.2/24  │
    └──┬───────────────────────────────────────┬───┘
       │                                       │
    ┌──┴───┐                              ┌────┴─┐
    │ ovs1 │ eth2                    eth1 │ ovs2 │
    │ br0  ├──────────────────────────────┤ br0  │
    └──────┘ 10.0.0.1/30      10.0.0.2/30 └──────┘
             └──── underlay:三层可达 ────┘

start.sh 做的事:

  • 给 h1、h2 配 overlay 地址(同一网段)
  • 给 ovs1 的 eth2、ovs2 的 eth1 配 underlay 地址,这两个接口不进网桥
  • 两边各建一个 br0,只接本地主机

启动时它会自己验一遍:

text
== 确认 underlay 通、overlay 不通 ==
  underlay 10.0.0.1 → 10.0.0.2  通
  overlay  h1 → h2  不通(符合预期,还没建隧道)

underlay 地址为什么不进网桥

underlay 是承载网,它要用 Linux 的 IP 路由把隧道包送到对端。接进 OVS 网桥就变成二层端口了,协议栈拿不到它,路由也就无从谈起。

记住这个分工:underlay 归 Linux 协议栈,overlay 归 OVS。

建隧道 ​

两端各一条命令:

bash
# ovs1 上
docker exec clab-vxlan-ovs1 ovs-vsctl add-port br0 vxlan0 \
  -- set Interface vxlan0 type=vxlan \
       options:remote_ip=10.0.0.2 options:key=100

# ovs2 上
docker exec clab-vxlan-ovs2 ovs-vsctl add-port br0 vxlan0 \
  -- set Interface vxlan0 type=vxlan \
       options:remote_ip=10.0.0.1 options:key=100

注意 remote_ip 填的是对端的 underlay 地址,两边正好相反。key=100 是 VNI,两端必须一致。

bash
docker exec clab-vxlan-ovs1 ovs-vsctl show
text
Bridge br0
    Port br0
        Interface br0
            type: internal
    Port eth1
        Interface eth1
    Port vxlan0
        Interface vxlan0
            type: vxlan
            options: {key="100", remote_ip="10.0.0.2"}

通了:

bash
docker exec clab-vxlan-h1 ping -c 3 192.168.10.2
text
3 packets transmitted, 3 received, 0% packet loss

h1 和 h2 之间隔着一个三层网络,但它们现在是二层互通的。

抓包看封装 ​

这是本章最值得亲手做的一步。在 underlay 接口上抓包:

bash
docker exec -d clab-vxlan-ovs2 \
  sh -c 'timeout 8 tcpdump -i eth1 -n -l -e udp port 4789 > /tmp/cap.txt 2>&1'
sleep 1
docker exec clab-vxlan-h1 ping -c 2 -i 0.3 192.168.10.2
sleep 8
docker exec clab-vxlan-ovs2 cat /tmp/cap.txt
text
aa:c1:ab:43:70:60 > aa:c1:ab:56:75:af, ethertype IPv4, length 148:
    10.0.0.1.48601 > 10.0.0.2.4789: VXLAN, flags [I] (0x08), vni 100
    aa:c1:ab:8e:a5:78 > aa:c1:ab:a6:45:5a, ethertype IPv4, length 98:
        192.168.10.1 > 192.168.10.2: ICMP echo request, id 85, seq 1

aa:c1:ab:56:75:af > aa:c1:ab:43:70:60, ethertype IPv4, length 148:
    10.0.0.2.48601 > 10.0.0.1.4789: VXLAN, flags [I] (0x08), vni 100
    aa:c1:ab:a6:45:5a > aa:c1:ab:8e:a5:78, ethertype IPv4, length 98:
        192.168.10.2 > 192.168.10.1: ICMP echo reply, id 85, seq 1

两层结构清清楚楚:

text
外层  10.0.0.1 → 10.0.0.2  UDP 目的端口 4789   ← underlay 地址
      VXLAN 头,vni 100                       ← 租户标识
内层  192.168.10.1 → 192.168.10.2  ICMP       ← overlay 地址,原封不动

再看长度:内层 98 字节,外层 148 字节,正好差 50 —— 和上一章算的封装开销对上了。

注意外层 UDP 的源端口

第一个包源端口 48601,另一个流是 32922。目的端口固定 4789,源端口是内层报文的哈希 —— 这是给 underlay 路由器做 ECMP 用的。

从 OVS 内部看 ​

MAC 表:对端主机从隧道口学到 ​

bash
docker exec clab-vxlan-ovs1 ovs-appctl fdb/show br0
text
 port  VLAN  MAC                Age
    1     0  aa:c1:ab:8e:a5:78    3      ← h1,从 eth1 学到
    2     0  aa:c1:ab:a6:45:5a    3      ← h2,从 vxlan0 学到

h2 的 MAC 从 2 号口(vxlan0) 学到。对 NORMAL 转发逻辑来说,隧道端口和物理端口完全一样 —— 它不知道也不关心那个口背后是一条隧道。

datapath:所有 VXLAN 共用一个设备 ​

bash
docker exec clab-vxlan-ovs1 ovs-dpctl show
text
  port 0: ovs-system (internal)
  port 1: br0 (internal)
  port 2: eth1
  port 3: vxlan_sys_4789 (vxlan: packet_type=ptap)

注意 datapath 里的名字是 vxlan_sys_4789,不是 vxlan0。

这台机器上所有 VXLAN 隧道共用这一个 datapath 设备,不管你建了多少个隧道端口。区分不同隧道靠的是包上携带的隧道元数据(对端 IP + VNI),不是靠不同的设备。

和 ovs-system 是全机共用的 datapath 是同一个设计思路。

它在 ip link 里是能看到的:

bash
docker exec clab-vxlan-ovs1 ip -br link | grep vxlan
text
vxlan_sys_4789   UNKNOWN   9e:53:98:6a:de:f2 <BROADCAST,MULTICAST,UP,LOWER_UP>

(对比 patch 端口:那个在 ip link 里根本不存在。)

trace:封装是一个 datapath 动作 ​

bash
docker exec clab-vxlan-ovs1 ovs-appctl ofproto/trace br0 \
  "in_port=1,dl_src=<h1MAC>,dl_dst=<h2MAC>"
text
bridge("br0")
-------------
 0. priority 0
    NORMAL
     -> forwarding to learned port
     -> output to kernel tunnel

Final flow: unchanged
Datapath actions: set(tunnel(tun_id=0x64,dst=10.0.0.2,ttl=64,
                            tp_dst=4789,flags(df|key))),3

两个关键点:

-> output to kernel tunnel —— NORMAL 决定往 2 号口发,而这个口是隧道。

set(tunnel(tun_id=0x64,dst=10.0.0.2,...)) —— 封装被翻译成了一个 datapath 动作:

  • tun_id=0x64 = 十进制 100,就是 VNI
  • dst=10.0.0.2 对端 underlay 地址
  • tp_dst=4789 目的 UDP 端口
  • flags(df|key) 设了 DF 位(这就是 MTU 问题的来源)

封装不是"另一个模块"做的,它就是流表动作的一部分。

换成 Geneve ​

改一个字段的事:

bash
docker exec clab-vxlan-ovs1 ovs-vsctl set Interface vxlan0 type=geneve
docker exec clab-vxlan-ovs2 ovs-vsctl set Interface vxlan0 type=geneve

立刻就通:

text
2 packets transmitted, 2 received, 0% packet loss

datapath 里换了个设备:

text
  port 4: genev_sys_6081 (geneve: packet_type=ptap)

端口从 4789 变成 6081,其余一切照旧。隧道类型对上层完全透明。

改回去:

bash
docker exec clab-vxlan-ovs1 ovs-vsctl set Interface vxlan0 type=vxlan
docker exec clab-vxlan-ovs2 ovs-vsctl set Interface vxlan0 type=vxlan

自动检查 ​

bash
./verify.sh
text
  [通过] ovs1 上 vxlan0 类型正确
  [通过] ovs1 的隧道对端是 10.0.0.2
  [通过] 两端 VNI 都是 100
  [通过] underlay 仍然通
  [通过] overlay:h1 能 ping 通 h2
  [通过] underlay 上抓到了 VXLAN 封装包(UDP 4789)

自测清单 ​

  1. underlay 地址为什么不能接进 OVS 网桥?
  2. remote_ip 填的是对端的哪个地址?
  3. 抓到的包,外层和内层分别是什么地址?长度差多少?
  4. 外层 UDP 源端口为什么每条流都不一样?
  5. MAC 表里 h2 的 MAC 从几号口学到?NORMAL 知道那是隧道吗?
  6. ovs-dpctl show 里为什么是 vxlan_sys_4789 而不是 vxlan0?
  7. trace 输出里 tun_id=0x64 是什么?

常见问题 ​

隧道建好了但不通 ​

按这个顺序查:

bash
# 1. underlay 通吗(最常见的原因)
docker exec clab-vxlan-ovs1 ping -c 2 10.0.0.2

# 2. 两端 VNI 一致吗
docker exec clab-vxlan-ovs1 ovs-vsctl get Interface vxlan0 options:key
docker exec clab-vxlan-ovs2 ovs-vsctl get Interface vxlan0 options:key

# 3. remote_ip 填对了吗(两边应该互指)
docker exec clab-vxlan-ovs1 ovs-vsctl get Interface vxlan0 options:remote_ip

# 4. underlay 上有封装包吗
docker exec clab-vxlan-ovs2 timeout 5 tcpdump -i eth1 -n -l udp port 4789

第 4 步能抓到包但业务不通 → 多半是 VNI 不匹配,或者对端网桥没接对。

ping 通但传文件卡死 ​

MTU 问题,上一章讲过。验证一下:

bash
# 1450 字节的载荷能过
docker exec clab-vxlan-h1 ping -c 2 -M do -s 1400 192.168.10.2
# 1500 字节的过不去
docker exec clab-vxlan-h1 ping -c 2 -M do -s 1472 192.168.10.2

第二条失败就确诊了。改小 overlay MTU:

bash
docker exec clab-vxlan-h1 ip link set eth1 mtu 1450
docker exec clab-vxlan-h2 ip link set eth1 mtu 1450

隧道端口显示 error ​

bash
ovs-vsctl show | grep -A3 vxlan0

常见原因:remote_ip 写成了域名(不支持)、options 拼写错误、或者本机没有到 remote_ip 的路由。

抓不到 4789 的包 ​

确认抓的是 underlay 接口(eth2 / eth1),不是 br0 或 vxlan_sys_4789。在 vxlan_sys_4789 上抓到的是解封装后的内层帧,看不到外层。

小结 ​

  • 隧道端口就是 type=vxlan 的 Interface,remote_ip 填对端 underlay 地址,key 是 VNI
  • underlay 地址配在接口上、不进网桥;underlay 归 Linux 路由,overlay 归 OVS
  • 抓包能直接看到两层结构,长度差 50 字节
  • 外层 UDP 目的端口固定 4789,源端口是内层哈希(为了 ECMP)
  • 对 NORMAL 来说隧道端口和物理端口没有区别,MAC 学习照常
  • datapath 里所有 VXLAN 共用 vxlan_sys_4789 一个设备
  • 封装是一个 datapath 动作:set(tunnel(tun_id=...,dst=...))
  • 换隧道类型只需改 type 字段,对上层完全透明
  • 排错顺序:underlay → VNI → remote_ip → 抓包

下一章:实验 15:多租户隔离,用不同的 VNI 在同一条物理链路上跑多个互相隔离的虚拟网络。