VPP的NAT插件: NAT44-EI 实战配置指南

前言

本文详细介绍如何在VMware虚拟机环境中配置VPP的NAT44-EI插件,实现内部网络到外部网络的地址转换。通过实际的操作步骤和原理说明,帮助读者理解NAT44-EI的工作机制。


一、环境准备

1.1 VMware网络配置

网口1:桥接模式(Bridged Mode)
复制代码
配置:
- 模式:桥接模式
- 桥接到:笔记本WiFi网卡(我的笔记本网卡目前地址是192.168.2.13,后面可以根据你实际的IP更换)
- 目的:让虚拟机直接连接到物理网络

原因:
桥接模式使虚拟机获得与物理机同网段的IP地址,可以直接访问外部网络。
这样VPP的NAT功能才能将内部流量转发到外部网络。
网口2:主机模式(Host-Only Mode)
复制代码
配置:
- 模式:主机模式
- 网段:10.0.0.0/24
- 子网掩码:255.255.255.0
- 目的:创建仅主机和虚拟机之间的私有网络

原因:
主机模式创建一个隔离的网络,只有Windows主机和虚拟机可以通信。
这个网络用于模拟"内部网络",测试NAT的转换功能。

配置好后:

1.2 Windows主机网络配置

VMnet虚拟网卡配置
bash 复制代码
# 查看VMnet网卡
ipconfig

# 手动配置VMnet网卡
IP地址:10.0.0.2
子网掩码:255.255.255.0
网关:10.0.0.1

原因说明

  • IP地址10.0.0.2:Windows主机在主机模式网络中的地址
  • 网关10.0.0.1:指向虚拟机的eno2np1接口(稍后配置)
  • 目的 :让Windows主机通过虚拟机访问外部网络
    配置好如下图:

二、VPP基础配置

2.1 准备工作

bash 复制代码
# 1. 在Linux虚拟机中关闭网卡(这里网卡名写自己实际的)
sudo ip link set eno1np0 down
sudo ip link set eno2np1 down

# 2. 加载VFIO模块(用于DPDK)
sudo modprobe vfio-pci

配置config,vpp1.conf这里网卡PCI地址写自己实际的

bash 复制代码
unix {
  nodaemon                    # 前台运行,方便查看日志
  cli-listen /run/vpp/cli.sock
}

api-trace { on }             # 启用 API 跟踪

dpdk {
  dev 0000:0b:00.0 { name eno1np0 }  # 绑定第一个网卡
  dev 0000:13:00.0 { name eno2np1 }  # 绑定第二个网卡
  no-multi-seg               # 关闭多段包(mlx5 推荐)
}

cpu {
  main-core 0                # 主核心
  corelist-workers 1         # Worker 核心
}

buffers {
  buffers-per-numa 131072    # 每个 NUMA 节点的缓冲区数量
  default data-size 2048     # 默认数据包大小
}

plugins {
  plugin dpdk_plugin.so { enable }
  plugin hs_plugin.so { enable }      # HostStack TCP 栈
  plugin app_plugin.so { enable }     # Echo server 命令
  plugin af_packet_plugin.so { enable }
}

session {
  enable
}
socksvr {
  default
}

开启vpp

bash 复制代码
sudo vpp -c ./vpp1.conf

原因

  • 关闭网卡:Linux内核不再管理这些网卡,交给VPP/DPDK控制
  • 加载VFIO:VFIO是用户空间驱动框架,允许VPP直接访问网卡硬件

2.2 配置外部网络接口(eno1np0)

步骤1:设置IP地址
bash 复制代码
vppctl# set int ip addr eno1np0 192.168.2.213/24

原因

  • IP地址192.168.2.213:虚拟机在桥接网络中的地址
  • /24:子网掩码255.255.255.0,与物理网络同网段
  • 目的:让VPP能够与外部网络(192.168.2.0/24)通信
步骤2:添加默认路由
bash 复制代码
vppctl# ip route add 0.0.0.0/0 via 192.168.2.1

原因

  • 0.0.0.0/0:默认路由,匹配所有目标地址
  • via 192.168.2.1:下一跳是WiFi网关
  • 目的:所有发往外网的数据包都通过网关192.168.2.1转发
步骤3:启用接口
bash 复制代码
vppctl# set int state eno1np0 up

原因

  • up:启用接口,使接口处于活动状态
  • 目的:接口必须处于up状态才能收发数据包

2.3 测试外部网络连通性

bash 复制代码
vppctl# ping 183.2.172.17

测试结果:ping通,说明VPP已经可以访问外部网络

原因

  • 183.2.172.17:百度服务器的IP地址
  • ping通:证明路由配置正确,VPP可以访问互联网

三、NAT44-EI配置

3.1 启用NAT44-EI插件

bash 复制代码
vppctl# nat44 ei plugin enable sessions 10000 users 10000

参数说明

  • sessions 10000:最大会话数10000
  • users 10000:最大用户数10000

原因

  • 启用插件:激活NAT44-EI功能
  • 设置限制:防止资源耗尽,限制最大会话和用户数

3.2 添加NAT地址池

bash 复制代码
vppctl# nat44 ei add address 192.168.2.213

原因

  • 192.168.2.213:NAT转换时使用的外部地址
  • 目的:内部网络(10.0.0.0/24)的流量会被转换为192.168.2.213

注意:这个地址必须与eno1np0的IP地址相同,因为NAT使用这个接口的IP作为外部地址。

3.3 配置接口方向

设置内部接口(Inside)
bash 复制代码
vppctl# set int nat44 ei in eno2np1

原因

  • eno2np1:连接主机模式网络的接口(10.0.0.0/24)
  • in:标记为内部接口
  • 目的:从eno2np1进入的数据包被认为是"内部到外部"(IN2OUT)流量
设置外部接口(Outside)
bash 复制代码
vppctl# set int nat44 ei out eno1np0

原因

  • eno1np0:连接桥接网络的接口(192.168.2.0/24)
  • out:标记为外部接口
  • 目的:从eno1np0进入的数据包被认为是"外部到内部"(OUT2IN)流量

3.4 验证配置

bash 复制代码
vppctl# show nat44 ei session

结果:显示没有会话(这是正常的,因为还没有流量通过)

原因

  • NAT会话是动态创建的
  • 只有当有数据包通过时,才会创建会话
  • 此时还没有流量,所以没有会话

四、功能测试

4.1 测试1:从内部网络访问外网

bash 复制代码
# Windows主机执行
ping -S 10.0.0.2 183.2.172.17

参数说明

  • -S 10.0.0.2:使用源IP地址10.0.2发送数据包
  • 183.2.172.17:目标地址(百度服务器)

测试结果:ping通,TTL=53

原因分析

  • ping通:说明NAT转换成功,数据包能够到达外网
  • TTL=53:从Windows到百度经过了多跳,TTL递减
  • 数据流向
    1. Windows (10.0.0.2) → 网关 (10.0.0.1) → 虚拟机 (eno2np1)
    2. VPP NAT44-EI:10.0.0.2 → 192.168.2.213(地址转换)
    3. 虚拟机 (eno1np0) → WiFi网关 (192.168.2.1) → 互联网
    4. 返回路径相反

4.2 测试2:从外部网络访问外网(对比)

bash 复制代码
# Windows主机执行
ping -S 192.168.2.13 183.2.172.17 -t

参数说明

  • -S 192.168.2.13:使用WiFi网卡的IP地址
  • -t:持续ping

测试结果:ping通,TTL=54

原因分析

  • TTL=54 vs TTL=53:TTL多1,说明少经过了一跳
  • 原因:直接从WiFi网卡发送,不经过VPP NAT44-EI处理
  • 对比 :说明VPP NAT44-EI确实在工作,作为路由器转发数据包时减少了TTL

五、数据流向详解

5.1 完整数据流向图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│ 场景:Windows主机 (10.0.0.2) ping 百度服务器 (183.2.172.17)     │
└─────────────────────────────────────────────────────────────────┘

【请求数据包流向】

┌──────────────┐
│  Windows     │
│  10.0.0.2    │
└──────┬───────┘
       │ ICMP Request
       │ 源IP: 10.0.0.2
       │ 目标IP: 183.2.172.17
       │
       ▼
┌──────────────┐
│  网关路由    │
│  10.0.0.1    │ ← Windows配置的网关指向虚拟机
└──────┬───────┘
       │
       ▼
┌─────────────────────────────────────────────────────────────┐
│                    VMware虚拟机                              │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  eno2np1 (Inside接口)                                 │  │
│  │  IP: 10.0.0.1                                        │  │
│  │  接收数据包:源10.0.0.2 → 目标183.2.172.17          │  │
│  └────────────────┬─────────────────────────────────────┘  │
│                   │                                        │
│                   ▼                                        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  VPP NAT44-EI 处理                                   │  │
│  │  1. 识别方向:IN2OUT(内部到外部)                    │  │
│  │  2. 查找会话:未找到,创建新会话                      │  │
│  │  3. 地址转换:                                        │  │
│  │     源IP: 10.0.0.2 → 192.168.2.213                   │  │
│  │     源端口: 随机端口 → 随机端口                       │  │
│  │  4. 记录会话:                                        │  │
│  │     内部: 10.0.0.2:端口                               │  │
│  │     外部: 192.168.2.213:端口                          │  │
│  │  5. IP路由转发:                                      │  │
│  │     TTL: 64 → 63 ← 这里减少1!                       │  │
│  │     (VPP作为路由器转发时减少TTL)                      │  │
│  └────────────────┬─────────────────────────────────────┘  │
│                   │                                        │
│                   ▼                                        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  eno1np0 (Outside接口)                                │  │
│  │  IP: 192.168.2.213                                   │  │
│  │  发送数据包:源192.168.2.213 → 目标183.2.172.17      │  │
│  │  TTL: 63(已减少1)                                   │  │
│  └────────────────┬─────────────────────────────────────┘  │
└───────────────────┼────────────────────────────────────────┘
                    │
                    ▼
┌──────────────┐
│  WiFi网卡    │
│ 192.168.2.13 │ ← 桥接模式,虚拟机通过此网卡访问物理网络
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   WiFi网关   │
│ 192.168.2.1  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   互联网     │
│              │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  百度服务器  │
│ 183.2.172.17 │
└──────────────┘


【响应数据包流向】

┌──────────────┐
│  百度服务器  │
│ 183.2.172.17 │
└──────┬───────┘
       │ ICMP Reply
       │ 源IP: 183.2.172.17
       │ 目标IP: 192.168.2.213 ← NAT转换后的地址
       │
       ▼
┌──────────────┐
│   互联网     │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   WiFi网关   │
│ 192.168.2.1  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  WiFi网卡    │
│ 192.168.2.13 │
└──────┬───────┘
       │
       ▼
┌─────────────────────────────────────────────────────────────┐
│                    VMware虚拟机                              │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  eno1np0 (Outside接口)                               │  │
│  │  接收数据包:源183.2.172.17 → 目标192.168.2.213     │  │
│  └────────────────┬─────────────────────────────────────┘  │
│                   │                                        │
│                   ▼                                        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  VPP NAT44-EI 处理                                   │  │
│  │  1. 识别方向:OUT2IN(外部到内部)                    │  │
│  │  2. 查找会话:根据目标IP和端口查找会话                │  │
│  │  3. 地址转换:                                        │  │
│  │     目标IP: 192.168.2.213 → 10.0.0.2                 │  │
│  │     目标端口: 外部端口 → 内部端口                     │  │
│  │  4. 更新会话:更新最后活动时间                        │  │
│  └────────────────┬─────────────────────────────────────┘  │
│                   │                                        │
│                   ▼                                        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  eno2np1 (Inside接口)                                │  │
│  │  发送数据包:源183.2.172.17 → 目标10.0.0.2           │  │
│  └────────────────┬─────────────────────────────────────┘  │
└───────────────────┼────────────────────────────────────────┘
                    │
                    ▼
┌──────────────┐
│  网关路由    │
│  10.0.0.1    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Windows     │
│  10.0.0.2    │ ← 收到响应,ping成功!
└──────────────┘

此时查看vpp活跃节点流图如下:

数据在网卡接收后经历了ip4-inputh后经过了ip4-sv-reassembly-feature、nat44-ei-in2out、nat44-ei-out2in、nat44-ei-in2out-slowpath、ip4-lookup、ip4-rewrite然后流向网卡

5.2 关键转换点说明

转换点1:IN2OUT(内部到外部)
复制代码
转换前:
  源IP: 10.0.0.2
  源端口: 随机端口(例如5000)
  目标IP: 183.2.172.17
  目标端口: 0(ICMP)

转换后:
  源IP: 192.168.2.213 ← NAT地址池中的地址
  源端口: 随机端口(例如6000)
  目标IP: 183.2.172.17(不变)
  目标端口: 0(不变)
转换点2:OUT2IN(外部到内部)
复制代码
转换前:
  源IP: 183.2.172.17
  源端口: 0
  目标IP: 192.168.2.213 ← NAT转换后的地址
  目标端口: 6000 ← NAT分配的端口

转换后:
  源IP: 183.2.172.17(不变)
  源端口: 0(不变)
  目标IP: 10.0.0.2 ← 还原为原始内部地址
  目标端口: 5000 ← 还原为原始内部端口

4.3 TTL差异分析:为什么少了一个TTL?

TTL对比
复制代码
测试1:ping -S 10.0.0.2 183.2.172.17
  结果:TTL = 53

测试2:ping -S 192.168.2.13 183.2.172.17
  结果:TTL = 54

差异:TTL相差1
TTL减少的原因

答案:TTL在VPP NAT44-EI处理时减少的!

详细分析

复制代码
路径1(经过NAT,TTL=53):
  Windows (10.0.0.2)
    ↓ TTL=64(Windows默认)
  网关 (10.0.0.1)
    ↓ TTL=64(网关不减少TTL,因为是二层转发)
  VPP NAT44-EI (eno2np1 → eno1np0)
    ↓ TTL=63 ← 这里减少了1!
  WiFi网卡 (192.168.2.13)
    ↓ TTL=63
  WiFi网关 (192.168.2.1)
    ↓ TTL=62(网关转发时减少)
  ...(经过多跳路由)
    ↓
  百度服务器 (183.2.172.17)
    ↓ 收到时TTL=53

路径2(不经过NAT,TTL=54):
  Windows WiFi网卡 (192.168.2.13)
    ↓ TTL=64(Windows默认)
  WiFi网关 (192.168.2.1)
    ↓ TTL=63(网关转发时减少)
  ...(经过多跳路由)
    ↓
  百度服务器 (183.2.172.17)
    ↓ 收到时TTL=54
为什么VPP会减少TTL?

原因

  1. VPP作为路由器

    • 当数据包从eno2np1进入,从eno1np0发出时
    • VPP实际上是在转发数据包(路由转发)
    • 根据IP协议规范,路由器转发数据包时必须减少TTL
  2. IP转发机制

    复制代码
    VPP的IP转发流程:
    1. 数据包进入VPP(eno2np1)
    2. NAT44-EI处理(地址转换)
    3. IP路由查找(ip4-lookup节点)
    4. 转发数据包(减少TTL)← 这里!
    5. 数据包发出(eno1np0)
  3. TTL检查代码

    c 复制代码
    // 文件:src/vnet/ip/ip4_forward.c
    // 函数:ip4_ttl_and_checksum_check
    
    static_always_inline void
    ip4_ttl_and_checksum_check (vlib_buffer_t * b, ip4_header_t * ip, ...)
    {
      ttl = ip->ttl;
      // 减少TTL
      ttl -= 1;
      ip->ttl = ttl;
      
      // 如果TTL<=0,发送ICMP Time Exceeded
      if (PREDICT_FALSE (ttl <= 0)) {
        icmp4_error_set_vnet_buffer (b, ICMP4_time_exceeded, ...);
      }
    }
    
    // 这个函数在ip4_rewrite_inline中被调用
    // 当VPP转发数据包时,会调用此函数减少TTL

六、配置验证和监控

6.1 查看NAT会话

bash 复制代码
vppctl# show nat44 ei session

执行ping后再次查看,应该能看到活跃的会话:

6.2 查看NAT统计信息

bash 复制代码
vppctl# show nat44 ei statistics

原因:监控NAT转换的统计信息,包括:

  • 转换的数据包数量
  • 创建的会话数量
  • 错误统计

七、常见问题排查

7.1 ping不通外部网络

可能原因

  1. 路由配置错误:检查默认路由是否正确
  2. 接口未启用:确认eno1np0处于up状态
  3. 网关不可达:ping网关192.168.2.1测试连通性

7.2 NAT转换不工作

可能原因

  1. 接口方向配置错误:确认eno2np1是inside,eno1np0是outside
  2. NAT地址未添加:确认已添加192.168.2.213到地址池
  3. 会话限制:检查是否达到最大会话数限制

7.3 Windows无法访问外网

可能原因

  1. 网关配置错误:Windows的VMnet网卡网关应设置为10.0.0.1
  2. 路由表问题:检查Windows路由表,确认10.0.0.0/24网段的路由
  3. 防火墙阻止:检查Windows防火墙是否阻止了ICMP

八、总结

8.1 配置要点回顾

  1. 网络拓扑

    • 桥接模式:连接外部网络(192.168.2.0/24)
    • 主机模式:创建内部网络(10.0.0.0/24)
  2. VPP配置

    • 外部接口:eno1np0(192.168.2.213)
    • 内部接口:eno2np1(10.0.0.1)
    • NAT地址池:192.168.2.213
  3. NAT工作原理

    • IN2OUT:内部地址转换为外部地址
    • OUT2IN:外部地址还原为内部地址
    • 会话跟踪:维护地址和端口映射关系

8.2 实际应用场景

这个配置可以应用于:

  • 企业网络:内部员工通过NAT访问互联网
  • 云环境:虚拟机通过NAT访问公网
  • 测试环境:模拟NAT环境进行应用测试

8.3 扩展学习

  • NAT44-ED vs NAT44-EI:了解两种NAT模式的区别
  • 静态映射:配置端口转发和静态NAT
  • 高可用性:配置NAT高可用性(HA)
  • 性能优化:调整会话数和用户数限制

附录:完整配置命令清单

bash 复制代码
# VPP基础配置
vppctl# set int ip addr eno1np0 192.168.2.213/24
vppctl# ip route add 0.0.0.0/0 via 192.168.2.1
vppctl# set int state eno1np0 up
vppctl# ping 183.2.172.17

# NAT44-EI配置
vppctl# nat44 ei plugin enable sessions 10000 users 10000
vppctl# nat44 ei add address 192.168.2.213
vppctl# set int nat44 ei in eno2np1
vppctl# set int nat44 ei out eno1np0

# 验证和监控
vppctl# show nat44 ei session
vppctl# show nat44 ei statistics

九、NAT44-EI源码深度解析:结合实战流程

本章节结合前面的实战配置流程,深入分析NAT44-EI的源码实现。我们将以实际测试场景(Windows 10.0.0.2 ping 183.2.172.17)为例,逐步追踪数据包在VPP中的处理过程,并详细解析每个关键函数的代码实现。

9.1 Feature Arc注册:NAT44-EI如何插入到数据包处理流程

9.1.1 NAT44-EI插入的Feature Arc

在实战配置中,当我们执行 set int nat44 ei in eno2np1set int nat44 ei out eno1np0 时,NAT44-EI的节点被注册到了 ip4-unicast Feature Arc中。

c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei.c

// 1. 分类节点(主线程)
VNET_FEATURE_INIT (ip4_nat_classify, static) = {
  .arc_name = "ip4-unicast",
  .node_name = "nat44-ei-classify",
  .runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
                               "ip4-sv-reassembly-feature"),
};

// 2. IN2OUT 节点(内部到外部)
VNET_FEATURE_INIT (ip4_nat44_ei_in2out, static) = {
  .arc_name = "ip4-unicast",
  .node_name = "nat44-ei-in2out",
  .runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
                               "ip4-sv-reassembly-feature"),
};

// 3. OUT2IN 节点(外部到内部)
VNET_FEATURE_INIT (ip4_nat44_ei_out2in, static) = {
  .arc_name = "ip4-unicast",
  .node_name = "nat44-ei-out2in",
  .runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
                               "ip4-sv-reassembly-feature",
                               "ip4-dhcp-client-detect"),
};

实战对应

  • 当数据包从 eno2np1(内部接口)进入时,会经过 nat44-ei-in2out 节点
  • 当数据包从 eno1np0(外部接口)进入时,会经过 nat44-ei-out2in 节点
9.1.2 为什么NAT必须在 ip4-sv-reassembly-feature 之后运行?
1. ip4-sv-reassembly-feature的作用

ip4-sv-reassembly-featureIPv4 Shallow Virtual Reassembly(IPv4浅层虚拟重组)功能节点。

主要功能

  • 处理IP分片:处理被分片的IP数据包
  • 提取L4信息:从第一个分片中提取L4层信息(源端口、目标端口、协议等)
  • 存储L4信息 :将L4信息存储在 vnet_bufferip.reass 字段中
  • 非分片处理:对于非分片数据包,直接提取L4信息并存储

代码实现

c 复制代码
// 文件:src/vnet/ip/reass/ip4_sv_reass.c
// 函数:ip4_sv_reass_inline()

// 对于非分片数据包(快速路径)
if (PREDICT_TRUE (!ip4_get_fragment_more (ip0) && 
                  !ip4_get_fragment_offset (ip0))) {
  // 提取L4端口信息
  vnet_buffer (b0)->ip.reass.l4_src_port = ip4_get_port (ip0, 1);
  vnet_buffer (b0)->ip.reass.l4_dst_port = ip4_get_port (ip0, 0);
  vnet_buffer (b0)->ip.reass.ip_proto = ip0->protocol;
  
  // 提取TCP/UDP/ICMP特定信息
  if (IP_PROTOCOL_TCP == ip0->protocol) {
    vnet_buffer (b0)->ip.reass.tcp_seq_number = th->seq_number;
    vnet_buffer (b0)->ip.reass.tcp_ack_number = th->ack_number;
    vnet_buffer (b0)->ip.reass.icmp_type_or_tcp_flags = th->flags;
  } else if (IP_PROTOCOL_ICMP == ip0->protocol) {
    vnet_buffer (b0)->ip.reass.icmp_type_or_tcp_flags = icmp->type;
  }
  
  // 标记不是非首分片
  vnet_buffer (b0)->ip.reass.is_non_first_fragment = 0;
}
2. NAT为什么需要L4信息?

NAT的核心功能

  • 会话查找:根据源IP、源端口、FIB索引、协议查找会话
  • 端口转换:修改源端口或目标端口
  • 会话创建:为新连接分配外部端口

NAT需要访问L4端口信息

c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_in2out.c
// 函数:nat44_ei_in2out_node_fn_inline()

// NAT构建查找键时需要源端口
init_nat_k (&kv0, ip0->src_address,                    // 源IP
            vnet_buffer (b0)->ip.reass.l4_src_port,    // 源端口 ← 需要!
            rx_fib_index0,                              // FIB索引
            proto0);                                    // 协议

// NAT转换时需要修改端口
if (!vnet_buffer (b0)->ip.reass.is_non_first_fragment) {
  old_port0 = vnet_buffer (b0)->ip.reass.l4_src_port;  // 源端口 ← 需要!
  udp0->src_port = s0->out2in.port;                    // 新端口
}

实战对应

  • Windows ping百度时,NAT需要知道ICMP的ID(相当于端口)
  • 如果没有L4信息,NAT无法构建查找键,无法查找会话
3. IP分片的问题

IP分片的特性

  • 第一个分片:包含完整的IP头和L4头(TCP/UDP端口)
  • 后续分片:只包含IP头,不包含L4头(没有端口信息)

问题场景

复制代码
分片数据包示例:

分片1(第一个分片):
  IP头:源IP=10.0.0.2, 目标IP=183.2.172.17
  TCP头:源端口=5000, 目标端口=443  ← 包含L4信息

分片2(后续分片):
  IP头:源IP=10.0.0.2, 目标IP=183.2.172.17
  数据:...(没有TCP头,没有端口信息)  ← 不包含L4信息

如果没有ip4-sv-reassembly-feature

  • NAT处理分片2时,无法获取端口信息
  • 无法构建查找键,无法查找会话
  • NAT转换失败!

有了ip4-sv-reassembly-feature

  • 从分片1中提取L4信息
  • 将L4信息存储在 vnet_buffer->ip.reass
  • 所有分片(包括分片2)都可以访问这些L4信息
  • NAT可以正确转换所有分片
4. 为什么即使没有ACL也必须启用?

关键理解

  • ACL规则:用于访问控制,是可选的
  • L4信息提取:NAT必须的,不是可选的

执行顺序

复制代码
数据包进入 → ip4-input
  ↓
ip4-unicast arc
  ├─ acl-plugin-in-ip4-fa (ACL检查) ← 可选,可能未启用
  ├─ ip4-sv-reassembly-feature (L4信息提取) ← 必须!NAT需要
  ├─ nat44-ei-classify (NAT分类)
  ├─ nat44-ei-in2out (NAT转换) ← 需要L4信息
  └─ ip4-lookup (路由查找)

即使ACL未启用

  • acl-plugin-in-ip4-fa 可能不在Feature Arc中
  • ip4-sv-reassembly-feature 必须在NAT之前运行
  • 因为NAT必须访问L4信息
5. ip4-sv-reassembly-feature在ip4-input后做了什么?

数据包处理流程

复制代码
1. 数据包进入 ip4-input 节点
   - 解析IP头
   - 验证IP校验和
   - 检查TTL
   - 发送到 ip4-unicast arc

2. 进入 ip4-sv-reassembly-feature 节点
   - 检查是否是分片数据包
   - 如果是分片:
     a. 创建或查找重组上下文
     b. 缓存分片
     c. 等待所有分片到达
     d. 从第一个分片提取L4信息
   - 如果不是分片:
     a. 直接从IP包中提取L4信息
     b. 存储到 vnet_buffer->ip.reass 字段
   - 标记数据包状态(is_non_first_fragment等)

3. 继续处理
   - 发送到下一个Feature节点(NAT)
   - NAT可以访问 vnet_buffer->ip.reass.l4_src_port 等信息

代码实现细节

c 复制代码
// 文件:src/vnet/ip/reass/ip4_sv_reass.c
// 函数:ip4_sv_reass_inline()

// 检查是否是分片
if (PREDICT_FALSE (ip4_get_fragment_more (ip0) ||
                   ip4_get_fragment_offset (ip0))) {
  // 是分片 → 慢路径处理
  goto slow_path;
}

// 不是分片 → 快速路径
// 提取L4信息
if (!l4_hdr_truncated (ip0)) {
  // L4头完整,提取端口信息
  vnet_buffer (b0)->ip.reass.l4_src_port = ip4_get_port (ip0, 1);
  vnet_buffer (b0)->ip.reass.l4_dst_port = ip4_get_port (ip0, 0);
  vnet_buffer (b0)->ip.reass.ip_proto = ip0->protocol;
  
  // 提取TCP特定信息
  if (IP_PROTOCOL_TCP == ip0->protocol) {
    tcp_header_t *th = ip4_next_header (ip0);
    vnet_buffer (b0)->ip.reass.tcp_seq_number = th->seq_number;
    vnet_buffer (b0)->ip.reass.tcp_ack_number = th->ack_number;
  }
  
  // 标记不是非首分片
  vnet_buffer (b0)->ip.reass.is_non_first_fragment = 0;
} else {
  // L4头被截断(分片导致)
  vnet_buffer (b0)->ip.reass.l4_hdr_truncated = 1;
}
6. NAT如何使用这些L4信息?

NAT44-EI使用L4信息的示例

c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_in2out.c

// 1. 构建查找键(需要源端口)
init_nat_k (&kv0, ip0->src_address,                    // 10.0.0.2
            vnet_buffer (b0)->ip.reass.l4_src_port,    // 5000 ← 来自ip4-sv-reassembly-feature
            rx_fib_index0,                              // FIB索引
            proto0);                                    // TCP

// 2. 查找会话
clib_bihash_search_8_8 (&nm->in2out, &kv0, &value0);

// 3. 转换端口(需要源端口)
if (!vnet_buffer (b0)->ip.reass.is_non_first_fragment) {
  old_port0 = vnet_buffer (b0)->ip.reass.l4_src_port;  // 5000 ← 来自ip4-sv-reassembly-feature
  udp0->src_port = s0->out2in.port;                    // 6000(新端口)
}

实战对应

  • Windows ping百度时,ICMP包经过 ip4-sv-reassembly-feature
  • 提取ICMP ID(存储在 l4_src_port 字段)
  • NAT使用ICMP ID构建查找键和进行转换
7. 总结

为什么NAT必须在ip4-sv-reassembly-feature之后运行?

  1. L4信息提取:NAT需要访问L4端口信息来进行会话查找和转换
  2. 分片支持:对于分片数据包,只有第一个分片包含L4信息,后续分片需要通过重组上下文获取
  3. 统一接口ip4-sv-reassembly-feature 提供了统一的接口,让NAT可以访问L4信息,无论数据包是否分片
  4. 性能优化 :对于非分片数据包,ip4-sv-reassembly-feature 快速提取L4信息;对于分片数据包,进行重组处理

在ip4-input后做了什么

  • 检查数据包是否是分片
  • 提取L4层信息(端口、协议等)
  • 将L4信息存储到 vnet_buffer->ip.reass 字段
  • 为后续的NAT处理提供L4信息访问接口
9.1.3 Feature Arc执行顺序
复制代码
数据包进入 → ip4-unicast arc
  ├─ acl-plugin-in-ip4-fa (ACL检查)
  ├─ ip4-sv-reassembly-feature (分片重组)
  ├─ nat44-ei-classify (NAT分类节点) ← 判断方向
  │   ├─ 目标地址是NAT地址池?→ OUT2IN
  │   └─ 否则 → IN2OUT
  ├─ nat44-ei-in2out (内部到外部转换)
  │   └─ 修改源IP和端口:10.0.0.2 → 192.168.2.213
  ├─ nat44-ei-out2in (外部到内部转换)
  │   └─ 修改目标IP和端口:192.168.2.213 → 10.0.0.2
  └─ ip4-lookup (路由查找)
      └─ 根据目标IP查找路由
      └─ 发送到输出接口

9.2 接口配置源码解析:set int nat44 ei in eno2np1 的完整调用链

9.2.1 CLI命令到代码的完整链路

实战操作

bash 复制代码
vppctl# set int nat44 ei in eno2np1

完整调用链

复制代码
1. CLI输入
   ↓
   vpp# set int nat44 ei in eno2np1

2. VPP CLI解析
   ↓
   VLIB_CLI_COMMAND (set_interface_nat44_ei_command)
   文件:src/plugins/nat/nat44-ei/nat44_ei_cli.c:1891

3. CLI命令函数
   ↓
   nat44_ei_feature_command_fn()
   文件:src/plugins/nat/nat44-ei/nat44_ei_cli.c:852
   功能:
   - 解析 "in eno2np1"
   - 将接口名称转换为 sw_if_index
   - 调用 nat44_ei_add_interface(sw_if_index, 1)

4. 接口添加函数
   ↓
   nat44_ei_add_interface(sw_if_index, is_inside)
   文件:src/plugins/nat/nat44-ei/nat44_ei.c:632
   功能:
   - 创建或获取接口结构
   - 设置接口标志位(IS_INSIDE)
   - 注册Feature节点到接口

5. Feature注册
   ↓
   vnet_feature_enable_disable("ip4-unicast", "nat44-ei-in2out", 
                                sw_if_index, 1, 0, 0)
   功能:
   - 将 "nat44-ei-in2out" 节点注册到接口的 ip4-unicast arc
   - 当数据包从该接口进入时,会经过 nat44-ei-in2out 节点
9.2.2 接口添加函数详解
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei.c:632

int nat44_ei_add_interface (u32 sw_if_index, u8 is_inside)
{
  nat44_ei_interface_t *i;
  const char *feature_name;
  int rv;
  
  // 1. 检查插件是否启用
  fail_if_disabled ();
  // 实战对应:确保已经执行了 "nat44 ei plugin enable"
  
  // 2. 获取或创建接口结构
  i = nat44_ei_get_interface (nm->interfaces, sw_if_index);
  
  if (!i) {
    // 创建新接口
    pool_get (nm->interfaces, i);
    i->sw_if_index = sw_if_index;  // eno2np1的接口索引
    i->flags = 0;
  }
  
  // 3. 设置标志位
  if (is_inside) {
    i->flags |= NAT44_EI_INTERFACE_FLAG_IS_INSIDE;
    feature_name = "nat44-ei-in2out";  // 内部接口使用IN2OUT节点
  } else {
    i->flags |= NAT44_EI_INTERFACE_FLAG_IS_OUTSIDE;
    feature_name = "nat44-ei-out2in";  // 外部接口使用OUT2IN节点
  }
  
  // 4. 注册Feature节点到接口
  rv = vnet_feature_enable_disable ("ip4-unicast", feature_name,
                                    sw_if_index, 1, 0, 0);
  // 参数说明:
  // - "ip4-unicast": Feature Arc名称
  // - feature_name: Feature节点名称(nat44-ei-in2out 或 nat44-ei-out2in)
  // - sw_if_index: 接口索引(eno2np1)
  // - 1: 启用(enable)
  
  return rv;
}

代码解析

为什么要检查插件是否启用?

  • 状态验证 :确保NAT44-EI插件已经启用(对应实战中的 nat44 ei plugin enable
  • 错误处理:如果未启用,返回错误,避免无效配置
  • 一致性:确保配置在正确的状态下进行

为什么要获取或创建接口结构?

  • 接口管理:每个接口需要单独管理(eno2np1和eno1np0分别管理)
  • 状态保存:保存接口的NAT配置状态(inside或outside)
  • 避免重复:如果接口已存在,直接使用,避免重复创建

为什么要设置标志位?

  • 方向识别 :通过标志位识别接口是内部还是外部
    • eno2np1NAT44_EI_INTERFACE_FLAG_IS_INSIDE
    • eno1np0NAT44_EI_INTERFACE_FLAG_IS_OUTSIDE
  • 快速判断:数据包进入时,可以通过接口标志快速判断方向
  • 功能区分:内部接口用于IN2OUT转换,外部接口用于OUT2IN转换

为什么要注册Feature节点?

  • 数据包处理 :当数据包从该接口进入时,会经过对应的NAT节点
    • eno2np1 进入 → 经过 nat44-ei-in2out 节点
    • eno1np0 进入 → 经过 nat44-ei-out2in 节点
  • 按需处理:只有配置了NAT的接口才会处理NAT转换
  • 性能优化:未配置的接口不经过NAT处理,提高性能

在整个NAT体系中的必要性

  1. 配置管理:接口配置是NAT工作的基础,没有接口配置,NAT无法工作
  2. 方向识别:接口类型用于识别数据包方向,这是NAT正确工作的前提
  3. 性能优化:按需处理,只有配置的接口才处理NAT,提高性能
  4. 灵活性:可以灵活配置哪些接口需要NAT

9.3 数据包处理流程源码解析:从Windows ping到百度服务器

9.3.1 场景回顾

实战场景

bash 复制代码
# Windows主机执行
ping -S 10.0.0.2 183.2.172.17

数据包信息

  • 源IP:10.0.0.2(Windows主机)
  • 目标IP:183.2.172.17(百度服务器)
  • 协议:ICMP
  • 接收接口:eno2np1(内部接口)
9.3.2 步骤1:数据包进入VPP并进入Feature Arc
复制代码
数据包:
  源IP:10.0.0.2
  目标IP:183.2.172.17
  协议:ICMP
  接收接口:eno2np1 (inside接口)
  TTL:64(Windows默认)

数据包进入VPP后,进入 ip4-unicast Feature Arc,经过ACL检查和分片重组后,到达 nat44-ei-classify 节点。

9.3.3 步骤2:分类节点判断方向(nat44-ei-classify)
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei.c
// 函数:nat44_ei_classify_inline_fn()

static_always_inline uword
nat44_ei_classify_inline_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
                             vlib_frame_t *frame)
{
  // 1. 获取数据包
  b0 = vlib_get_buffer (vm, bi0);
  ip0 = vlib_buffer_get_current (b0);
  // ip0->src_address = 10.0.0.2
  // ip0->dst_address = 183.2.172.17
  
  // 2. 默认是 IN2OUT(内部到外部)
  u32 next0 = NAT44_EI_CLASSIFY_NEXT_IN2OUT;
  
  // 3. 检查目标地址是否是NAT地址池中的地址
  vec_foreach (ap, nm->addresses) {
    // nm->addresses 包含:192.168.2.213(实战配置中添加的)
    if (ip0->dst_address.as_u32 == ap->addr.as_u32) {
      // 183.2.172.17 != 192.168.2.213 → 不匹配
      // 目标地址是NAT地址 → OUT2IN
      next0 = NAT44_EI_CLASSIFY_NEXT_OUT2IN;
      goto enqueue0;
    }
  }
  // 实战中:目标地址183.2.172.17不在NAT地址池中,所以不进入此分支
  
  // 4. 检查静态映射
  if (pool_elts (nm->static_mappings)) {
    // 实战中:没有配置静态映射,所以不进入此分支
    init_nat_k (&kv0, ip0->dst_address, 0, 0, 0);
    if (!clib_bihash_search_8_8 (&nm->static_mapping_by_external,
                                 &kv0, &value0)) {
      m = pool_elt_at_index (nm->static_mappings, value0.value);
      if (m->local_addr.as_u32 != m->external_addr.as_u32) {
        next0 = NAT44_EI_CLASSIFY_NEXT_OUT2IN;
      }
    }
  }
  
  // 5. 发送到对应的节点
  // 实战中:next0 = NAT44_EI_CLASSIFY_NEXT_IN2OUT
  // 发送到:nat44-ei-in2out 节点
  vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next,
                                   n_left_to_next, bi0, next0);
}

代码解析

为什么默认是 IN2OUT?

  • 常见场景:大多数情况下,数据包是从内部网络发起的(内部主机访问外部服务器)
  • 性能优化:默认假设是 IN2OUT,可以减少不必要的检查
  • 实战对应:Windows主机访问百度服务器,确实是内部到外部的流量

为什么要检查NAT地址池?

  • 方向识别 :NAT地址池(nm->addresses)存储所有可用的公网IP地址
    • 实战中:nm->addresses 包含 192.168.2.213(通过 nat44 ei add address 192.168.2.213 添加)
  • OUT2IN判断:如果目标地址是NAT地址池中的地址,说明这是外部到内部的流量
  • 实战对应
    • 请求包:目标地址 183.2.172.17 不在NAT地址池中 → IN2OUT
    • 响应包:目标地址 192.168.2.213 在NAT地址池中 → OUT2IN

为什么要检查静态映射?

  • 服务器发布:静态映射用于服务器发布,外部主机通过NAT地址访问内部服务器
  • 精确匹配:静态映射可以精确匹配IP和端口
  • 实战对应:实战中没有配置静态映射,所以不进入此分支

在整个NAT体系中的必要性

  1. 性能优化:分类节点在数据包处理的最早阶段,快速判断方向,避免后续不必要的处理
  2. 正确性保证:准确的方向判断是NAT正确工作的前提,错误的方向会导致数据包无法正确转换
  3. 灵活性:支持多种场景(动态NAT、静态映射、服务器发布等)

结果 :数据包被发送到 nat44-ei-in2out 节点

9.3.4 步骤3:IN2OUT节点处理(nat44-ei-in2out)
3.1 查找会话
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_in2out.c
// 函数:nat44_ei_in2out_node_fn_inline()

static_always_inline uword
nat44_ei_in2out_node_fn_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
                                vlib_frame_t *frame, int is_slow_path,
                                int is_output_feature)
{
  // 1. 获取数据包信息
  ip0 = vlib_buffer_get_current (b0);
  rx_sw_if_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX];
  // rx_sw_if_index0 = eno2np1的接口索引
  
  rx_fib_index0 = vec_elt (nm->ip4_main->fib_index_by_sw_if_index,
                          rx_sw_if_index0);
  // 获取接收接口的FIB索引
  
  // 2. 确定协议类型
  proto0 = ip_proto_to_nat_proto (ip0->protocol);
  // proto0 = NAT_PROTOCOL_ICMP(ICMP协议)
  
  // 3. 构建查找键(4元组:源IP、源端口、FIB索引、协议)
  init_nat_k (&kv0, ip0->src_address,                    // 10.0.0.2
              vnet_buffer (b0)->ip.reass.l4_src_port,    // ICMP没有端口,使用0
              rx_fib_index0,                              // FIB索引
              proto0);                                    // ICMP
  
  // 4. 在 in2out 哈希表中查找会话
  if (clib_bihash_search_8_8 (&nm->in2out, &kv0, &value0) != 0) {
    // 未找到会话 → 需要创建新会话(慢路径)
    // 实战中:第一次ping,没有会话,进入慢路径
    if (is_slow_path) {
      // 调用 slow_path() 创建新会话
      next0 = slow_path (nm, b0, ip0, ip0->src_address,
                        vnet_buffer (b0)->ip.reass.l4_src_port,
                        rx_fib_index0, proto0, &s0, node, next0,
                        thread_index, now);
    } else {
      // 发送到慢路径节点
      next0 = NAT44_EI_IN2OUT_NEXT_SLOW_PATH;
    }
  } else {
    // 找到会话 → 快速路径
    // 实战中:第二次ping时,会话已存在,走快速路径
    s0 = pool_elt_at_index (nm->per_thread_data[thread_index].sessions,
                           nat_value_get_session_index (&value0));
  }
}

代码解析

为什么要获取FIB索引?

  • FIB(Forwarding Information Base):路由表索引
  • 多VRF支持:不同的接口可能属于不同的VRF(Virtual Routing and Forwarding)
  • 会话区分:同一个IP地址在不同VRF中可能有不同的NAT映射
  • 实战对应:实战中只有一个VRF,FIB索引用于区分不同VRF中的会话
  • 什么是FIB和VRF :文末问答环节有提及
    为什么使用4元组作为查找键?
  • 端点无关(Endpoint Independent):NAT44-EI的特点
  • 4元组:源IP、源端口、FIB索引、协议
  • 不包含目标地址和端口:同一个内部IP和端口,访问不同目标时使用相同的外部端口
  • 实战对应
    • 第一次ping:10.0.0.2:0 → 创建会话,分配外部端口
    • 第二次ping:10.0.0.2:0 → 找到已有会话,使用相同的外部端口
  • 端口复用:这样可以实现端口复用,提高端口利用率

为什么要区分快速路径和慢路径?

  • 快速路径:已存在会话,直接查找哈希表,性能高
  • 慢路径:新会话,需要分配地址和端口,创建会话,性能较低
  • 分离设计:分离设计可以提高整体性能,大多数数据包走快速路径
  • 实战对应
    • 第一次ping:走慢路径,创建会话
    • 后续ping:走快速路径,直接转换

为什么使用哈希表查找?

  • 性能:哈希表查找时间复杂度为 O(1),性能高
  • 可扩展性:NAT需要处理大量并发连接,哈希表可以快速查找会话
  • 数据结构clib_bihash_8_8_t 是VPP优化的双向哈希表,支持8字节键和8字节值
3.2 创建新会话(慢路径)
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_in2out.c
// 函数:slow_path()

static u32
slow_path (nat44_ei_main_t *nm, vlib_buffer_t *b0, ip4_header_t *ip0,
           ip4_address_t i2o_addr, u16 i2o_port, u32 rx_fib_index0,
           nat_protocol_t nat_proto, nat44_ei_session_t **sessionp,
           vlib_node_runtime_t *node, u32 next0, u32 thread_index, f64 now)
{
  // 1. 检查是否超过最大会话数
  if (nat44_ei_maximum_sessions_exceeded (nm, thread_index)) {
    // 实战中:sessions 10000,通常不会超过
    return NAT44_EI_IN2OUT_NEXT_DROP;
  }
  
  // 2. 尝试匹配静态映射
  if (nat44_ei_static_mapping_match (i2o_addr, i2o_port, rx_fib_index0,
                                     nat_proto, &sm_addr, &sm_port,
                                     &sm_fib_index, 0, 0, &identity_nat)) {
    // 实战中:没有配置静态映射,不进入此分支
    // 没有静态映射 → 需要动态分配
    // 分配外部地址和端口
    if (nm->alloc_addr_and_port (
          nm->addresses, rx_fib_index0, thread_index, nat_proto,
          ip0->src_address, &sm_addr, &sm_port, nm->port_per_thread,
          nm->per_thread_data[thread_index].snat_thread_index)) {
      // 分配失败
      return NAT44_EI_IN2OUT_NEXT_DROP;
    }
  }
  // 实战中:动态分配外部地址和端口
  // sm_addr = 192.168.2.213(从NAT地址池中分配)
  // sm_port = 随机端口(例如6000)
  
  // 3. 获取或创建用户
  u = nat44_ei_user_get_or_create (nm, &ip0->src_address, rx_fib_index0,
                                   thread_index);
  // 实战中:创建用户,内部IP = 10.0.0.2
  
  // 4. 分配或回收会话
  s = nat44_ei_session_alloc_or_recycle (nm, u, thread_index, now);
  
  // 5. 设置会话信息
  s->in2out.addr = i2o_addr;           // 10.0.0.2
  s->in2out.port = i2o_port;          // 0(ICMP没有端口)
  s->in2out.fib_index = rx_fib_index0;
  s->nat_proto = nat_proto;            // ICMP
  s->out2in.addr = sm_addr;            // 192.168.2.213(分配的外部地址)
  s->out2in.port = sm_port;            // 6000(分配的外部端口)
  s->out2in.fib_index = nm->outside_fib_index;
  s->ext_host_addr = ip0->dst_address; // 183.2.172.17
  s->ext_host_port = vnet_buffer (b0)->ip.reass.l4_dst_port; // 0(ICMP)
  
  // 6. 添加到哈希表
  init_nat_i2o_kv (&kv0, s, thread_index, session_index);
  clib_bihash_add_or_overwrite_stale_8_8 (&nm->in2out, &kv0, ...);
  // 添加到 in2out 哈希表:
  // 键:10.0.0.2:0:FIB:ICMP
  // 值:会话索引
  
  init_nat_o2i_kv (&kv0, s, thread_index, session_index);
  clib_bihash_add_or_overwrite_stale_8_8 (&nm->out2in, &kv0, ...);
  // 添加到 out2in 哈希表:
  // 键:192.168.2.213:6000:FIB:ICMP
  // 值:会话索引
  
  // 7. 记录日志
  nat_ipfix_logging_nat44_ses_create (...);
  nat_syslog_nat44_apmadd (...);
  
  return next0;
}

代码解析

为什么要检查最大会话数?

  • 资源保护:防止会话数过多导致内存耗尽
  • 性能保护:过多的会话会影响查找性能
  • 配置限制:管理员可以配置最大会话数(实战中:sessions 10000),控制资源使用
  • 实战对应:如果超过10000个会话,新会话会被丢弃

为什么要先检查静态映射?

  • 优先级:静态映射优先级高于动态分配
  • 精确控制:静态映射允许管理员精确控制地址和端口映射
  • 服务器发布:静态映射通常用于服务器发布,需要优先处理
  • 实战对应:实战中没有配置静态映射,所以走动态分配路径

为什么要动态分配地址和端口?

  • 资源管理 :NAT地址池中的地址和端口是有限的,需要合理分配
    • 实战中:NAT地址池只有 192.168.2.213 一个地址
  • 端口复用:NAT44-EI支持端口复用,同一个外部端口可以用于多个连接(只要目标不同)
  • 负载均衡:可以跨多个NAT地址分配连接,实现负载均衡

为什么要管理用户?

  • 用户限制:可以限制每个用户的最大会话数(实战中:users 10000)
  • 资源统计:可以统计每个用户的资源使用情况
  • 会话管理:用户的会话可以统一管理,便于清理和统计
  • 实战对应:每个内部IP(如10.0.0.2)对应一个用户

为什么要添加到两个哈希表?

  • in2out哈希表 :用于IN2OUT方向查找(内部到外部)
    • 键:内部地址和端口(10.0.0.2:0)
    • 值:会话索引
  • out2in哈希表 :用于OUT2IN方向查找(外部到内部)
    • 键:外部地址和端口(192.168.2.213:6000)
    • 值:会话索引
  • 双向查找:支持双向通信,两个方向都需要快速查找
  • 实战对应
    • 请求包:通过 in2out 哈希表查找会话
    • 响应包:通过 out2in 哈希表查找会话

为什么要记录日志?

  • 审计:记录NAT转换事件,便于审计和排查问题
  • 监控:可以监控NAT的使用情况
  • 合规:某些场景下需要记录NAT转换日志,满足合规要求

会话创建结果

复制代码
会话信息:
  内部:10.0.0.2:0
  外部:192.168.2.213:6000
  目标:183.2.172.17:0
  协议:ICMP
3.3 执行NAT转换(快速路径)
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_in2out.c
// 函数:nat44_ei_in2out_node_fn_inline()

// 1. 标记数据包已NAT
b0->flags |= VNET_BUFFER_F_IS_NATED;
// 防止数据包被多次NAT转换

// 2. 修改源IP地址
old_addr0 = ip0->src_address.as_u32;        // 10.0.0.2
ip0->src_address = s0->out2in.addr;        // 192.168.2.213
new_addr0 = ip0->src_address.as_u32;
// 实战中:源IP从 10.0.0.2 转换为 192.168.2.213

// 3. 更新IP校验和
sum0 = ip0->checksum;
sum0 = ip_csum_update (sum0, old_addr0, new_addr0,
                      ip4_header_t, src_address);
ip0->checksum = ip_csum_fold (sum0);
// 修改IP地址后,必须更新校验和

// 4. 修改源端口(ICMP没有端口,但ICMP有ID字段)
if (proto0 == NAT_PROTOCOL_ICMP) {
  // ICMP处理:修改ICMP ID字段
  icmp0 = (icmp46_header_t *) (ip0 + 1);
  old_id = icmp0->id;
  icmp0->id = s0->out2in.port;  // 使用分配的端口作为ICMP ID
  new_id = icmp0->id;
  
  // 更新ICMP校验和
  sum0 = icmp0->checksum;
  sum0 = ip_csum_update (sum0, old_id, new_id,
                        icmp46_header_t, id);
  icmp0->checksum = ip_csum_fold (sum0);
}

// 5. 设置输出FIB索引
vnet_buffer (b0)->sw_if_index[VLIB_TX] = s0->out2in.fib_index;
// 指定数据包应该从哪个FIB表查找路由

// 6. 更新会话统计
nat44_ei_session_update_counters (s0, now, 
                                 vlib_buffer_length_in_chain (vm, b0),
                                 thread_index);
// 更新会话的流量和包数统计

// 7. 更新LRU列表
nat44_ei_session_update_lru (nm, s0, thread_index);
// 更新LRU列表,用于会话回收

代码解析

为什么要标记数据包已NAT?

  • 避免重复转换:防止数据包被多次NAT转换
  • 调试和追踪:可以通过标记识别哪些数据包经过了NAT
  • 其他功能识别:其他VPP功能可以根据这个标记做相应处理

为什么要修改源IP地址?

  • NAT核心功能 :将内部私有IP地址转换为公网IP地址
    • 实战中:10.0.0.2(私有地址)→ 192.168.2.213(公网地址)
  • 路由可达:公网IP地址可以在互联网上路由
  • 地址隐藏:隐藏内部网络结构,提高安全性

为什么要更新IP校验和?

  • 校验和验证:IP协议要求校验和必须正确,否则数据包会被丢弃
  • 增量更新:修改IP地址后,校验和必须重新计算
  • 性能优化:使用增量更新算法,只更新变化的部分,而不是重新计算整个校验和

为什么要修改ICMP ID字段?

  • ICMP特殊性:ICMP没有端口,但ICMP有ID字段用于标识不同的ICMP会话
  • 会话区分:通过不同的ID区分不同的ICMP会话
  • 端口映射:将ICMP ID映射到外部端口
  • 实战对应:ICMP ID从内部ID转换为外部ID(6000)

为什么要设置输出FIB索引?

  • 路由选择:指定数据包应该从哪个FIB表查找路由
  • 多VRF支持:支持多VRF场景,不同VRF使用不同的路由表
  • 正确转发:确保数据包从正确的接口发送出去
  • 实战对应:设置外部FIB索引,确保数据包从eno1np0发送

为什么要更新会话统计?

  • 监控:统计每个会话的流量和包数
  • 计费:可以用于流量计费
  • 故障排查:可以用于排查网络问题
  • 会话超时:根据最后活动时间判断会话是否超时

为什么要更新LRU列表?

  • 会话回收:LRU(Least Recently Used)算法用于回收最久未使用的会话
  • 资源管理:当会话数达到上限时,回收最久未使用的会话
  • 内存优化:及时回收不活跃的会话,释放内存

转换结果

复制代码
转换前:
  源IP:10.0.0.2
  源端口:0(ICMP ID)
  目标IP:183.2.172.17
  目标端口:0

转换后:
  源IP:192.168.2.213  ← 已修改
  源端口:6000         ← 已修改(ICMP ID)
  目标IP:183.2.172.17
  目标端口:0
9.3.5 步骤4:IP路由转发(TTL减少)

数据包继续在 ip4-unicast arc 中处理,经过 ip4-lookup 节点查找路由,然后进入 ip4-rewrite 节点进行转发。

c 复制代码
// 文件:src/vnet/ip/ip4_forward.c
// 函数:ip4_ttl_and_checksum_check

static_always_inline void
ip4_ttl_and_checksum_check (vlib_buffer_t * b, ip4_header_t * ip, ...)
{
  ttl = ip->ttl;
  // 实战中:TTL = 64(Windows发送时的初始值)
  
  // 减少TTL
  ttl -= 1;
  ip->ttl = ttl;
  // 实战中:TTL = 64 → 63
  
  // 如果TTL<=0,发送ICMP Time Exceeded
  if (PREDICT_FALSE (ttl <= 0)) {
    icmp4_error_set_vnet_buffer (b, ICMP4_time_exceeded, ...);
  }
}

实战对应

  • 数据包进入VPP时:TTL = 64
  • VPP转发后:TTL = 63(减少1)
  • 这就是为什么测试1的TTL比测试2少1的原因
9.3.6 步骤5:响应数据包处理(OUT2IN)

当百度服务器响应时,数据包从 eno1np0(外部接口)进入,经过 nat44-ei-out2in 节点处理。

c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_out2in.c
// 函数:nat44_ei_out2in_node_fn()

// 1. 构建查找键(目标地址和端口)
init_nat_k (&kv0, ip0->dst_address,                    // 192.168.2.213
            vnet_buffer (b0)->ip.reass.l4_dst_port,    // 6000
            nm->outside_fib_index,                      // 外部FIB索引
            proto0);                                    // ICMP

// 2. 在 out2in 哈希表中查找会话
if (clib_bihash_search_8_8 (&nm->out2in, &kv0, &value0) == 0) {
  // 找到会话
  s0 = pool_elt_at_index (nm->per_thread_data[thread_index].sessions,
                         nat_value_get_session_index (&value0));
  
  // 3. 修改目标IP地址
  old_addr0 = ip0->dst_address.as_u32;        // 192.168.2.213
  ip0->dst_address = s0->in2out.addr;        // 10.0.0.2
  new_addr0 = ip0->dst_address.as_u32;
  // 实战中:目标IP从 192.168.2.213 转换为 10.0.0.2
  
  // 4. 更新IP校验和
  sum0 = ip0->checksum;
  sum0 = ip_csum_update (sum0, old_addr0, new_addr0,
                        ip4_header_t, dst_address);
  ip0->checksum = ip_csum_fold (sum0);
  
  // 5. 修改目标端口(ICMP ID)
  old_id = icmp0->id;  // 6000
  icmp0->id = s0->in2out.port;  // 0(原始ICMP ID)
  new_id = icmp0->id;
  
  // 6. 更新ICMP校验和
  sum0 = icmp0->checksum;
  sum0 = ip_csum_update (sum0, old_id, new_id,
                        icmp46_header_t, id);
  icmp0->checksum = ip_csum_fold (sum0);
  
  // 7. 设置输出FIB索引
  vnet_buffer (b0)->sw_if_index[VLIB_TX] = s0->in2out.fib_index;
  // 指定数据包应该从内部接口发送
  
  // 8. 更新会话统计
  nat44_ei_session_update_counters (s0, now, 
                                   vlib_buffer_length_in_chain (vm, b0),
                                   thread_index);
}

转换结果:
转换前:
  源IP:183.2.172.17
  源端口:0
  目标IP:192.168.2.213
  目标端口:6000

转换后:
  源IP:183.2.172.17
  源端口:0
  目标IP:10.0.0.2  ← 已修改
  目标端口:0       ← 已修改

代码解析

为什么要通过目标地址查找会话?

  • OUT2IN方向:响应数据包的目标地址是NAT外部地址,需要通过外部地址查找会话
  • 哈希表查找 :使用 out2in 哈希表,键是外部地址和端口
  • 实战对应 :目标地址 192.168.2.213:6000 匹配会话的外部地址和端口

为什么要修改目标IP和端口?

  • 还原地址 :将NAT外部地址还原为内部地址
    • 实战中:192.168.2.213 → 10.0.0.2
  • 端口还原 :将外部端口还原为内部端口
    • 实战中:6000 → 0(原始ICMP ID)

为什么要更新校验和?

  • 协议要求:IP和ICMP协议要求校验和必须正确
  • 数据完整性:确保数据在传输过程中没有被篡改
  • 性能优化:使用增量更新算法,只更新变化的部分

9.4 核心数据结构解析

9.4.1 nat44_ei_main_t(主结构)
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei.h

typedef struct {
  // 哈希表:内部到外部映射
  clib_bihash_8_8_t in2out;
  // 实战中:存储 10.0.0.2:0 → 会话索引 的映射
  
  // 哈希表:外部到内部映射
  clib_bihash_8_8_t out2in;
  // 实战中:存储 192.168.2.213:6000 → 会话索引 的映射
  
  // 地址池
  nat44_ei_address_t *addresses;
  // 实战中:包含 192.168.2.213(通过 nat44 ei add address 添加)
  
  // 接口列表
  nat44_ei_interface_t *interfaces;
  // 实战中:包含 eno2np1(inside)和 eno1np0(outside)
  
  // 静态映射
  nat44_ei_static_mapping_t *static_mappings;
  // 实战中:为空(没有配置静态映射)
  
  // 每线程数据
  nat44_ei_main_per_thread_data_t *per_thread_data;
  // 存储每个线程的会话池
  
  // 配置
  nat44_ei_config_t rconfig;
  // 实战中:sessions=10000, users=10000
  
  // 是否启用
  u8 enabled;
  // 实战中:1(已启用)
  
  // 外部FIB索引
  u32 outside_fib_index;
  // 外部接口的FIB索引
  
  // 外部FIB列表
  nat44_ei_outside_fib_t *outside_fibs;
  // 外部FIB列表
} nat44_ei_main_t;
9.4.2 nat44_ei_session_t(会话结构)
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei.h

typedef struct {
  // 外部网络元组(Outside)
  struct {
    ip4_address_t addr;      // 192.168.2.213
    u32 fib_index;           // 外部FIB索引
    u16 port;                // 6000
  } out2in;
  
  // 内部网络元组(Inside)
  struct {
    ip4_address_t addr;      // 10.0.0.2
    u32 fib_index;           // 内部FIB索引
    u16 port;                // 0(ICMP ID)
  } in2out;
  
  // 协议
  nat_protocol_t nat_proto;  // ICMP
  
  // 标志
  u32 flags;
  
  // 用户索引
  u32 user_index;
  
  // 最后活动时间
  f64 last_heard;
  
  // 统计信息
  u64 total_bytes;
  u32 total_pkts;
} nat44_ei_session_t;

实战对应

  • in2out.addr = 10.0.0.2(内部地址)
  • in2out.port = 0(ICMP ID)
  • out2in.addr = 192.168.2.213(外部地址)
  • out2in.port = 6000(外部端口)

9.5 哈希键构建函数解析

9.5.1 init_nat_k函数
c 复制代码
// 文件:src/plugins/nat/nat44-ei/nat44_ei_inlines.h:56

always_inline void
init_nat_k (clib_bihash_kv_8_8_t *kv, ip4_address_t addr, u16 port,
            u32 fib_index, nat_protocol_t proto)
{
  kv->key = calc_nat_key (addr, port, fib_index, proto);
  kv->value = ~0ULL;
}

always_inline u64
calc_nat_key (ip4_address_t addr, u16 port, u32 fib_index, u8 proto)
{
  return (u64) addr.as_u32 << 32 | (u64) port << 16 | fib_index << 3 |
         (proto & 0x7);
}

哈希键结构

复制代码
64位键:
  [63:32]  IP地址(32位)      ← 10.0.0.2 或 192.168.2.213
  [31:16]  端口(16位)        ← 0 或 6000
  [15:3]   FIB索引(13位)     ← FIB索引
  [2:0]    协议(3位)         ← ICMP

实战对应

  • IN2OUT查找键:10.0.0.2:0:FIB:ICMP
  • OUT2IN查找键:192.168.2.213:6000:FIB:ICMP

代码解析

为什么要使用64位键?

  • 性能优化:64位键可以在一次操作中完成比较,性能高
  • 内存对齐:64位对齐,提高缓存效率
  • 哈希效率:64位键的哈希计算效率高

为什么要这样分配位域?

  • IP地址(32位):IPv4地址是32位,占高32位
  • 端口(16位):端口号是16位,占中间16位
  • FIB索引(13位):FIB索引通常不会超过8192,13位足够
  • 协议(3位):协议类型只有几种(TCP、UDP、ICMP等),3位足够

为什么端点无关不包含目标地址和端口?

  • 端口复用:同一个内部IP和端口,访问不同目标时使用相同的外部端口
  • 资源节约:减少哈希键的大小,提高查找性能
  • NAT44-EI特性:这是NAT44-EI的核心特性,区别于NAT44-ED
  • 实战对应:Windows主机访问不同目标时,都使用相同的外部端口

9.6 完整数据包处理流程总结

结合实战场景,完整的数据包处理流程如下:

复制代码
【请求数据包:Windows 10.0.0.2 → 百度 183.2.172.17】

1. 数据包进入VPP(eno2np1)
   - 源IP:10.0.0.2
   - 目标IP:183.2.172.17
   - TTL:64

2. 进入 ip4-unicast Feature Arc
   - ACL检查
   - 分片重组
   - nat44-ei-classify(分类节点)
     - 检查目标地址是否是NAT地址池:183.2.172.17 != 192.168.2.213 → IN2OUT
     - 发送到 nat44-ei-in2out 节点

3. nat44-ei-in2out 节点处理
   - 构建查找键:10.0.0.2:0:FIB:ICMP
   - 查找会话:未找到(第一次ping)
   - 慢路径创建会话:
     - 分配外部地址:192.168.2.213
     - 分配外部端口:6000
     - 创建会话并添加到哈希表
   - 执行转换:
     - 源IP:10.0.0.2 → 192.168.2.213
     - ICMP ID:0 → 6000
     - 更新校验和
   - TTL:64(不变)

4. IP路由查找(ip4-lookup)
   - 查找路由:0.0.0.0/0 via 192.168.2.1
   - 确定输出接口:eno1np0

5. IP转发处理(ip4-rewrite)
   - TTL:64 → 63 ← 这里减少1!
   - 更新校验和

6. 数据包发出(eno1np0)
   - 源IP:192.168.2.213
   - 目标IP:183.2.172.17
   - TTL:63

【响应数据包:百度 183.2.172.17 → Windows 10.0.0.2】

1. 数据包进入VPP(eno1np0)
   - 源IP:183.2.172.17
   - 目标IP:192.168.2.213
   - TTL:53(经过多跳路由后)

2. 进入 ip4-unicast Feature Arc
   - nat44-ei-classify(分类节点)
     - 检查目标地址是否是NAT地址池:192.168.2.213 == 192.168.2.213 → OUT2IN
     - 发送到 nat44-ei-out2in 节点

3. nat44-ei-out2in 节点处理
   - 构建查找键:192.168.2.213:6000:FIB:ICMP
   - 查找会话:找到(通过 out2in 哈希表)
   - 执行转换:
     - 目标IP:192.168.2.213 → 10.0.0.2
     - ICMP ID:6000 → 0
     - 更新校验和
   - TTL:53(不变)

4. IP路由查找(ip4-lookup)
   - 查找路由:10.0.0.0/24 via 10.0.0.1
   - 确定输出接口:eno2np1

5. 数据包发出(eno2np1)
   - 源IP:183.2.172.17
   - 目标IP:10.0.0.2
   - TTL:53

6. Windows主机收到响应
   - ping成功!

9.7 关键代码函数总结

函数名 文件位置 功能 实战对应
nat44_ei_add_interface nat44_ei.c:632 添加接口并标记方向 set int nat44 ei in/out
nat44_ei_classify_inline_fn nat44_ei.c:3364 判断数据包方向 分类节点
nat44_ei_in2out_node_fn_inline nat44_ei_in2out.c:1272 IN2OUT转换 内部到外部转换
slow_path nat44_ei_in2out.c:366 创建新会话 第一次ping时创建会话
nat44_ei_out2in_node_fn nat44_ei_out2in.c OUT2IN转换 外部到内部转换
init_nat_k nat44_ei_inlines.h:56 构建哈希键 4元组键构建
ip4_ttl_and_checksum_check ip4_forward.c 减少TTL TTL减少1

9.8 NAT44-EI核心特性总结

  1. 端点无关(Endpoint Independent)

    • 使用4元组(源IP、源端口、FIB、协议)作为哈希键
    • 不包含目标地址和端口
    • 同一个内部IP和端口,访问不同目标时使用相同的外部端口
    • 实战体现:Windows主机访问不同目标时,都使用相同的外部端口
  2. 快速路径/慢路径

    • 已存在会话 → 快速路径(直接转换)
    • 新会话 → 慢路径(创建会话)
    • 实战体现:第一次ping走慢路径,后续ping走快速路径
  3. 接口配置

    • 通过接口配置识别内部和外部网络
    • 数据包通过接收接口判断方向
    • 实战体现:eno2np1是inside,eno1np0是outside
  4. 会话管理

    • 双向哈希表(in2out 和 out2in)
    • 每线程会话池
    • LRU列表管理
    • 实战体现:会话存储在双向哈希表中,支持快速查找
  5. TTL处理

    • NAT转换不改变TTL
    • IP转发时减少TTL
    • 实战体现:TTL在IP转发节点减少,不在NAT转换节点减少

希望这篇实战指南和源码解析帮助你深入理解VPP NAT44-EI的实现! 🚀

相关推荐
车载测试工程师2 小时前
CAPL学习-ETH功能函数-对象类
网络·tcp/ip·以太网·capl·canoe
珠海西格电力2 小时前
零碳园区数字感知基础架构规划:IoT 设备布点与传输管网衔接设计
大数据·运维·人工智能·物联网·智慧城市·能源
卓码软件测评2 小时前
CNAS软件测试机构:【Postman集合从接口组织到自动化测试套件的过程】
网络·测试工具·性能优化·测试用例·压力测试·postman
能不能别报错3 小时前
k8s的CICD流水线环境搭建实验(containerd版)
云原生·容器·kubernetes
JavaEdge.3 小时前
零距离拆解银行司库系统(TMS)的微服务设计与实践
微服务·云原生·架构
阿巴~阿巴~3 小时前
HTTP服务器实现请求解析与响应构建:从基础架构到动态交互
服务器·网络·网络协议·http·交互·请求解析·响应构建
黛琳ghz3 小时前
极速云原生:openEuler之Redis与Nginx部署性能实战
redis·nginx·云原生·操作系统·压力测试·openeuler·服务器部署
翼龙云_cloud3 小时前
腾讯云渠道商:腾讯云轻量服务器和CVM有什么差异?
运维·服务器·云计算·php·腾讯云
qq_296544653 小时前
驱动精灵、驱动人生、NVIDIA专业显卡驱动、360驱动大师、联想乐驱动,电脑驱动修复工具大全
网络·电脑·负载均衡