vm跨节点通过geneve隧道收发包过程

3 阅读16分钟

Geneve收包流程详解

网络流量如何从物理网卡 eth0 进入 OVS 上的 tap 口

场景说明

一个外部主机发送数据到 KubeVirt VM,数据如何从物理网卡一路到达 VM 的 tap 口。

Geneve 隧道(Kube-OVN 默认方式)

eth0 仍然由内核协议栈管理,OVS 通过隧道端口利用 eth0 发包

┌───────────────────────────────────────────────────────────────┐
│                                                               │
│   ┌─────────────────────────────────────────────────────┐     │
│   │                  OVS br-int                          │     │
│   │                                                     │     │
│   │   ┌──────┐   ┌──────┐   ┌─────────────────────┐    │     │
│   │   │ veth │   │ veth │   │ geneve_sys          │    │     │
│   │   │ VM-A │   │ VM-B │   │ (type=geneve)       │    │     │
│   │   └──────┘   └──────┘   │ remote_ip=对端节点IP  │    │     │
│   │                         └──────────┬──────────┘    │     │
│   │                                    │               │     │
│   └────────────────────────────────────┼───────────────┘     │
│                                        │                     │
│            OVS 生成 Geneve 封装后的数据包  │                     │
│            交给内核协议栈发送              │                     │
│                                        ▼                     │
│   ┌────────────────────────────────────────────────────┐     │
│   │              Host 内核协议栈                        │     │
│   │                                                    │     │
│   │   路由查找: 目的IP=对端节点IP → 出接口=eth0          │     │
│   │                                                    │     │
│   └──────────────────────────┬─────────────────────────┘     │
│                              │                               │
│   ┌──────────────────────────┴─────────────────────────┐     │
│   │              eth0 (物理网卡)                        │     │
│   │              IP: 192.168.1.10                      │     │
│   │              由内核协议栈正常管理                     │     │
│   └──────────────────────────┬─────────────────────────┘     │
│                              │                               │
└──────────────────────────────┼───────────────────────────────┘
                               │
                               ▼
                       ═══════════════
                         物理网络
                       ═══════════════

关键点:

  • eth0 仍然属于内核协议栈
  • OVS 通过 Geneve 隧道端口封装数据包
  • 封装后的外层 IP 包通过内核正常路由经 eth0 发出
  • 这是 Kube-OVN 的默认方式

详细收包流程(外部流量 → VM)

场景:Geneve 隧道方式

外部另一个节点上的 VM 发送数据到本节点的 VM。

外部节点封装好的 Geneve 数据包:
┌──────────────────────────────────────────────────────────┐
 Outer Eth  Outer IP       UDP     Geneve  原始以太网帧 
 dst: eth0  dst: 本机IP     dp:6081│ VNI     dst: VM MAC 
  的MAC     src: 对端节点IP                              
└──────────────────────────────────────────────────────────┘

完整收包路径

物理网络(线缆上的电信号/光信号)
    │
    ▼
═══════════════════════════════════════════════════════════════
第一阶段:物理网卡收包
═══════════════════════════════════════════════════════════════
    │
    │ ① 物理网卡 eth0 收到以太网帧
    │   - 网卡校验 FCS (帧校验序列)
    │   - 网卡 DMA 将数据写入内核 Ring Buffer (sk_buff)
    │   - 触发硬中断 (IRQ)
    │
    ▼
eth0 网卡驱动 (如 ixgbe, mlx5)
    │
    │ ② 硬中断处理
    │   - 禁止该队列的后续中断
    │   - 调度 NAPI 软中断
    │
    │ ③ NAPI 软中断 (NET_RX_SOFTIRQ)
    │   - napi_poll() 批量从 Ring Buffer 取包
    │   - 构建 sk_buff 结构体
    │   - 设置 skb->dev = eth0
    │   - 设置 skb->protocol = ETH_P_IP (0x0800)
    │
    ▼
netif_receive_skb(skb)              ← 内核统一收包入口
    │
    │ ④ 内核协议栈开始处理
    │   - tc ingress 规则 (如果有)
    │   - XDP 处理 (如果有)
    │   - 检查 skb->protocol
    │
    ▼
═══════════════════════════════════════════════════════════════
第二阶段:内核协议栈解封装外层头部
═══════════════════════════════════════════════════════════════
    │
    │ ⑤ IP 层处理
    │   ip_rcv()
    │   - 检查外层目的 IP = 本机 IP → 本地处理
    │   - ip_local_deliver()
    │
    ▼
    │ ⑥ UDP 层处理
    │   udp_rcv()
    │   - 检查目的端口 = 6081 (Geneve)
    │   - 找到注册在该端口的 Geneve 处理函数
    │
    ▼
    │ ⑦ Geneve 解封装
    │   geneve_udp_encap_recv()
    │   - 剥离外层: Ethernet + IP + UDP + Geneve 头部
    │   - 提取 VNI (虚拟网络标识)
    │   - 获得内层的原始以太网帧
    │   - 根据 VNI 找到对应的 OVS 隧道端口
    │
    │   解封装前:
    │   ┌──────────┬──────────┬─────┬────────┬───────────┐
    │   │ Outer Eth│ Outer IP │ UDP │ Geneve │ 原始以太网帧│
    │   └──────────┴──────────┴─────┴────────┴───────────┘
    │
    │   解封装后:
    │   ┌───────────┐
    │   │ 原始以太网帧│  skb->dev = geneve_sys (OVS 隧道端口)
    │   └───────────┘
    │
    ▼
═══════════════════════════════════════════════════════════════
第三阶段:OVS br-int 流表处理
═══════════════════════════════════════════════════════════════
    │
    │ ⑧ 数据包进入 OVS
    │   ovs_vport_receive(geneve_sys 端口, skb)
    │   - 入端口 = geneve_sys (隧道端口)
    │
    ▼
OVS 内核数据路径 (openvswitch.ko)
    │
    │ ⑨ 流表匹配
    │   ovs_flow_tbl_lookup(skb)
    │
    │   匹配字段:
    │   - in_port = geneve_sys
    │   - tun_id = VNI (标识 VPC/子网)
    │   - dl_dst = VM 的 MAC 地址
    │   - nw_dst = VM 的 IP 地址
    │
    │   ┌──────────────────────────────────────────────┐
    │   │  OVN 逻辑流表处理:                            │
    │   │                                              │
    │   │  table 0: 入端口分类                          │
    │   │    → 识别为隧道流量,提取 VNI                   │
    │   │                                              │
    │   │  table 10-20: 入方向 ACL (安全组)              │
    │   │    → 检查是否允许该流量                        │
    │   │                                              │
    │   │  table 30: 逻辑交换机                         │
    │   │    → MAC 地址查找,确定目的端口                 │
    │   │                                              │
    │   │  table 40-50: 出方向 ACL                      │
    │   │    → 检查出方向安全组规则                      │
    │   │                                              │
    │   │  table 60: 输出动作                           │
    │   │    → output:veth-host (VM对应的端口)           │
    │   └──────────────────────────────────────────────┘
    │
    │ ⑩ 执行动作: 从 veth-host 端口输出
    │   ovs_execute_actions()
    │   → output 到 veth-host 端口
    │   → dev_queue_xmit(skb) 
    │   → skb->dev = veth-host
    │
    ▼
═══════════════════════════════════════════════════════════════
第四阶段:veth pair 跨 Namespace
═══════════════════════════════════════════════════════════════
    │
    │ ⑪ veth-host 发送
    │   veth_xmit(skb)
    │   - 找到 peer = veth-pod
    │   - dev_forward_skb(veth-pod, skb)
    │   - 数据包跨越 namespace 边界
    │   - skb->dev = veth-pod
    │   - netif_rx(skb) 在 Pod namespace 触发收包
    │
    ▼
═══════════════════════════════════════════════════════════════
第五阶段:Pod Network Namespace 内部
═══════════════════════════════════════════════════════════════
    │
    │ ⑫ veth-pod 收包
    │   netif_receive_skb(skb)
    │   - veth-pod 是 Linux Bridge 的端口
    │   - 进入 Bridge 处理
    │
    ▼
Linux Bridge (k6t-net0)
    │
    │ ⑬ Bridge 转发
    │   br_handle_frame(skb)
    │   - br_fdb_update(): 学习源 MAC
    │   - br_fdb_find_rcu(): 查找目的 MAC
    │   - 命中: 目的端口 = tap0
    │   - br_forward(tap0, skb)
    │
    ▼
═══════════════════════════════════════════════════════════════
第六阶段:tap → vhost-net → VM
═══════════════════════════════════════════════════════════════
    │
    │ ⑭ tap0 收到数据包
    │   tun_net_xmit(skb)
    │   - 将 skb 放入 tap 的 socket 接收队列
    │   - 唤醒 vhost-net 内核线程 (通过 eventfd)
    │
    ▼
vhost-net 内核线程
    │
    │ ⑮ 处理 RX vring
    │   handle_rx()
    │   - 从 tap socket 队列取出 skb
    │   - 读取 VM 预先提交的空 RX buffer (avail ring)
    │   - 复制数据: skb → vring 共享内存
    │   - 更新 used ring
    │   - 通过 irqfd 向 VM 注入虚拟中断
    │
    ▼
VM (Guest)
    │
    │ ⑯ Guest 收到虚拟中断
    │   virtio-net 驱动 NAPI 轮询
    │   - 从 used ring 读取数据
    │   - 构建 Guest sk_buff
    │   - 上送 Guest 协议栈
    │   - TCP/IP 解封装
    │   - 放入 socket 接收缓冲区
    │
    ▼
VM 内应用程序
    │
    │ ⑰ recv() 返回数据
    │
    ▼
应用获得数据 ✅

关键转折点图解

数据包身份的变化过程:

物理网络上:
┌──────────┬──────────┬─────┬────────┬───────────────────┐
│ Outer Eth│ Outer IP │ UDP │ Geneve │ Inner Eth + IP+TCP│
│ 物理MAC  │ 节点IP   │ 6081│ VNI    │ VM MAC + VM IP    │
└──────────┴──────────┴─────┴────────┴───────────────────┘
                │
      ① eth0 收包 (DMA + 硬中断)
                │
                ▼
      ② 内核 IP/UDP 层处理
                │
      ③ Geneve 解封装 ← 💡 关键转折点 #1
                │         外层头部被剥离
                ▼         数据包 "进入" OVS 隧道端口

OVS br-int 内:
┌───────────────────┐
│ Inner Eth + IP+TCP│  skb->dev = geneve_sys
│ VM MAC + VM IP    │  OVS tunnel metadata: tun_id=VNI
└───────────────────┘
                │
      ④ OVS 流表匹配
      ⑤ output 到 veth-host ← 💡 关键转折点 #2
                │               数据包 "离开" OVS 进入 veth
                ▼

veth pair:
┌───────────────────┐
│ Inner Eth + IP+TCP│  skb->dev = veth-host → veth-pod
│ VM MAC + VM IP    │
└───────────────────┘
                │
      ⑥ 跨 namespace ← 💡 关键转折点 #3
                │       数据包进入 Pod netns
                ▼

Linux Bridge → tap0:
┌───────────────────┐
│ Inner Eth + IP+TCP│  skb 放入 tap socket 队列
│ VM MAC + VM IP    │
└───────────────────┘
                │
      ⑦ vhost-net → vring ← 💡 关键转折点 #4
                │              数据包 "进入" VM
                ▼

VM 内部:
┌───────────────────┐
│ Inner Eth + IP+TCP│  Guest virtio-net 驱动收包
│ VM MAC + VM IP    │  Guest 协议栈正常处理
└───────────────────┘

数据复制与开销标注

阶段                    数据复制              模式切换              延迟
──────────────────────────────────────────────────────────────────────────
eth0 DMA 收包          复制#1 (DMA→内存)     硬中断+软中断         ~2-5μs
                                      
内核 IP/UDP 处理       无复制               无                    ~1-2μs

Geneve 解封装          无复制(指针移动)      无                    ~1-2μs

OVS 流表匹配           无复制               首包可能upcall        ~2-5μs

veth 跨 namespace     复制#2 (skb_clone)   软中断                ~2-5μs

Linux Bridge 转发      无复制(指针传递)      无                    ~1-3μs

tap → vhost-net       复制#3 (skb→vring)   eventfd 通知          ~2-4μs

vring → VM            无额外复制(共享内存)   VM Enter ⚡           ~1-3μs

VM 协议栈 → 应用       复制#4 (内核→用户)    系统调用              ~1-2μs
──────────────────────────────────────────────────────────────────────────
总计                   4 次复制              5-7 次切换            ~15-35μs

验证和调试命令

# 查看 OVS 网桥和端口
ovs-vsctl show

# 查看 br-int 上的端口
ovs-vsctl list-ports br-int

# 查看 Geneve 隧道端口
ovs-vsctl find interface type=geneve

# 查看 OVS 流表(看数据包如何被匹配和转发)
ovs-ofctl dump-flows br-int

# 查看某个端口的统计(确认数据包是否到达)
ovs-vsctl get interface <port-name> statistics

# 在各个点抓包验证
# 物理网卡上抓包(能看到 Geneve 封装)
tcpdump -i eth0 -nn udp port 6081

# OVS 内部端口抓包
ovs-tcpdump -i <port-name>

# Pod namespace 内抓包
nsenter -t <pid> -n tcpdump -i veth-pod

# tap 设备抓包
nsenter -t <pid> -n tcpdump -i tap0

一句话总结

外部流量通过 eth0 → 内核协议栈 IP/UDP 处理 → Geneve 解封装 → OVS 隧道端口进入 br-int → 流表匹配找到目标 veth 端口 → veth 跨 namespace → Linux Bridge 转发 → tap0 → vhost-net 写入 vring → VM 收包。核心在于 Geneve 解封装是连接物理网卡和 OVS 虚拟世界的桥梁,外层 IP 负责节点间物理可达,内层帧负责 VM 间逻辑通信。

Geneve 发包流程详解

场景说明

本节点的 VM-A 发送数据到远端节点的 VM-B,需要通过 Geneve 隧道跨节点传输。

本节点 VM-A (10.0.1.10) → Geneve 隧道 → 远端节点 VM-B (10.0.1.20)
本节点 eth0: 192.168.1.10
远端节点 eth0: 192.168.1.20

整体流程概览

VM-A 发出原始数据包:
┌──────────────────────────────────┐
│ Eth: dst=VM-B MAC               │
│ IP:  dst=10.0.1.20 (VM-B)      │
│ TCP: dst=80                     │
│ Payload                         │
└──────────────────────────────────┘
          │
          │ 经过 tap → bridge → veth → OVS
          ▼
OVS 决定需要通过 Geneve 隧道发出,封装后:
┌─────────────────────────────────────────────────────────────────┐
│ Outer Eth: dst=网关/下一跳MAC  src=eth0 MAC                      │
│ Outer IP:  dst=192.168.1.20(远端节点)  src=192.168.1.10(本节点)   │
│ Outer UDP: dst=6081(Geneve)  src=随机端口                        │
│ Geneve:    VNI=0x1234  Options(可选)                             │
│ ┌──────────────────────────────────┐                            │
│ │ Inner Eth: dst=VM-B MAC          │                            │
│ │ Inner IP:  dst=10.0.1.20         │  ← 原始数据包被完整包裹      │
│ │ Inner TCP: dst=80                │                            │
│ │ Payload                          │                            │
│ └──────────────────────────────────┘                            │
└─────────────────────────────────────────────────────────────────┘
          │
          │ 通过内核协议栈路由,从 eth0 发出
          ▼
      物理网络 → 远端节点

完整发包流程

VM-A 应用程序 send()
    
    
═══════════════════════════════════════════════════════════════
第一阶段:VM 内部  tap
═══════════════════════════════════════════════════════════════
    
      Guest 应用 send()
       [复制 #1] 用户空间 → Guest 内核
    
      Guest 内核协议栈封装
       TCP   IP   以太网帧头
       src MAC = VM-A MAC
       dst MAC = VM-A 网关 MAC (或 VM-B MAC,取决于是否同子网)
       src IP  = 10.0.1.10
       dst IP  = 10.0.1.20
    
      virtio-net 驱动发送
       [复制 #2] sk_buff → TX vring
       写通知寄存器  VM Exit 
    
      vhost-net 处理
       [复制 #3] vring → Host sk_buff
       注入 tap0  netif_rx()
    
    
═══════════════════════════════════════════════════════════════
第二阶段:tap  Linux Bridge  veth  OVS br-int
═══════════════════════════════════════════════════════════════
    
      tap0  Linux Bridge (k6t-net0)
       br_handle_frame()
       MAC 学习 + FDB 转发  出端口 = veth-pod
    
      veth-pod  veth-host (跨 namespace)
       [复制 #4] skb_clone + 软中断
    
      veth-host  OVS br-int
       ovs_vport_receive(veth-host 端口, skb)
    
    
═══════════════════════════════════════════════════════════════
第三阶段:OVS/OVN 流表处理  💡 核心决策点
═══════════════════════════════════════════════════════════════
    
      OVS 内核数据路径流表匹配
       ovs_flow_tbl_lookup(skb)
    
       匹配字段:
       ┌──────────────────────────────────────┐
        in_port  = veth-host (VM-A 的端口)    
        dl_src   = VM-A MAC                  
        dl_dst   = VM-B MAC                  
        nw_src   = 10.0.1.10                 
        nw_dst   = 10.0.1.20                 
       └──────────────────────────────────────┘
    
      OVN 逻辑流表流水线 (翻译为 OpenFlow 规则)
    
       ┌─────────────────────────────────────────────────────┐
                                                            
         table 0: 入端口分类                                 
           match: in_port=veth-host                         
           action: 设置 logical_port=vm-a-port, 下一个表     
                                                            
         table 8-16: Ingress ACL (入方向安全组)               
           match: 源IP/端口 + 安全组规则                      
           action: 允许/拒绝                                 
                                                            
         table 24: Logical Switch MAC 学习                   
           match: dl_dst=VM-B MAC                           
           action: 确定目的逻辑端口=vm-b-port                 
                                                            
         table 32: Logical Router (如果跨子网)                
           match: nw_dst=10.0.1.20                          
           action: 路由查找, 可能 SNAT/DNAT                  
                                                            
         table 40-48: Egress ACL (出方向安全组)               
           match: 目的 IP/端口 + 安全组规则                    
           action: 允许/拒绝                                 
                                                            
         table 60: 输出判断                                  
           💡 关键决策: VM-B 在哪个节点?                      
           查询 OVN Southbound DB:                          
             vm-b-port  chassis=远端节点  IP=192.168.1.20 
                                                            
           action: set_tunnel(tun_id=VNI),                  
                   set_field(tun_dst=192.168.1.20),         
                   output:geneve_sys                        
                                                           
                   Geneve 隧道端口发出                     
                                                            
       └─────────────────────────────────────────────────────┘
    
      执行动作: output  geneve_sys 隧道端口
       ovs_execute_actions()
    
       action 具体执行:
       a. set_tunnel: 设置隧道元数据
          - tun_id (VNI) = 标识 VPC/逻辑网络
          - tun_dst = 192.168.1.20 (远端节点 IP)
       b. output:geneve_sys
          - 调用 geneve 隧道端口的发送函数
    
    
═══════════════════════════════════════════════════════════════
第四阶段:Geneve 封装  💡 封装核心
═══════════════════════════════════════════════════════════════
    
      OVS 调用 Geneve 隧道端口的发送函数
       ovs_vport_send(geneve_sys, skb, tunnel_metadata)
          geneve_xmit()
    
      构建 Geneve 头部
    
       geneve_build_header()
       ┌─────────────────────────────────────────────┐
                Geneve Header (8 bytes + options)    
                                                    
          0                   1                   2 
          0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
          Ver │Opt Len│O│C│  Rsvd   Protocol     
          (0)                   (0x6558      
                                =Ethernet)   
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
                 VNI (24 bits)          Reserved│  
                 0x001234                         
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
              Variable Length Options (可选)        
              OVN 用此携带逻辑端口信息等             
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
       └─────────────────────────────────────────────┘
    
      构建外层 UDP 头部
    
       udp_tunnel_xmit_skb()
       ┌─────────────────────────────────────────────┐
                UDP Header (8 bytes)                
                                                    
          Source Port: hash(内层五元组) & 0xFFFF     
                       随机化源端口,用于 ECMP 负载均衡│
          Dest Port:   6081 (IANA 标准 Geneve 端口)  
          Length:      Geneve头 + 内层帧长度          
          Checksum:    0x0000 (通常关闭)             
                                                    
       └─────────────────────────────────────────────┘
    
       💡 源端口随机化的意义:
       - 物理交换机根据 UDP 五元组做 ECMP 哈希
       - 不同的内层流量  不同的 UDP 源端口  分散到不同物理链路
       - 避免所有隧道流量走同一条物理路径
    
      构建外层 IP 头部
    
       iptunnel_xmit()
       ┌─────────────────────────────────────────────┐
                IP Header (20 bytes)                
                                                    
          Version:     4                            
          Protocol:    17 (UDP)                     
          TTL:         64                           
          Source IP:   192.168.1.10  (本节点 eth0)   
          Dest IP:     192.168.1.20  (远端节点 eth0) 
          DF:          1  (Don't Fragment,通常设置)  
          Total Len:   外层IP头+UDP头+Geneve头+内层帧 
                                                    
       └─────────────────────────────────────────────┘
    
       💡  IP 和目的 IP 的来源:
       - 目的 IP: OVN Southbound DB 记录了每个 chassis  IP
       -  IP:   内核路由查找确定出接口后,使用出接口的 IP
    
    
═══════════════════════════════════════════════════════════════
第五阶段:内核协议栈路由 + 外层以太网封装
═══════════════════════════════════════════════════════════════
    
      封装后的完整数据包:
    
       此时 skb 内容:
       ┌──────────────────────────────────────────────────────┐
        (还没有外层Eth头)                                     
        Outer IP  UDP:6081  Geneve:VNI  Inner Eth+IP+TCP 
       └──────────────────────────────────────────────────────┘
    
      内核路由查找
       ip_route_output_key()
    
       查找路由表:
       ┌────────────────────────────────────────────────────┐
         $ ip route get 192.168.1.20                      
         192.168.1.20 via 192.168.1.1 dev eth0            
                       网关            出接口            
       └────────────────────────────────────────────────────┘
    
       结果:
       - 出接口 = eth0
       - 下一跳 = 192.168.1.1 (网关) 或直连
    
      邻居子系统 (ARP)
       neigh_resolve_output()
    
       查找 ARP 缓存 / 发送 ARP 请求:
       ┌────────────────────────────────────────────────────┐
         下一跳 192.168.1.1  MAC = aa:bb:cc:dd:ee:01     
                                                           
         如果 ARP 缓存未命中:                               
         - 数据包暂存队列                                   
         - 发送 ARP Request                                
         - 收到 ARP Reply 后继续处理                        
       └────────────────────────────────────────────────────┘
    
      构建外层以太网头部
       dev_hard_header()
    
       ┌─────────────────────────────────────────────┐
              Ethernet Header (14 bytes)            
                                                    
          Dst MAC:    aa:bb:cc:dd:ee:01 (网关MAC)   
          Src MAC:    eth0  MAC 地址              
          EtherType:  0x0800 (IPv4)                
                                                    
       └─────────────────────────────────────────────┘
    
    
═══════════════════════════════════════════════════════════════
第六阶段:物理网卡发送
═══════════════════════════════════════════════════════════════
    
      最终完整数据包:
    
       ┌────────────────────────────────────────────────────────────────┐
        Outer Eth   Outer IP      UDP    Geneve  原始以太网帧      
        14 bytes    20 bytes      8B     8B+                      
                                                                  
        dst: 网关    dst:192.168   dp:    VNI:    dst: VM-B MAC   
             MAC      .1.20      6081   0x1234  src: VM-A MAC   
        src: eth0   src:192.168   sp:    proto:  type: 0x0800    
             MAC      .1.10      随机   0x6558  IP+TCP+Payload  
        type:       proto: UDP         │(Trans                    
        0x0800      TTL: 64             Eth)                     
       └────────────────────────────────────────────────────────────────┘
    
       总封装开销: 14 + 20 + 8 + 8 = 50 bytes (无 Geneve Options)
                   14 + 20 + 8 + 8 + N = 50+N bytes (有 Options)
    
      进入网卡发送队列
       dev_queue_xmit(skb)
        Traffic Control (qdisc) 排队
        调用 eth0  ndo_start_xmit()
    
      物理网卡驱动发送
       ixgbe_xmit_frame() / mlx5e_xmit() / ...
       - 填充 TX 描述符 (DMA descriptor)
       -  skb 数据地址写入描述符
       - 通知网卡 (doorbell register)
       - 网卡通过 DMA 读取数据
       - 网卡将以太网帧转换为电信号/光信号
       - 发送到物理线缆
       [复制 #5: DMA 传输]
    
      发送完成
       - 网卡发送完成中断
       - 驱动释放 TX 描述符和 skb
    
    
物理网络  交换机  远端节点

Geneve 封装的内核代码路径

// OVS 执行 output 到 geneve 端口
ovs_execute_actions()
  → do_output()
    → ovs_vport_send(geneve_port, skb)

// Geneve 端口发送
geneve_xmit(skb)
  │
  ├── ① 查找路由 (确定出接口和下一跳)
  │   rt = geneve_get_v4_rt()
  │     → ip_route_output_key(目的IP=远端节点IP)
  │     → 返回路由表项: {出接口=eth0, 网关=192.168.1.1}
  │
  ├── ② 检查 MTU
  │   if (skb->len + 封装头部总长 > 出接口MTU)
  │     → 需要分片或返回 ICMP Too Big
  │     → 这就是为什么隧道环境下有效 MTU 减小
  │
  ├── ③ 构建 Geneve 头部
  │   geneve_build_header()
  │     → skb_push(skb, sizeof(genevehdr))  // 在 skb 前面扩展空间
  │     → 填充 VNI, Protocol, Options
  │
  ├── ④ 构建外层 UDP + IP + 发送
  │   udp_tunnel_xmit_skb(rt, skb, src_ip, dst_ip, tos, ttl, 
  │                        src_port, dst_port=6081)
  │     │
  │     ├── udp_set_csum()          // UDP 校验和 (通常设为0)
  │     ├── skb_push(skb, UDP头)    // 添加 UDP 头
  │     ├── iptunnel_xmit()         // 添加 IP 头 + 发送
  │     │     ├── skb_push(skb, IP头)
  │     │     ├── ip_local_out(skb)  // 进入内核 IP 输出路径
  │     │     │     ├── NF_HOOK(POSTROUTING)  // iptables/nftables
  │     │     │     ├── ip_output()
  │     │     │     ├── ip_finish_output()
  │     │     │     │     ├── 检查是否需要分片
  │     │     │     │     ├── neigh_resolve_output()  // ARP 解析
  │     │     │     │     ├── dev_hard_header()        // 添加以太网头
  │     │     │     │     └── dev_queue_xmit(skb)      // 进入网卡发送
  │     │     │     │           └── eth0->ndo_start_xmit(skb)
  │     │     │     │                 └── DMA 发送

skb 在封装过程中的变化

封装前 skb 内存布局:
┌─────────┬──────────────────────────────────────────────┐
│ headroom│  Eth(VM) │ IP(VM) │ TCP │ Payload            │
│ (空闲)  │  14B     │ 20B    │ 20B │ ...                │
└─────────┴──────────────────────────────────────────────┘
    ↑                 ↑                                  ↑
  skb->head       skb->data                         skb->tail


步骤1: geneve_build_header() — skb_push 添加 Geneve 头
┌─────┬────────┬──────────────────────────────────────────┐
│ head│ Geneve │  Eth(VM) │ IP(VM) │ TCP │ Payload        │
│room │  8B+   │  14B     │ 20B    │ 20B │ ...            │
└─────┴────────┴──────────────────────────────────────────┘
               ↑
           skb->data (向前移动了)


步骤2: udp_set_csum + skb_push — 添加 UDP 头
┌───┬─────┬────────┬──────────────────────────────────────┐
│hd │ UDP │ Geneve │  Eth(VM) │ IP(VM) │ TCP │ Payload    │
│rm │  8B │  8B+   │  14B     │ 20B    │ 20B │ ...        │
└───┴─────┴────────┴──────────────────────────────────────┘
          ↑
      skb->data (继续向前)


步骤3: iptunnel_xmit — 添加外层 IP 头
┌─┬───────┬─────┬────────┬────────────────────────────────┐
│ │Out IP │ UDP │ Geneve │  Eth(VM) │ IP(VM) │ TCP │ ...  │
│ │ 20B   │  8B │  8B+   │  14B     │ 20B    │ 20B │      │
└─┴───────┴─────┴────────┴────────────────────────────────┘
   ↑
 skb->data


步骤4: dev_hard_header — 添加外层以太网头
┌─────────┬───────┬─────┬────────┬────────────────────────┐
│Out Eth  │Out IP │ UDP │ Geneve │ Eth(VM)│IP(VM)│TCP│... │
│ 14B     │ 20B   │  8B │  8B+   │ 14B    │ 20B  │20B│    │
└─────────┴───────┴─────┴────────┴────────────────────────┘
↑                                                         ↑
skb->data                                            skb->tail

💡 关键: 通过 skb_push() 不断向前扩展 skb->data 指针
   不需要重新分配内存,只要 headroom 足够
   如果 headroom 不够 → skb_cow_head() 重新分配(有复制开销)

MTU 问题详解

标准以太网 MTU = 1500 bytes

VM 发出的原始 IP 包最大长度:
┌───────────────────────────────┐
 IP Header  TCP  Payload        1500 bytes
   20B      20B   1460B    
└───────────────────────────────┘

Geneve 封装后的外层 IP 包长度:
┌──────────┬─────┬────────┬─────────────────────┐
 Outer IP  UDP  Geneve  Inner Eth + 原始IP包  
   20B     8B   8B+     14B +  1500B       
└──────────┴─────┴────────┴─────────────────────┘
= 20 + 8 + 8 + 14 + 1500 = 1550 bytes   💥 超过物理 MTU!

解决方案:
┌───────────────────────────────────────────────────────────┐
                                                           
  方案1: 减小 VM  MTU                                     
    VM  MTU = 1500 - 50 = 1450                           
    VM 发出的 IP   1450                                  
    封装后  1450 + 50 = 1500                             
    缺点: 需要配置 VM 内的 MTU                               
                                                           
  方案2: 增大物理网络 MTU (Jumbo Frame)                      
    物理交换机 + eth0 MTU = 9000                             
    VM  MTU 保持 1500                                     
    封装后 1500 + 50 = 1550 < 9000                        
    缺点: 需要全链路支持 Jumbo Frame                         
                                                           
  方案3: Kube-OVN 默认做法                                  
    设置 Pod/VM  MTU = 1400                               
    留足够的 headroom 给隧道封装                              
                                                           
└───────────────────────────────────────────────────────────┘

UDP 源端口与 ECMP 负载均衡

为什么 Geneve 的 UDP 源端口要随机化?

物理网络:
                    ┌──── 链路 A ────┐
  发送节点 ────────▶│ 交换机 (ECMP)  │────────▶ 接收节点
                    │                │
                    │  hash(五元组)   │
                    │  决定走哪条链路  │
                    │                │
                    └──── 链路 B ────┘

如果所有隧道流量的 UDP 源端口固定:
  所有包的五元组 = (src_ip, dst_ip, UDP, 固定src_port, 6081)
  → hash 值相同 → 全部走链路 A → 链路 B 空闲 ❌

随机化 UDP 源端口:
  流量1: (src_ip, dst_ip, UDP, 49152, 6081) → hash=X → 链路 A
  流量2: (src_ip, dst_ip, UDP, 53201, 6081) → hash=Y → 链路 B
  → 流量均匀分布 ✅

源端口计算方式:
  src_port = hash(内层源IP, 内层目的IP, 内层源端口, 内层目的端口, 协议)
  → 同一条内层流 → 相同的外层 UDP 源端口 → 相同的物理路径
  → 保证同一条 TCP 连接的包不会乱序

Geneve Options(OVN 特有)

OVN 使用 Geneve Options 携带逻辑网络元数据:

┌─────────────────────────────────────────────────────────────┐
│                   Geneve Options                            │
│                                                             │
│   Option Class = 0x0102 (OVN)                               │
│   Option Type  = 0x80                                       │
│   Option Data:                                              │
│   ┌──────────────────────────────────────┐                  │
│   │  Logical Ingress Port  (逻辑入端口)   │ ← VM-A 的逻辑端口│
│   │  Logical Egress Port   (逻辑出端口)   │ ← VM-B 的逻辑端口│
│   │  Logical Datapath      (逻辑数据路径) │ ← 逻辑交换机/路由器│
│   └──────────────────────────────────────┘                  │
│                                                             │
│   这些元数据让远端节点的 OVS 不需要重新做完整的逻辑流表匹配     │
│   直接知道这个包属于哪个逻辑网络、从哪个逻辑端口来的            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

与收包流程的对称对比

发包 (本节点 VM → 远端节点 VM):                收包 (远端节点 VM → 本节点 VM):
─────────────────────────────                ─────────────────────────────

VM send()                                    VM recv()
    ↓                                            ↑
virtio TX vring                              virtio RX vring
    ↓                                            ↑
vhost-net → tap0                             vhost-net ← tap0
    ↓                                            ↑
Linux Bridge 转发                             Linux Bridge 转发
    ↓                                            ↑
veth-pod → veth-host                         veth-pod ← veth-host
    ↓                                            ↑
OVS br-int 流表匹配                           OVS br-int 流表匹配
    ↓                                            ↑
output: geneve_sys                           input: geneve_sys
    ↓                                            ↑
┌─────────────────┐                          ┌─────────────────┐
│ Geneve 封装      │                          │ Geneve 解封装    │
│ + UDP 头        │                          │ 剥离外层头部     │
│ + 外层 IP 头     │                          │ IP→UDP→Geneve   │
│ + 外层 Eth 头    │                          │ → 原始帧         │
└────────┬────────┘                          └────────┬────────┘
         ↓                                            ↑
内核路由 + ARP                                内核 IP/UDP 栈处理
         ↓                                            ↑
eth0 DMA 发送                                eth0 DMA 收包
         ↓                                            ↑
物理网络 ═══════════════════════════════════▶ 物理网络

一句话总结

Geneve 发包的核心是 OVS 流表决定目的节点 → geneve_xmit() 通过 skb_push() 依次添加 Geneve 头、UDP 头、外层 IP 头 → 内核路由查找确定出接口和下一跳 → ARP 解析添加外层以太网头 → eth0 网卡驱动 DMA 发送。整个过程中 UDP 源端口随机化实现 ECMP 负载均衡,Geneve Options 携带 OVN 逻辑网络元数据,MTU 需要预留 50+ 字节的封装开销。