前言
本文详细介绍如何在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递减
- 数据流向 :
- Windows (10.0.0.2) → 网关 (10.0.0.1) → 虚拟机 (eno2np1)
- VPP NAT44-EI:10.0.0.2 → 192.168.2.213(地址转换)
- 虚拟机 (eno1np0) → WiFi网关 (192.168.2.1) → 互联网
- 返回路径相反
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?
原因:
-
VPP作为路由器:
- 当数据包从eno2np1进入,从eno1np0发出时
- VPP实际上是在转发数据包(路由转发)
- 根据IP协议规范,路由器转发数据包时必须减少TTL
-
IP转发机制:
VPP的IP转发流程: 1. 数据包进入VPP(eno2np1) 2. NAT44-EI处理(地址转换) 3. IP路由查找(ip4-lookup节点) 4. 转发数据包(减少TTL)← 这里! 5. 数据包发出(eno1np0) -
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不通外部网络
可能原因:
- 路由配置错误:检查默认路由是否正确
- 接口未启用:确认eno1np0处于up状态
- 网关不可达:ping网关192.168.2.1测试连通性
7.2 NAT转换不工作
可能原因:
- 接口方向配置错误:确认eno2np1是inside,eno1np0是outside
- NAT地址未添加:确认已添加192.168.2.213到地址池
- 会话限制:检查是否达到最大会话数限制
7.3 Windows无法访问外网
可能原因:
- 网关配置错误:Windows的VMnet网卡网关应设置为10.0.0.1
- 路由表问题:检查Windows路由表,确认10.0.0.0/24网段的路由
- 防火墙阻止:检查Windows防火墙是否阻止了ICMP
八、总结
8.1 配置要点回顾
-
网络拓扑:
- 桥接模式:连接外部网络(192.168.2.0/24)
- 主机模式:创建内部网络(10.0.0.0/24)
-
VPP配置:
- 外部接口:eno1np0(192.168.2.213)
- 内部接口:eno2np1(10.0.0.1)
- NAT地址池:192.168.2.213
-
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 eno2np1 和 set 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-feature 是 IPv4 Shallow Virtual Reassembly(IPv4浅层虚拟重组)功能节点。
主要功能:
- 处理IP分片:处理被分片的IP数据包
- 提取L4信息:从第一个分片中提取L4层信息(源端口、目标端口、协议等)
- 存储L4信息 :将L4信息存储在
vnet_buffer的ip.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之后运行?
- L4信息提取:NAT需要访问L4端口信息来进行会话查找和转换
- 分片支持:对于分片数据包,只有第一个分片包含L4信息,后续分片需要通过重组上下文获取
- 统一接口 :
ip4-sv-reassembly-feature提供了统一的接口,让NAT可以访问L4信息,无论数据包是否分片 - 性能优化 :对于非分片数据包,
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)
- 避免重复:如果接口已存在,直接使用,避免重复创建
为什么要设置标志位?
- 方向识别 :通过标志位识别接口是内部还是外部
eno2np1→NAT44_EI_INTERFACE_FLAG_IS_INSIDEeno1np0→NAT44_EI_INTERFACE_FLAG_IS_OUTSIDE
- 快速判断:数据包进入时,可以通过接口标志快速判断方向
- 功能区分:内部接口用于IN2OUT转换,外部接口用于OUT2IN转换
为什么要注册Feature节点?
- 数据包处理 :当数据包从该接口进入时,会经过对应的NAT节点
- 从
eno2np1进入 → 经过nat44-ei-in2out节点 - 从
eno1np0进入 → 经过nat44-ei-out2in节点
- 从
- 按需处理:只有配置了NAT的接口才会处理NAT转换
- 性能优化:未配置的接口不经过NAT处理,提高性能
在整个NAT体系中的必要性:
- 配置管理:接口配置是NAT工作的基础,没有接口配置,NAT无法工作
- 方向识别:接口类型用于识别数据包方向,这是NAT正确工作的前提
- 性能优化:按需处理,只有配置的接口才处理NAT,提高性能
- 灵活性:可以灵活配置哪些接口需要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体系中的必要性:
- 性能优化:分类节点在数据包处理的最早阶段,快速判断方向,避免后续不必要的处理
- 正确性保证:准确的方向判断是NAT正确工作的前提,错误的方向会导致数据包无法正确转换
- 灵活性:支持多种场景(动态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一个地址
- 实战中:NAT地址池只有
- 端口复用: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核心特性总结
-
端点无关(Endpoint Independent)
- 使用4元组(源IP、源端口、FIB、协议)作为哈希键
- 不包含目标地址和端口
- 同一个内部IP和端口,访问不同目标时使用相同的外部端口
- 实战体现:Windows主机访问不同目标时,都使用相同的外部端口
-
快速路径/慢路径
- 已存在会话 → 快速路径(直接转换)
- 新会话 → 慢路径(创建会话)
- 实战体现:第一次ping走慢路径,后续ping走快速路径
-
接口配置
- 通过接口配置识别内部和外部网络
- 数据包通过接收接口判断方向
- 实战体现:eno2np1是inside,eno1np0是outside
-
会话管理
- 双向哈希表(in2out 和 out2in)
- 每线程会话池
- LRU列表管理
- 实战体现:会话存储在双向哈希表中,支持快速查找
-
TTL处理
- NAT转换不改变TTL
- IP转发时减少TTL
- 实战体现:TTL在IP转发节点减少,不在NAT转换节点减少
希望这篇实战指南和源码解析帮助你深入理解VPP NAT44-EI的实现! 🚀