目录
- 第一章:概述
- [第二章:核心主文件 - nat44_ei.c / nat44_ei.h](#第二章:核心主文件 - nat44_ei.c / nat44_ei.h)
- [第三章:内联函数库 - nat44_ei_inlines.h](#第三章:内联函数库 - nat44_ei_inlines.h)
- [第四章:内部到外部转换 - nat44_ei_in2out.c](#第四章:内部到外部转换 - nat44_ei_in2out.c)
- [第五章:外部到内部转换 - nat44_ei_out2in.c](#第五章:外部到内部转换 - nat44_ei_out2in.c)
- [第六章:多Worker Handoff - nat44_ei_handoff.c](#第六章:多Worker Handoff - nat44_ei_handoff.c)
- [第七章:高可用性支持 - nat44_ei_ha.c / nat44_ei_ha.h](#第七章:高可用性支持 - nat44_ei_ha.c / nat44_ei_ha.h)
- [第八章:DPO支持 - nat44_ei_dpo.c / nat44_ei_dpo.h](#第八章:DPO支持 - nat44_ei_dpo.c / nat44_ei_dpo.h)
- [第九章:CLI命令处理 - nat44_ei_cli.c](#第九章:CLI命令处理 - nat44_ei_cli.c)
- [第十章:API处理 - nat44_ei_api.c](#第十章:API处理 - nat44_ei_api.c)
- [第十一章:API定义 - nat44_ei.api](#第十一章:API定义 - nat44_ei.api)
- 第十二章:文件依赖关系和调用关系
学习本章可能需要提前了解的文章
VPP中FIB(转发信息库)和VRF(虚拟路由转发)详解:从设计理念到实际应用
VPP API生成器详解:从API文件到多平台代码的完整流程
VPP中Frame Queue无锁MPSC队列详解:从原理到实现
第一章:概述
1.1 NAT44-EI 插件架构
NAT44-EI(Endpoint Independent NAT44)是VPP中实现端点无关NAT的核心插件。该插件采用模块化设计,通过多个文件协同工作,实现了高性能、可扩展的NAT功能。
1.2 文件组织结构
NAT44-EI插件包含以下文件:
核心文件:
nat44_ei.c/nat44_ei.h- 核心主文件,包含主要数据结构和初始化逻辑
功能模块:
nat44_ei_in2out.c- 内部到外部的NAT转换处理nat44_ei_out2in.c- 外部到内部的NAT转换处理nat44_ei_handoff.c- 多Worker线程之间的数据包handoff
高级特性:
nat44_ei_ha.c/nat44_ei_ha.h- 高可用性(High Availability)支持nat44_ei_dpo.c/nat44_ei_dpo.h- DPO(Data Plane Object)转发支持
接口层:
nat44_ei_cli.c- CLI命令处理nat44_ei_api.c- API消息处理nat44_ei.api- API定义文件
工具库:
nat44_ei_inlines.h- 内联函数和工具函数
1.3 文档说明
本文档按照文件组织,每个章节对应一个或一组相关文件。每个章节首先列出文件的概要和主要作用,后续会逐步展开详细内容。
第二章:核心主文件 - nat44_ei.c / nat44_ei.h
2.1 文件概要
文件位置:
src/plugins/nat/nat44-ei/nat44_ei.c(3514行)src/plugins/nat/nat44-ei/nat44_ei.h(692行)
主要作用:
这是NAT44-EI插件的核心文件,定义了整个插件的基础架构、数据结构和核心管理功能。nat44_ei.h 定义了所有数据结构、函数声明和常量,nat44_ei.c 实现了所有核心管理功能。
2.2 数据结构定义(nat44_ei.h)
2.2.1 核心数据结构
1. nat44_ei_main_t - 全局主结构体(第312-480行)
这是整个插件的核心数据结构,包含所有NAT状态和配置信息:
c
typedef struct nat44_ei_main_s {
// 数据库配置
u32 translations; // 每个线程的最大会话数
u32 translation_buckets; // 会话哈希表桶数
u32 user_buckets; // 用户哈希表桶数
// 功能标志
u8 out2in_dpo; // OUT2IN DPO模式标志
u8 forwarding_enabled; // 转发启用标志
u8 static_mapping_only; // 仅静态映射模式
u8 static_mapping_connection_tracking; // 静态映射连接跟踪
// MSS调整
u16 mss_clamping; // TCP MSS调整值
// 静态映射哈希表
clib_bihash_8_8_t static_mapping_by_local; // 按本地地址查找
clib_bihash_8_8_t static_mapping_by_external; // 按外部地址查找
nat44_ei_static_mapping_t *static_mappings; // 静态映射池
// 接口池
nat44_ei_interface_t *interfaces; // 普通接口池
nat44_ei_interface_t *output_feature_interfaces; // 输出特性接口池
// 限制配置
u32 max_users_per_thread; // 每线程最大用户数
u32 max_translations_per_thread; // 每线程最大会话数
u32 max_translations_per_user; // 每用户最大会话数
// VRF配置
u32 inside_vrf_id; // 内部VRF ID
u32 inside_fib_index; // 内部FIB索引
u32 outside_vrf_id; // 外部VRF ID
u32 outside_fib_index; // 外部FIB索引
// Worker线程配置
u32 num_workers; // Worker线程数
u32 first_worker_index; // 第一个Worker索引
u32 *workers; // Worker线程列表
u16 port_per_thread; // 每线程端口数
// 主查找表(全局,所有线程共享)
clib_bihash_8_8_t out2in; // OUT2IN方向会话表
clib_bihash_8_8_t in2out; // IN2OUT方向会话表
// 每线程数据
nat44_ei_main_per_thread_data_t *per_thread_data;
// 地址池
nat44_ei_address_t *addresses; // NAT地址池向量
// 地址和端口分配
nat44_ei_alloc_out_addr_and_port_function_t *alloc_addr_and_port;
nat44_ei_addr_and_port_alloc_alg_t addr_and_port_alloc_alg;
u8 psid_offset, psid_length; // MAP-E参数
u16 psid; // MAP-E PSID
u16 start_port, end_port; // 端口范围参数
// FIB管理
nat44_ei_fib_t *fibs; // FIB向量
nat44_ei_outside_fib_t *outside_fibs; // 外部FIB向量
// 自动地址添加
u32 *auto_add_sw_if_indices; // 自动添加地址的接口索引
// 静态映射解析
nat44_ei_static_map_resolve_t *to_resolve; // 待解析的静态映射
// 节点索引
u32 in2out_node_index;
u32 out2in_node_index;
u32 in2out_output_node_index;
// Frame Queue索引(用于Worker handoff)
u32 fq_in2out_index;
u32 fq_in2out_output_index;
u32 fq_out2in_index;
// 随机数种子(用于端口分配)
u32 random_seed;
// 超时配置
nat_timeouts_t timeouts;
// 统计计数器
vlib_simple_counter_main_t total_users;
vlib_simple_counter_main_t total_sessions;
vlib_simple_counter_main_t user_limit_reached;
// 详细统计计数器(fastpath/slowpath/in2out/out2in)
struct {
struct {
struct { foreach_nat_counter; } in2out;
struct { foreach_nat_counter; } out2in;
} fastpath;
struct {
struct { foreach_nat_counter; } in2out;
struct { foreach_nat_counter; } out2in;
} slowpath;
vlib_simple_counter_main_t hairpinning;
} counters;
// API和日志
u16 msg_id_base; // API消息ID基址
vlib_log_class_t log_class; // 日志类
u8 log_level; // 日志级别
// 便利指针
api_main_t *api_main;
ip4_main_t *ip4_main;
ip_lookup_main_t *ip4_lookup_main;
// FIB源
fib_source_t fib_src_hi; // 高优先级FIB源
fib_source_t fib_src_low; // 低优先级FIB源
// PAT标志
u8 pat; // 端口地址转换启用标志
// Frame Queue配置
u32 frame_queue_nelts; // Frame Queue元素数
// 插件状态
u8 enabled; // 插件启用标志
u32 hairpin_reg; // Hairpinning注册计数
// 运行配置
nat44_ei_config_t rconfig; // 运行时配置
// Hairpinning Frame Queue索引
u32 in2out_hairpinning_finish_ip4_lookup_node_fq_index;
u32 in2out_hairpinning_finish_interface_output_node_fq_index;
u32 hairpinning_fq_index;
vnet_main_t *vnet_main;
} nat44_ei_main_t;
全局实例:
c
extern nat44_ei_main_t nat44_ei_main; // 第482行,全局单例
2. nat44_ei_session_t - NAT会话结构体(第202-260行)
存储单个NAT会话的所有信息:
c
typedef CLIB_PACKED (struct {
// 外部网络元组(OUT2IN查找键)
struct {
ip4_address_t addr; // 外部IP地址
u32 fib_index; // FIB索引
u16 port; // 外部端口
} out2in;
// 内部网络元组(IN2OUT查找键)
struct {
ip4_address_t addr; // 内部IP地址
u32 fib_index; // FIB索引
u16 port; // 内部端口
} in2out;
nat_protocol_t nat_proto; // NAT协议类型(TCP/UDP/ICMP/OTHER)
u32 flags; // 会话标志位
// 每用户会话列表
u32 per_user_index; // 在用户会话列表中的索引
u32 per_user_list_head_index; // 用户会话列表头索引
// LRU列表
u32 lru_head_index; // LRU列表头索引
u32 lru_index; // 在LRU列表中的索引
f64 last_lru_update; // 最后LRU更新时间
// 时间戳
f64 last_heard; // 最后听到时间(用于超时)
f64 ha_last_refreshed; // HA最后刷新时间
// 统计信息
u64 total_bytes; // 总字节数
u32 total_pkts; // 总数据包数
// 外部主机信息(用于HA)
ip4_address_t ext_host_addr; // 外部主机IP
u16 ext_host_port; // 外部主机端口
ip4_address_t ext_host_nat_addr; // 外部主机NAT IP
u16 ext_host_nat_port; // 外部主机NAT端口
// TCP状态
u8 state; // TCP状态
u32 i2o_fin_seq; // IN2OUT FIN序列号
u32 o2i_fin_seq; // OUT2IN FIN序列号
u64 tcp_closed_timestamp; // TCP关闭时间戳
u32 user_index; // 所属用户索引
}) nat44_ei_session_t;
3. nat44_ei_user_t - NAT用户结构体(第262-268行)
管理每个内部IP地址的会话:
c
typedef CLIB_PACKED (struct {
ip4_address_t addr; // 用户IP地址
u32 fib_index; // FIB索引
u32 sessions_per_user_list_head_index; // 用户会话列表头索引
u32 nsessions; // 动态会话数
u32 nstaticsessions; // 静态会话数
}) nat44_ei_user_t;
4. nat44_ei_address_t - NAT地址池结构体(第70-77行)
管理外部IP地址和端口分配:
c
typedef struct {
ip4_address_t addr; // IP地址
u32 fib_index; // FIB索引(~0表示全局)
u32 busy_ports[NAT_N_PROTOCOLS]; // 每个协议已使用的端口数
u32 *busy_ports_per_thread[NAT_N_PROTOCOLS]; // 每线程端口使用数
uword *busy_port_bitmap[NAT_N_PROTOCOLS]; // 端口位图(65536位)
} nat44_ei_address_t;
5. nat44_ei_static_mapping_t - 静态映射结构体(第157-182行)
存储静态NAT映射配置:
c
typedef struct {
ip4_address_t pool_addr; // 首选池地址
ip4_address_t local_addr; // 本地IP地址
ip4_address_t external_addr; // 外部IP地址
u16 local_port; // 本地端口
u16 external_port; // 外部端口
u32 vrf_id; // VRF ID
u32 fib_index; // FIB索引
nat_protocol_t proto; // 协议
u32 *workers; // Worker线程列表
u8 *tag; // 标签字符串
nat44_ei_lb_addr_port_t *locals; // 负载均衡后端列表
u32 flags; // 标志位
} nat44_ei_static_mapping_t;
6. nat44_ei_interface_t - 接口配置结构体(第184-188行)
存储接口的NAT配置:
c
typedef struct {
u32 sw_if_index; // 软件接口索引
u8 flags; // 标志位(INSIDE/OUTSIDE)
} nat44_ei_interface_t;
7. nat44_ei_main_per_thread_data_t - 每线程数据结构体(第270-298行)
每个线程的独立数据:
c
typedef struct {
clib_bihash_8_8_t user_hash; // 用户哈希表
nat44_ei_user_t *users; // 用户池
nat44_ei_session_t *sessions; // 会话池
dlist_elt_t *list_pool; // 双向链表元素池
dlist_elt_t *lru_pool; // LRU列表池
u32 tcp_trans_lru_head_index; // TCP传输LRU头
u32 tcp_estab_lru_head_index; // TCP建立LRU头
u32 udp_lru_head_index; // UDP LRU头
u32 icmp_lru_head_index; // ICMP LRU头
u32 unk_proto_lru_head_index; // 未知协议LRU头
u32 snat_thread_index; // NAT线程索引
u32 thread_index; // 真实线程索引
} nat44_ei_main_per_thread_data_t;
2.2.2 枚举和标志定义
1. 地址和端口分配算法枚举(第45-55行)
c
typedef enum {
NAT44_EI_ADDR_AND_PORT_ALLOC_ALG_DEFAULT = 0, // 默认算法
NAT44_EI_ADDR_AND_PORT_ALLOC_ALG_MAPE = 1, // MAP-E算法
NAT44_EI_ADDR_AND_PORT_ALLOC_ALG_RANGE = 2, // 端口范围算法
} nat44_ei_addr_and_port_alloc_alg_t;
2. 接口标志(第57-59行)
c
#define NAT44_EI_INTERFACE_FLAG_IS_INSIDE (1 << 0) // 内部接口
#define NAT44_EI_INTERFACE_FLAG_IS_OUTSIDE (1 << 1) // 外部接口
3. 会话标志(第61-63行)
c
#define NAT44_EI_SESSION_FLAG_STATIC_MAPPING (1 << 0) // 静态映射会话
#define NAT44_EI_SESSION_FLAG_UNKNOWN_PROTO (1 << 1) // 未知协议会话
4. 静态映射标志(第65-68行)
c
#define NAT44_EI_SM_FLAG_ADDR_ONLY (1 << 0) // 仅地址映射
#define NAT44_EI_SM_FLAG_IDENTITY_NAT (1 << 1) // 身份NAT
#define NAT44_EI_SM_FLAG_SWITCH_ADDRESS (1 << 2) // 切换地址(DHCP)
5. 配置结构体(第111-128行)
c
typedef struct {
u32 users; // 最大用户数
u32 sessions; // 最大会话数
u32 user_sessions; // 每用户最大会话数
u8 static_mapping_only; // 仅静态映射
u8 connection_tracking; // 连接跟踪
u8 out2in_dpo; // OUT2IN DPO模式
u32 inside_vrf; // 内部VRF ID
u32 outside_vrf; // 外部VRF ID
} nat44_ei_config_t;
2.3 Feature Arc注册(nat44_ei.c)
NAT44-EI在VPP的Feature Arc系统中注册了9个Feature节点,用于数据包处理的不同阶段:
2.3.1 注册的Feature节点
1. nat44-ei-classify - 数据包分类节点(第82-87行)
c
VNET_FEATURE_INIT (ip4_nat_classify, static) = {
.arc_name = "ip4-unicast", // IPv4单播转发弧
.node_name = "nat44-ei-classify", // 节点名称
.runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
"ip4-sv-reassembly-feature"),
};
作用:
- 在
ip4-unicast弧上运行 - 在ACL和分片重组之后运行
- 决定数据包应该走IN2OUT还是OUT2IN路径
- 检查目标IP是否是NAT地址池中的地址
- 检查静态映射
2. nat44-ei-handoff-classify - Handoff分类节点(第88-93行)
c
VNET_FEATURE_INIT (ip4_nat_handoff_classify, static) = {
.arc_name = "ip4-unicast",
.node_name = "nat44-ei-handoff-classify",
.runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
"ip4-sv-reassembly-feature"),
};
作用:
- 多Worker场景下使用的分类节点
- 功能与
nat44-ei-classify相同,但转发到handoff节点
3. nat44-ei-in2out - IN2OUT转换节点(第94-99行)
c
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"),
};
作用:
- 处理从内部到外部的NAT转换
- 修改源IP和源端口
- 创建新会话或查找现有会话
4. nat44-ei-out2in - OUT2IN转换节点(第100-106行)
c
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"),
};
作用:
- 处理从外部到内部的NAT转换
- 修改目标IP和目标端口
- 查找会话或静态映射
5. nat44-ei-in2out-output - 输出特性节点(第107-112行)
c
VNET_FEATURE_INIT (ip4_nat44_ei_in2out_output, static) = {
.arc_name = "ip4-output", // IPv4输出特性弧
.node_name = "nat44-ei-in2out-output",
.runs_after = VNET_FEATURES ("acl-plugin-out-ip4-fa",
"ip4-sv-reassembly-output-feature"),
};
作用:
- 在
ip4-output弧上运行 - 用于输出接口的NAT转换
- 处理从内部到外部的输出流量
6. nat44-ei-hairpinning - Hairpinning节点(第113-117行)
c
VNET_FEATURE_INIT (ip4_nat44_ei_hairpinning, static) = {
.arc_name = "ip4-local", // IPv4本地处理弧
.node_name = "nat44-ei-hairpinning",
.runs_before = VNET_FEATURES ("ip4-local-end-of-arc"),
};
作用:
- 在
ip4-local弧上运行 - 处理Hairpinning场景(内部通过外部地址访问内部)
- 在本地处理结束之前运行
7. nat44-ei-in2out-worker-handoff - IN2OUT Worker Handoff(第118-122行)
c
VNET_FEATURE_INIT (ip4_nat44_ei_in2out_worker_handoff, static) = {
.arc_name = "ip4-unicast",
.node_name = "nat44-ei-in2out-worker-handoff",
.runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa"),
};
作用:
- 多Worker场景下的IN2OUT handoff
- 将数据包handoff到正确的Worker线程
8. nat44-ei-out2in-worker-handoff - OUT2IN Worker Handoff(第123-128行)
c
VNET_FEATURE_INIT (ip4_nat44_ei_out2in_worker_handoff, static) = {
.arc_name = "ip4-unicast",
.node_name = "nat44-ei-out2in-worker-handoff",
.runs_after = VNET_FEATURES ("acl-plugin-in-ip4-fa",
"ip4-dhcp-client-detect"),
};
作用:
- 多Worker场景下的OUT2IN handoff
- 将数据包handoff到正确的Worker线程
9. nat44-ei-in2out-output-worker-handoff - 输出特性Worker Handoff(第129-134行)
c
VNET_FEATURE_INIT (ip4_nat44_ei_in2out_output_worker_handoff, static) = {
.arc_name = "ip4-output",
.node_name = "nat44-ei-in2out-output-worker-handoff",
.runs_after = VNET_FEATURES ("acl-plugin-out-ip4-fa",
"ip4-sv-reassembly-output-feature"),
};
作用:
- 多Worker场景下的输出特性handoff
- 在
ip4-output弧上运行
2.3.2 Feature Arc说明
ip4-unicast 弧:
- IPv4单播转发弧
- 所有IPv4单播数据包都会经过此弧
- NAT44-EI的主要处理弧
ip4-output 弧:
- IPv4输出特性弧
- 数据包从接口输出前经过此弧
- 用于输出接口的NAT处理
ip4-local 弧:
- IPv4本地处理弧
- 发往本地IP的数据包经过此弧
- 用于Hairpinning处理
2.4 插件初始化(nat44_ei_init)
函数位置: nat44_ei.c 第382-488行
函数签名:
c
clib_error_t *nat44_ei_init (vlib_main_t *vm)
初始化流程:
1. 初始化全局主结构体(第393行)
c
clib_memset (nm, 0, sizeof (*nm));
- 清零整个
nat44_ei_main结构体
2. 设置便利指针(第395-400行)
c
nm->vnet_main = vnet_get_main ();
nm->ip4_main = &ip4_main;
nm->api_main = vlibapi_get_main ();
nm->ip4_lookup_main = &ip4_main.lookup_main;
- 保存VPP主要子系统的指针,便于后续访问
3. 初始化Frame Queue索引(第402-405行)
c
nm->fq_out2in_index = ~0;
nm->fq_in2out_index = ~0;
nm->fq_in2out_output_index = ~0;
- 初始化为无效值(~0),后续在启用时初始化
4. 设置日志级别(第407行)
c
nm->log_level = NAT_LOG_ERROR;
- 默认日志级别为ERROR
5. 设置节点索引(第409行)
c
nat44_ei_set_node_indexes (nm, vm);
- 查找并保存关键节点的索引(in2out、out2in、in2out-output)
6. 注册日志类(第410行)
c
nm->log_class = vlib_log_register_class ("nat44-ei", 0);
- 注册日志类,用于日志输出
7. 初始化统计计数器(第412-431行)
c
nat_init_simple_counter (nm->total_users, "total-users", "/nat44-ei/total-users");
nat_init_simple_counter (nm->total_sessions, "total-sessions", "/nat44-ei/total-sessions");
nat_init_simple_counter (nm->user_limit_reached, "user-limit-reached", "/nat44-ei/user-limit-reached");
// ... 更多计数器
- 初始化所有统计计数器
- 包括fastpath、slowpath、in2out、out2in等各个方向的计数器
8. 配置Worker线程(第433-458行)
c
p = hash_get_mem (tm->thread_registrations_by_name, "workers");
if (p) {
tr = (vlib_thread_registration_t *) p[0];
if (tr) {
nm->num_workers = tr->count;
nm->first_worker_index = tr->first_index;
}
}
num_threads = tm->n_vlib_mains - 1;
nm->port_per_thread = 0xffff - 1024;
vec_validate (nm->per_thread_data, num_threads);
if (nm->num_workers > 1) {
for (i = 0; i < nm->num_workers; i++)
bitmap = clib_bitmap_set (bitmap, i, 1);
nat44_ei_set_workers (bitmap);
clib_bitmap_free (bitmap);
} else {
nm->per_thread_data[0].snat_thread_index = 0;
}
- 获取Worker线程信息
- 初始化每线程数据结构
- 如果有多Worker,默认使用所有Worker
9. 注册回调函数(第460-468行)
c
cbi.function = nat44_ei_ip4_add_del_interface_address_cb;
vec_add1 (nm->ip4_main->add_del_interface_address_callbacks, cbi);
cbi.function = nat44_ei_ip4_add_del_addr_only_sm_cb;
vec_add1 (nm->ip4_main->add_del_interface_address_callbacks, cbi);
cbt.function = nat44_ei_update_outside_fib;
vec_add1 (nm->ip4_main->table_bind_callbacks, cbt);
- 注册接口地址变化回调
- 注册FIB表绑定变化回调
10. 分配FIB源(第470-473行)
c
nm->fib_src_low = fib_source_allocate (
"nat44-ei-low", FIB_SOURCE_PRIORITY_LOW, FIB_SOURCE_BH_SIMPLE);
nm->fib_src_hi = fib_source_allocate ("nat44-ei-hi", FIB_SOURCE_PRIORITY_HI,
FIB_SOURCE_BH_SIMPLE);
- 分配两个FIB源,用于管理FIB路由条目
- 高优先级用于关键路由,低优先级用于普通路由
11. 初始化DPO和HA模块(第475-477行)
c
nat_dpo_module_init ();
nat_ha_init (vm, nm->num_workers, num_threads);
- 初始化DPO模块(用于OUT2IN DPO模式)
- 初始化HA模块(高可用性支持)
12. 初始化Hairpinning Frame Queue(第479-486行)
c
nm->hairpinning_fq_index =
vlib_frame_queue_main_init (nat44_ei_hairpinning_node.index, 0);
nm->in2out_hairpinning_finish_ip4_lookup_node_fq_index =
vlib_frame_queue_main_init (
nat44_ei_in2out_hairpinning_finish_ip4_lookup_node.index, 0);
nm->in2out_hairpinning_finish_interface_output_node_fq_index =
vlib_frame_queue_main_init (
nat44_ei_in2out_hairpinning_finish_interface_output_node.index, 0);
- 初始化Hairpinning相关的Frame Queue
13. 注册API(第487行)
c
return nat44_ei_api_hookup (vm);
- 注册所有API消息处理函数
注册宏:
c
VLIB_INIT_FUNCTION (nat44_ei_init); // 第490行
- 使用VPP的初始化宏,在VPP启动时自动调用
2.5 插件启用/禁用
2.5.1 插件启用(nat44_ei_plugin_enable)
函数位置: nat44_ei.c 第492-570行
函数签名:
c
int nat44_ei_plugin_enable (nat44_ei_config_t c)
启用流程:
1. 检查是否已启用(第497行)
c
fail_if_enabled ();
- 如果已启用,返回错误
2. 设置默认值(第499-506行)
c
if (!c.users)
c.users = 1024; // 默认1024用户
if (!c.sessions)
c.sessions = 10 * 1024; // 默认10K会话
if (!c.user_sessions)
c.user_sessions = c.sessions; // 默认等于总会话数
3. 保存配置(第508行)
c
nm->rconfig = c;
4. 设置Frame Queue大小(第510-511行)
c
if (!nm->frame_queue_nelts)
nm->frame_queue_nelts = NAT_FQ_NELTS_DEFAULT; // 默认64
5. 计算哈希表桶数(第513-515行)
c
nm->translations = c.sessions;
nm->translation_buckets = nat_calc_bihash_buckets (c.sessions);
nm->user_buckets = nat_calc_bihash_buckets (c.users);
- 根据会话数和用户数计算哈希表桶数
6. 设置PAT标志(第517-518行)
c
nm->pat = (!c.static_mapping_only ||
(c.static_mapping_only && c.connection_tracking));
- 如果不是仅静态映射模式,或者静态映射模式启用了连接跟踪,则启用PAT
7. 设置功能标志(第520-524行)
c
nm->static_mapping_only = c.static_mapping_only;
nm->static_mapping_connection_tracking = c.connection_tracking;
nm->out2in_dpo = c.out2in_dpo;
nm->forwarding_enabled = 0;
nm->mss_clamping = 0;
8. 设置限制(第526-528行)
c
nm->max_users_per_thread = c.users;
nm->max_translations_per_thread = c.sessions;
nm->max_translations_per_user = c.user_sessions;
9. 配置VRF(第530-536行)
c
nm->inside_vrf_id = c.inside_vrf;
nm->inside_fib_index = fib_table_find_or_create_and_lock (
FIB_PROTOCOL_IP4, c.inside_vrf, nm->fib_src_hi);
nm->outside_vrf_id = c.outside_vrf;
nm->outside_fib_index = fib_table_find_or_create_and_lock (
FIB_PROTOCOL_IP4, c.outside_vrf, nm->fib_src_hi);
- 查找或创建内部和外部VRF
- 锁定FIB表
10. 重置超时(第538行)
c
nat_reset_timeouts (&nm->timeouts);
- 重置所有协议的超时时间
11. 初始化数据库(第539-540行)
c
nat44_ei_db_init (nm->translations, nm->translation_buckets,
nm->user_buckets);
- 初始化会话表、用户表、静态映射表
12. 设置默认分配算法(第541行)
c
nat44_ei_set_alloc_default ();
- 设置地址和端口分配算法为默认算法
13. 清零统计计数器(第543-545行)
c
vlib_zero_simple_counter (&nm->total_users, 0);
vlib_zero_simple_counter (&nm->total_sessions, 0);
vlib_zero_simple_counter (&nm->user_limit_reached, 0);
14. 初始化Frame Queue(多Worker场景)(第547-564行)
c
if (nm->num_workers > 1) {
if (nm->fq_in2out_index == ~0) {
nm->fq_in2out_index = vlib_frame_queue_main_init (
nm->in2out_node_index, nm->frame_queue_nelts);
}
if (nm->fq_out2in_index == ~0) {
nm->fq_out2in_index = vlib_frame_queue_main_init (
nm->out2in_node_index, nm->frame_queue_nelts);
}
if (nm->fq_in2out_output_index == ~0) {
nm->fq_in2out_output_index = vlib_frame_queue_main_init (
nm->in2out_output_node_index, nm->frame_queue_nelts);
}
}
- 如果有多Worker,初始化Frame Queue用于handoff
15. 启用HA(第566行)
c
nat_ha_enable ();
16. 设置启用标志(第567行)
c
nm->enabled = 1;
2.5.2 插件禁用(nat44_ei_plugin_disable)
函数位置: nat44_ei.c 第1267-1310行
函数签名:
c
int nat44_ei_plugin_disable ()
禁用流程:
1. 检查是否已禁用(第1273行)
c
fail_if_disabled ();
2. 禁用HA(第1275行)
c
nat_ha_disable ();
3. 删除所有静态映射(第1277-1279行)
c
rc = nat44_ei_del_static_mappings ();
if (rc)
error = VNET_API_ERROR_BUG;
4. 删除所有地址(第1281-1283行)
c
rc = nat44_ei_del_addresses ();
if (rc)
error = VNET_API_ERROR_BUG;
5. 删除所有接口(第1285-1287行)
c
rc = nat44_ei_del_interfaces ();
if (rc)
error = VNET_API_ERROR_BUG;
6. 删除所有输出特性接口(第1289-1291行)
c
rc = nat44_ei_del_output_interfaces ();
if (rc)
error = VNET_API_ERROR_BUG;
7. 释放数据库(第1293-1302行)
c
if (nm->pat) {
clib_bihash_free_8_8 (&nm->in2out);
clib_bihash_free_8_8 (&nm->out2in);
vec_foreach (tnm, nm->per_thread_data) {
nat44_ei_worker_db_free (tnm);
}
}
- 释放会话哈希表
- 释放每线程数据
8. 清零配置(第1304行)
c
clib_memset (&nm->rconfig, 0, sizeof (nm->rconfig));
9. 重置标志(第1306-1307行)
c
nm->forwarding_enabled = 0;
nm->enabled = 0;
2.6 接口管理
2.6.1 添加接口(nat44_ei_add_interface)
函数位置: nat44_ei.c 第631-770行
函数签名:
c
int nat44_ei_add_interface (u32 sw_if_index, u8 is_inside)
参数:
sw_if_index: 软件接口索引is_inside: 1=内部接口,0=外部接口
处理流程:
1. 检查插件是否启用(第642行)
c
fail_if_disabled ();
2. 检查OUT2IN DPO模式限制(第644-648行)
c
if (nm->out2in_dpo && !is_inside) {
nat44_ei_log_err ("error unsupported");
return VNET_API_ERROR_UNSUPPORTED;
}
- OUT2IN DPO模式不支持外部接口
3. 检查是否已在output_feature_interfaces中(第650-654行)
c
if (nat44_ei_get_interface (nm->output_feature_interfaces, sw_if_index)) {
nat44_ei_log_err ("error interface already configured");
return VNET_API_ERROR_VALUE_EXIST;
}
4. 检查接口是否已存在(第656-703行)
c
i = nat44_ei_get_interface (nm->interfaces, sw_if_index);
if (i) {
// 如果已经是相同类型,直接返回成功
if ((nat44_ei_interface_is_inside (i) && is_inside) ||
(nat44_ei_interface_is_outside (i) && !is_inside)) {
return 0;
}
// 如果是不同类型,需要切换Feature
// 选择Feature名称
if (nm->num_workers > 1) {
del_feature_name = !is_inside ? "nat44-ei-in2out-worker-handoff" :
"nat44-ei-out2in-worker-handoff";
feature_name = "nat44-ei-handoff-classify";
} else {
del_feature_name = !is_inside ? "nat44-ei-in2out" : "nat44-ei-out2in";
feature_name = "nat44-ei-classify";
}
// 启用分片重组
rv = ip4_sv_reass_enable_disable_with_refcnt (sw_if_index, 1);
// 禁用旧Feature
rv = vnet_feature_enable_disable ("ip4-unicast", del_feature_name,
sw_if_index, 0, 0, 0);
// 启用新Feature
rv = vnet_feature_enable_disable ("ip4-unicast", feature_name,
sw_if_index, 1, 0, 0);
// 如果是外部接口,禁用Hairpinning
if (!is_inside) {
rv = nat44_ei_hairpinning_enable (0);
}
}
5. 创建新接口(第704-740行)
c
else {
// 选择Feature名称
if (nm->num_workers > 1) {
feature_name = is_inside ? "nat44-ei-in2out-worker-handoff" :
"nat44-ei-out2in-worker-handoff";
} else {
feature_name = is_inside ? "nat44-ei-in2out" : "nat44-ei-out2in";
}
// 验证接口计数器
nat_validate_interface_counters (nm, sw_if_index);
// 启用分片重组
rv = ip4_sv_reass_enable_disable_with_refcnt (sw_if_index, 1);
// 启用Feature
rv = vnet_feature_enable_disable ("ip4-unicast", feature_name,
sw_if_index, 1, 0, 0);
// 如果是内部接口且未启用OUT2IN DPO,启用Hairpinning
if (is_inside && !nm->out2in_dpo) {
rv = nat44_ei_hairpinning_enable (1);
}
// 创建接口结构
pool_get (nm->interfaces, i);
i->sw_if_index = sw_if_index;
i->flags = 0;
}
6. 获取FIB索引(第742-743行)
c
fib_index = fib_table_get_index_for_sw_if_index (FIB_PROTOCOL_IP4, sw_if_index);
7. 设置接口标志和外部FIB(第745-763行)
c
if (!is_inside) {
i->flags |= NAT44_EI_INTERFACE_FLAG_IS_OUTSIDE;
// 添加到外部FIB列表
outside_fib = nat44_ei_get_outside_fib (nm->outside_fibs, fib_index);
if (outside_fib) {
outside_fib->refcount++;
} else {
vec_add2 (nm->outside_fibs, outside_fib, 1);
outside_fib->fib_index = fib_index;
outside_fib->refcount = 1;
}
// 添加NAT地址到FIB
nat44_ei_add_del_addr_to_fib_foreach_addr (sw_if_index, 1);
nat44_ei_add_del_addr_to_fib_foreach_addr_only_sm (sw_if_index, 1);
} else {
i->flags |= NAT44_EI_INTERFACE_FLAG_IS_INSIDE;
}
2.6.2 删除接口(nat44_ei_del_interface)
函数位置: nat44_ei.c 第772-898行
处理流程类似添加接口,但执行相反操作:
- 禁用Feature节点
- 禁用分片重组
- 从FIB删除路由
- 更新外部FIB引用计数
- 释放接口结构
2.7 地址池管理
2.7.1 添加地址(nat44_ei_add_address)
函数位置: nat44_ei.c 第2991-3033行
函数签名:
c
int nat44_ei_add_address (ip4_address_t *addr, u32 vrf_id)
处理流程:
1. 检查是否已存在(第3000-3007行)
c
vec_foreach (ap, nm->addresses) {
if (ap->addr.as_u32 == addr->as_u32) {
nat44_ei_log_err ("address exist");
return VNET_API_ERROR_VALUE_EXIST;
}
}
2. 添加到地址向量(第3009行)
c
vec_add2 (nm->addresses, ap, 1);
3. 初始化地址结构(第3011-3028行)
c
ap->fib_index = ~0; // 默认全局
ap->addr = *addr;
if (vrf_id != ~0) {
ap->fib_index = fib_table_find_or_create_and_lock (
FIB_PROTOCOL_IP4, vrf_id, nm->fib_src_low);
}
// 初始化端口位图和计数器
nat_protocol_t proto;
for (proto = 0; proto < NAT_N_PROTOCOLS; ++proto) {
ap->busy_port_bitmap[proto] = 0;
ap->busy_ports[proto] = 0;
ap->busy_ports_per_thread[proto] = 0;
vec_validate_init_empty (ap->busy_ports_per_thread[proto],
tm->n_vlib_mains - 1, 0);
}
4. 添加到外部接口的FIB(第3030行)
c
nat44_ei_add_del_addr_to_fib_foreach_out_if (addr, 1);
2.7.2 删除地址(nat44_ei_del_address)
函数位置: nat44_ei.c 第3035-3122行
处理流程:
- 查找地址
- 删除使用该地址的会话
- 处理静态映射
- 从FIB删除路由
- 释放端口位图
- 从向量删除
2.8 静态映射管理
2.8.1 添加静态映射(nat44_ei_add_static_mapping)
函数位置: nat44_ei.c 第2347-2381行(外部接口),第2414-2569行(内部实现)
处理流程:
- 处理SWITCH_ADDRESS标志(DHCP场景)
- 调用内部函数添加映射
- 管理哈希表(by_local和by_external)
- 分配端口(如果不是地址映射)
- 设置Worker索引(多Worker场景)
2.8.2 删除静态映射(nat44_ei_del_static_mapping)
函数位置: nat44_ei.c 第2383-2412行(外部接口),第2571-2687行(内部实现)
处理流程:
- 处理SWITCH_ADDRESS标志
- 从哈希表删除
- 释放端口
- 删除相关会话
- 释放结构
2.9 会话管理
2.9.1 获取或创建用户(nat44_ei_user_get_or_create)
函数位置: nat44_ei.c 第1429-1486行
处理流程:
- 查找用户哈希表
- 如果不存在,创建新用户
- 初始化用户会话列表
- 检查用户限制
2.9.2 分配或回收会话(nat44_ei_session_alloc_or_recycle)
函数位置: nat44_ei.c 第1488-1558行
处理流程:
- 检查用户会话限制
- 如果超限,回收最旧的会话(LRU)
- 否则分配新会话
- 初始化会话结构
- 添加到用户会话列表
2.9.3 释放会话数据(nat44_ei_free_session_data)
函数位置: nat44_ei.c 第1560-1596行
处理流程:
- 从IN2OUT和OUT2IN哈希表删除
- 发送系统日志和IPFIX日志
- HA同步(如果启用)
- 释放端口(如果不是静态会话)
2.9.4 删除会话(nat44_ei_delete_session)
函数位置: nat44_ei.c 第2051-2081行
处理流程:
- 从用户会话列表删除
- 释放会话结构
- 更新用户会话计数
- 如果用户无会话,删除用户
2.10 地址和端口分配
2.10.1 默认分配算法(nat44_ei_alloc_default_cb)
函数位置: nat44_ei.c 第1834-1941行
算法特点:
- 根据源IP地址选择地址池中的地址
- 使用源IP地址的哈希值选择起始地址
- 随机选择端口(在每线程端口范围内)
- 检查端口是否已被使用
2.10.2 端口范围分配(nat44_ei_alloc_range_cb)
函数位置: nat44_ei.c 第1943-1978行
算法特点:
- 在指定的端口范围内随机选择
- 检查端口是否已被使用
2.10.3 MAP-E分配(nat44_ei_alloc_mape_cb)
函数位置: nat44_ei.c 第1980-2017行
算法特点:
- 根据MAP-E算法计算端口
- 使用PSID和偏移量
2.10.4 端口管理函数
检查端口是否使用(第188-192行):
c
always_inline bool
nat44_ei_port_is_used (nat44_ei_address_t *a, u8 proto, u16 port)
{
return clib_bitmap_get (a->busy_port_bitmap[proto], port);
}
分配端口(第194-200行):
c
always_inline void
nat44_ei_port_get (nat44_ei_address_t *a, u8 proto, u16 port)
{
ASSERT (!nat44_ei_port_is_used (a, proto, port));
a->busy_port_bitmap[proto] =
clib_bitmap_set (a->busy_port_bitmap[proto], port, 1);
}
释放端口(第202-208行):
c
always_inline void
nat44_ei_port_put (nat44_ei_address_t *a, u8 proto, u16 port)
{
ASSERT (nat44_ei_port_is_used (a, proto, port));
a->busy_port_bitmap[proto] =
clib_bitmap_set (a->busy_port_bitmap[proto], port, 0);
}
2.11 Worker线程管理
2.11.1 设置Worker线程(nat44_ei_set_workers)
函数位置: nat44_ei.c 第256-280行
处理流程:
- 检查Worker数量
- 更新Worker列表
- 设置每线程的索引
- 计算每线程端口数
2.11.2 Worker索引计算
IN2OUT Worker索引(第1718-1735行):
c
u32 nat44_ei_get_in2out_worker_index (ip4_header_t *ip0, u32 rx_fib_index0,
u8 is_output)
{
// 根据源IP地址哈希选择Worker
hash = ip0->src_address.as_u32 + (ip0->src_address.as_u32 >> 8) +
(ip0->src_address.as_u32 >> 16) + (ip0->src_address.as_u32 >> 24);
if (PREDICT_TRUE (is_pow2 (_vec_len (nm->workers))))
next_worker_index += nm->workers[hash & (_vec_len (nm->workers) - 1)];
else
next_worker_index += nm->workers[hash % _vec_len (nm->workers)];
}
OUT2IN Worker索引(第1752-1832行):
- 根据目标端口选择Worker
- 考虑静态映射的Worker分配
根据端口获取线程索引(第1738-1749行):
c
u32 nat44_ei_get_thread_idx_by_port (u16 e_port)
{
// 根据外部端口计算线程索引
thread_idx = nm->first_worker_index +
nm->workers[(e_port - 1024) / nm->port_per_thread %
_vec_len (nm->workers)];
}
2.12 数据包分类节点(nat44-ei-classify)
函数位置: nat44_ei.c 第3364-3463行
节点实现: nat44_ei_classify_inline_fn
分类逻辑:
1. 检查目标IP是否是NAT地址池地址(第3404-3411行)
c
vec_foreach (ap, nm->addresses) {
if (ip0->dst_address.as_u32 == ap->addr.as_u32) {
next0 = NAT44_EI_CLASSIFY_NEXT_OUT2IN;
goto enqueue0;
}
}
2. 检查静态映射(第3413-3435行)
c
if (PREDICT_FALSE (pool_elts (nm->static_mappings))) {
// 先尝试仅IP地址匹配
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;
goto enqueue0;
}
// 再尝试完整匹配(IP+端口+协议)
init_nat_k (&kv0, ip0->dst_address,
vnet_buffer (b0)->ip.reass.l4_dst_port, 0,
ip_proto_to_nat_proto (ip0->protocol));
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;
}
}
3. 默认走IN2OUT路径(第3388行)
c
u32 next0 = NAT44_EI_CLASSIFY_NEXT_IN2OUT;
2.13 关键设计特点
- 全局单例模式 :使用
nat44_ei_main_t nat44_ei_main作为全局主结构,所有模块共享 - 多线程支持 :通过
per_thread_data数组管理每个线程的数据,避免锁竞争 - 哈希表优化 :使用
clib_bihash_8_8_t实现O(1)查找性能,支持多线程安全 - 端口位图管理:使用位图(bitmap)高效管理65536个端口的分配状态
- LRU机制:使用双向链表实现会话的LRU管理,自动回收最旧会话
- 向量化处理:使用VPP的向量化框架,批量处理数据包
- 预测优化 :使用
PREDICT_TRUE/PREDICT_FALSE提示编译器优化分支 - 引用计数:外部FIB使用引用计数,支持多个接口共享同一FIB
2.14 依赖关系
-
依赖的头文件:
nat44_ei_inlines.h- 内联函数和工具函数nat44_ei_ha.h- HA接口nat44_ei_dpo.h- DPO接口nat/lib/nat_inlines.h- NAT库函数nat/lib/nat_syslog.h- 系统日志nat/lib/ipfix_logging.h- IPFIX日志
-
被依赖:
- 所有其他NAT44-EI模块都依赖此文件
- CLI和API模块通过此文件访问核心功能
- 数据包处理模块通过此文件访问会话表和配置
2.15 深入理解:为什么NAT需要单独维护Worker线程?
2.15.1 问题背景
RSS(Receive Side Scaling)机制:
- 多队列网卡使用RSS将数据包分发到不同的接收队列
- RSS基于数据包的5元组(源IP、源端口、目标IP、目标端口、协议)进行哈希
- 相同5元组的数据包会被分配到同一个队列,由同一个Worker线程处理
NAT的挑战:
- NAT需要处理双向流量(IN2OUT和OUT2IN)
- 同一会话的两个方向的数据包,5元组完全不同
- 必须确保同一会话的所有数据包都在同一个Worker线程上处理
2.15.2 为什么RSS无法满足NAT需求?
场景示例:
假设有一个NAT会话:
- 内部主机:
10.0.0.2:5000→ NAT外部:192.168.2.213:6000→ 外部服务器:8.8.8.8:80
IN2OUT方向(内部→外部):
数据包1:源IP=10.0.0.2, 源端口=5000, 目标IP=8.8.8.8, 目标端口=80, 协议=TCP
RSS哈希值 = hash(10.0.0.2, 5000, 8.8.8.8, 80, TCP) = Hash_A
→ 可能分配到Worker 1
OUT2IN方向(外部→内部):
数据包2:源IP=8.8.8.8, 源端口=80, 目标IP=192.168.2.213, 目标端口=6000, 协议=TCP
RSS哈希值 = hash(8.8.8.8, 80, 192.168.2.213, 6000, TCP) = Hash_B
→ 可能分配到Worker 2(与Worker 1不同!)
问题:
Hash_A ≠ Hash_B,因为5元组完全不同- 两个数据包可能被分配到不同的Worker线程
- Worker 1有会话信息,但Worker 2没有
- Worker 2无法正确进行NAT转换
2.15.3 NAT的Worker选择策略
1. IN2OUT方向的Worker选择(第1718-1735行)
c
u32 nat44_ei_get_in2out_worker_index (ip4_header_t *ip0, u32 rx_fib_index0,
u8 is_output)
{
nat44_ei_main_t *nm = &nat44_ei_main;
u32 next_worker_index = 0;
u32 hash;
next_worker_index = nm->first_worker_index;
// 根据源IP地址计算哈希值
hash = ip0->src_address.as_u32 + (ip0->src_address.as_u32 >> 8) +
(ip0->src_address.as_u32 >> 16) + (ip0->src_address.as_u32 >> 24);
// 根据哈希值选择Worker
if (PREDICT_TRUE (is_pow2 (_vec_len (nm->workers))))
next_worker_index += nm->workers[hash & (_vec_len (nm->workers) - 1)];
else
next_worker_index += nm->workers[hash % _vec_len (nm->workers)];
return next_worker_index;
}
选择依据:
- 源IP地址:同一内部主机的所有连接都分配到同一个Worker
- 目的:确保同一用户的所有会话在同一Worker上,便于管理和限制
2. OUT2IN方向的Worker选择(第1752-1832行)
c
u32 nat44_ei_get_out2in_worker_index (vlib_buffer_t *b, ip4_header_t *ip0,
u32 rx_fib_index0, u8 is_output)
{
// ...
u16 port = vnet_buffer (b)->ip.reass.l4_dst_port;
// 根据外部端口选择Worker
next_worker_index = nat44_ei_get_thread_idx_by_port (
clib_net_to_host_u16 (port));
return next_worker_index;
}
选择依据:
- 目标端口:根据NAT分配的外部端口选择Worker
- 端口分配策略:每个Worker分配固定范围的端口
- 公式 :
worker_index = (port - 1024) / port_per_thread % num_workers
端口分配示例:
假设有2个Worker,port_per_thread = 32255
Worker 0: 端口 1024-33278
Worker 1: 端口 33279-64533
外部端口6000 → Worker 0
外部端口40000 → Worker 1
2.15.4 NAT Worker选择的优势
1. 会话一致性保证
- IN2OUT:根据源IP选择Worker,创建会话
- OUT2IN:根据目标端口选择Worker,找到会话
- 关键:端口分配时已经考虑了Worker分配,确保一致性
2. 用户隔离
- 同一内部IP的所有会话在同一Worker上
- 便于实现每用户会话限制
- 便于统计和管理
3. 负载均衡
- IN2OUT:根据源IP哈希,实现负载均衡
- OUT2IN:根据端口范围,实现负载均衡
4. 避免锁竞争
- 每个Worker有独立的会话表(per_thread_data)
- 全局哈希表(in2out/out2in)是只读的,多线程安全
- 会话创建和删除在单个Worker上进行,无需锁
2.15.5 与RSS的对比
| 特性 | RSS(网卡) | NAT Worker选择 |
|---|---|---|
| 哈希依据 | 5元组(源IP、源端口、目标IP、目标端口、协议) | IN2OUT:源IP OUT2IN:目标端口 |
| 双向一致性 | ❌ 无法保证(5元组不同) | ✅ 保证(端口分配时已考虑) |
| 会话查找 | ❌ 无法找到(不同Worker) | ✅ 可以找到(同一Worker) |
| 用户隔离 | ❌ 无 | ✅ 有(同一IP同一Worker) |
| 适用场景 | 普通转发 | NAT转换 |
2.15.6 代码实现细节
端口分配时的Worker考虑(第1860-1872行):
c
// 在分配端口时,已经考虑了Worker分配
portnum = (port_per_thread * snat_thread_index) +
nat_random_port (&nm->random_seed, 0, port_per_thread - 1) + 1024;
说明:
snat_thread_index:Worker在Worker列表中的索引(0, 1, 2...)port_per_thread:每个Worker分配的端口范围大小- 分配的端口范围:
[1024 + snat_thread_index * port_per_thread, 1024 + (snat_thread_index + 1) * port_per_thread)
OUT2IN查找时的Worker计算(第1738-1749行):
c
u32 nat44_ei_get_thread_idx_by_port (u16 e_port)
{
nat44_ei_main_t *nm = &nat44_ei_main;
u32 thread_idx = nm->num_workers;
if (nm->num_workers > 1) {
// 根据端口反推Worker索引
thread_idx = nm->first_worker_index +
nm->workers[(e_port - 1024) / nm->port_per_thread %
_vec_len (nm->workers)];
}
return thread_idx;
}
说明:
- 通过端口号反推Worker索引
- 公式:
worker_index = (port - 1024) / port_per_thread - 确保OUT2IN数据包能找到正确的Worker
2.15.7 总结
为什么NAT不能依赖RSS:
- 5元组不一致:NAT转换后,双向数据包的5元组完全不同
- 会话查找需求:OUT2IN方向需要根据外部IP:端口查找会话,RSS无法保证找到正确的Worker
- 状态一致性:NAT是有状态的,需要保证同一会话的所有数据包在同一Worker上处理
- 用户管理:需要按用户(内部IP)管理会话,RSS无法提供这种粒度
NAT的解决方案:
- IN2OUT:根据源IP哈希选择Worker,确保同一用户在同一Worker
- OUT2IN:根据目标端口选择Worker,端口分配时已考虑Worker分配
- 端口分配策略:每个Worker分配固定范围的端口,便于反向查找
- Frame Queue:使用Frame Queue实现跨Worker handoff,避免锁竞争
2.16 FIB和VRF在NAT中的使用
2.16.1 生活化理解:FIB和VRF是什么?
FIB(Forwarding Information Base)就像"地址簿"
FIB告诉系统:
- 当收到一个数据包时
- 根据目标IP地址
- 应该从哪个接口发送出去
- 下一跳是什么
VRF(Virtual Routing and Forwarding)就像"虚拟路由器"
VRF实现:
- 网络隔离:不同VRF的网络完全隔离
- 地址空间复用:不同VRF可以使用相同的IP地址
- 资源共享:一台物理设备运行多个VRF
详细讲解:
FIB和VRF的详细设计概念、核心机制、在VPP中的应用场景,以及与业界实现的对比,请参考专门的文章:
- 《VPP FIB和VRF详解:从设计理念到实际应用》
2.16.2 NAT为什么需要FIB?
问题:NAT不是只做地址转换吗?为什么需要路由表?
答案:NAT需要知道"这个数据包应该走哪个门"
生活化例子:
想象你是一个门卫,负责:
- 检查每个进出的人(数据包)
- 决定是否需要换证件(NAT转换)
- 决定应该走哪个门(路由决策)
场景1:内部员工要出去(IN2OUT)
员工A(10.0.0.2)要去找客户(8.8.8.8)
- 你查FIB表:8.8.8.8 → 应该走"外部门"
- 你确认:外部门需要换证件(NAT转换)
- 你给员工A换一个临时证件(192.168.2.213:6000)
- 员工A从外部门出去
场景2:客户要进来找员工(OUT2IN)
客户(8.8.8.8)拿着临时证件(192.168.2.213:6000)要进来
- 你查FIB表:192.168.2.213 → 应该走"外部门"
- 你确认:这是NAT地址,需要转换
- 你查记录:192.168.2.213:6000 → 10.0.0.2:5000
- 你给客户换回内部证件,让他从内部门进去
关键点:
- 没有FIB,NAT不知道数据包应该走哪个门
- FIB告诉NAT:这个目标地址应该从哪个接口出去
- NAT根据接口类型(内部/外部)决定是否需要转换
2.16.3 FIB在NAT中的主要使用场景
场景1:添加NAT地址到FIB
当添加NAT地址时,在FIB表中登记该地址,确保OUT2IN数据包能被正确识别。
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第2117-2144行):
c
void nat44_ei_add_del_addr_to_fib (ip4_address_t *addr, u8 p_len,
u32 sw_if_index, int is_add)
{
// 在FIB表中添加/删除NAT地址路由
fib_table_entry_update_one_path (
fib_index, &prefix, nm->fib_src_low,
FIB_ENTRY_FLAG_CONNECTED | FIB_ENTRY_FLAG_LOCAL,
DPO_PROTO_IP4, NULL, sw_if_index, ~0, 1, NULL, 0);
}
场景2:通过FIB查找判断是否需要NAT
快速路径中,通过FIB查找判断数据包的目标接口,决定是否需要NAT转换。
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第213-244行):
c
static_always_inline int
nat44_ei_not_translate_fast (vlib_node_runtime_t *node, u32 sw_if_index0,
ip4_header_t *ip0, u32 proto0, u32 rx_fib_index0)
{
// 查FIB表:这个目标地址应该走哪个门?
fei = fib_table_lookup (rx_fib_index0, &pfx);
// 检查:这个门是外部门吗?
if (sw_if_index是外部接口)
return 0; // 是外部门,需要转换
return 1; // 不是外部门,不转换
}
场景3:多VRF支持
当有多个外部FIB(多VRF场景)时,通过FIB查找确定应该用哪个FIB。
场景4:DPO模式
DPO模式下,FIB直接指向NAT DPO,实现快速转发。
详细实现:
FIB和VRF的详细实现原理、代码结构、多VRF场景处理等,请参考:
- 《VPP FIB和VRF详解:从设计理念到实际应用》
2.16.4 总结
FIB在NAT中的核心作用:
- 路由决策:告诉NAT数据包应该走哪个接口
- 接口识别:通过FIB查找确定接口类型(内部/外部),判断是否需要NAT
- 地址登记:将NAT地址登记到FIB,确保OUT2IN数据包能被正确识别
- 多VRF支持:使用FIB索引区分不同VRF中的会话
- 性能优化:DPO模式下,FIB直接指向NAT处理,避免Feature Arc开销
NAT对FIB的依赖:
- NAT不是简单的地址转换,还需要路由决策
- FIB提供了路由查找和接口识别的能力
- NAT利用FIB实现智能的路由和转换决策
- 没有FIB,NAT无法知道数据包应该走哪个门
2.17 深入理解:Hairpinning功能详解
2.17.1 生活化理解:什么是Hairpinning?
想象一下:公司内部的"内部快递"
假设你在一家大公司工作,公司有:
- 内部网络:员工都在10.0.0.0/24网段
- 外部网络:公司对外公布的地址是192.168.2.213
- 内部服务器:10.0.0.3:80(公司内部的文件服务器)
问题场景:
员工A(10.0.0.2)想访问文件服务器,但他只知道:
- 外部地址:192.168.2.213:8080(公司对外公布的地址)
- 他不知道内部地址:10.0.0.3:80
没有Hairpinning会发生什么?
员工A发送数据包:10.0.0.2 → 192.168.2.213:8080
↓
NAT转换源地址:192.168.2.213:6000 → 192.168.2.213:8080
↓
数据包被发送到外部网络(互联网)
↓
❌ 问题:数据包绕了一大圈,可能根本回不来!
有了Hairpinning会发生什么?
员工A发送数据包:10.0.0.2 → 192.168.2.213:8080
↓
NAT转换源地址:192.168.2.213:6000 → 192.168.2.213:8080
↓
NAT发现:目标地址是NAT地址(本地地址)
↓
Hairpinning处理:192.168.2.213:8080 → 10.0.0.3:80(查映射表)
↓
数据包直接发送到内部服务器(不走外部网络)
↓
✅ 成功:数据包"发夹式"地转回来,不走外部网络
为什么叫"Hairpinning"(发夹)?
- 形象比喻:数据包像发夹一样,从内部出发,经过NAT,又回到内部
- 不走弯路:数据包不会真正发送到外部网络,而是在NAT处"发夹"回来
- 英文"Hairpin":就是发夹的意思,形象地描述了数据包的路径
2.17.2 为什么需要Hairpinning?
生活化例子:公司内部的"统一入口"
场景1:统一访问地址
公司对外公布:
"访问文件服务器,请访问:192.168.2.213:8080"
问题:
- 外部用户:通过这个地址访问(正常,走外部网络)
- 内部员工:也想通过这个地址访问(问题来了!)
没有Hairpinning:
- 内部员工访问192.168.2.213:8080
- 数据包被发送到外部网络
- 可能绕一大圈,甚至回不来
- 用户体验差,网络效率低
有Hairpinning:
- 内部员工访问192.168.2.213:8080
- NAT发现是本地地址,直接转换回内部地址
- 数据包直接发送到内部服务器
- 用户体验好,网络效率高
场景2:VPN用户访问内部资源
场景:
- 员工通过VPN连接到公司网络
- VPN分配的外部地址:192.168.2.213
- 员工想访问公司内部服务器(10.0.0.100)
- 使用外部地址访问:192.168.2.213(通过静态映射)
没有Hairpinning:
- 数据包绕到外部网络再回来
- 延迟高,效率低
有Hairpinning:
- 数据包在NAT处直接转换
- 延迟低,效率高
场景3:负载均衡场景
场景:
- 多个内部服务器提供相同服务(10.0.0.10, 10.0.0.11, 10.0.0.12)
- 通过NAT外部地址统一对外提供服务(192.168.2.213:8080)
- 内部客户端也需要通过外部地址访问
- 需要Hairpinning支持
好处:
- 统一入口,简化配置
- 内部和外部用户使用相同的地址
- 负载均衡正常工作
2.17.3 Hairpinning的工作原理(生活化解释)
完整流程:就像快递公司的"内部转送"
步骤1:员工A发送请求
员工A(10.0.0.2:5000)想访问文件服务器
他只知道外部地址:192.168.2.213:8080
他发送数据包:10.0.0.2:5000 → 192.168.2.213:8080
步骤2:IN2OUT处理(NAT转换源地址)
数据包到达NAT(IN2OUT方向):
- NAT看到:这是从内部来的数据包
- NAT创建会话:10.0.0.2:5000 → 192.168.2.213:6000
- NAT转换源地址:192.168.2.213:6000
- 目标地址不变:192.168.2.213:8080
- 数据包继续转发
步骤3:路由查找(发现是本地地址)
IP路由查找(ip4-lookup节点):
- 查找192.168.2.213的路由
- 发现:这是本地地址(NAT地址)
- 转发到ip4-local弧(本地处理弧)
步骤4:Hairpinning处理(关键步骤)
Hairpinning节点处理:
1. 查找映射表:192.168.2.213:8080 → 谁?
- 查找静态映射:找到了!→ 10.0.0.3:80
- 或者查找活动会话:找到了!→ 10.0.0.3:80
2. 转换目标地址:
- 目标IP:192.168.2.213 → 10.0.0.3
- 目标端口:8080 → 80
- 更新校验和
3. 重新路由:
- 目标地址现在是内部地址
- 转发到ip4-lookup重新路由
步骤5:重新路由到内部服务器
IP路由查找(ip4-lookup节点):
- 查找10.0.0.3的路由
- 找到:内部接口路由
- 转发到内部接口
- 数据包发送到服务器B(10.0.0.3:80)
最终结果:
数据包路径:
10.0.0.2:5000 → [NAT转换源] → 192.168.2.213:6000
↓
[Hairpinning转换目标]
↓
10.0.0.3:80
数据包没有真正发送到外部网络,而是在NAT处"发夹"回来!
2.17.4 Hairpinning代码实现
1. Hairpinning节点注册(src/plugins/nat/nat44-ei/nat44_ei.c 第113-117行)
生活化理解:
就像在"本地处理部门"设置一个"内部转送窗口"
当有包裹(数据包)发往公司地址(NAT地址)时,
先送到这个窗口检查,看是否需要转送到内部部门
c
VNET_FEATURE_INIT (ip4_nat44_ei_hairpinning, static) = {
.arc_name = "ip4-local", // 在"本地处理部门"(ip4-local弧)上运行
.node_name = "nat44-ei-hairpinning",
.runs_before = VNET_FEATURES ("ip4-local-end-of-arc"), // 在本地处理结束之前运行
};
作用:
- 在
ip4-local弧上运行,处理发往本地IP(NAT地址)的数据包 - 在本地处理结束之前运行,确保能拦截到这些数据包
2. Hairpinning核心函数(src/plugins/nat/nat44-ei/nat44_ei.c 第1106-1228行)
生活化理解:
这是"内部转送窗口"的核心处理逻辑:
1. 查地址簿:这个外部地址对应哪个内部地址?
2. 检查工作台:这个包裹应该在哪个工作台处理?
3. 防重复:避免同一个包裹被反复处理
4. 改地址:把外部地址改成内部地址
c
static_always_inline int
nat44_ei_hairpinning (vlib_main_t *vm, vlib_node_runtime_t *node,
nat44_ei_main_t *nm, u32 thread_index, vlib_buffer_t *b0,
ip4_header_t *ip0, udp_header_t *udp0,
tcp_header_t *tcp0, u32 proto0, int do_trace,
u32 *required_thread_index)
{
nat44_ei_session_t *s0 = NULL;
clib_bihash_kv_8_8_t kv0, value0;
ip_csum_t sum0;
u32 new_dst_addr0 = 0, old_dst_addr0, si = ~0;
u16 new_dst_port0 = ~0, old_dst_port0;
int rv;
ip4_address_t sm0_addr;
u16 sm0_port;
u32 sm0_fib_index;
u32 old_sw_if_index = vnet_buffer (b0)->sw_if_index[VLIB_TX];
// 步骤1:先查静态映射表(就像查"固定地址簿")
if (!nat44_ei_static_mapping_match (
ip0->dst_address, udp0->dst_port, nm->outside_fib_index, proto0,
&sm0_addr, &sm0_port, &sm0_fib_index, 1 /* by external */, 0, 0)) {
// 找到了静态映射:外部地址 → 内部地址
new_dst_addr0 = sm0_addr.as_u32; // 新的目标IP(内部地址)
new_dst_port0 = sm0_port; // 新的目标端口(内部端口)
vnet_buffer (b0)->sw_if_index[VLIB_TX] = sm0_fib_index; // 新的FIB索引
}
// 步骤2:如果静态映射找不到,查活动会话表(就像查"临时地址簿")
else {
// 构造查找键:外部地址+端口+协议
init_nat_k (&kv0, ip0->dst_address, udp0->dst_port,
nm->outside_fib_index, proto0);
// 在out2in哈希表中查找
rv = clib_bihash_search_8_8 (&nm->out2in, &kv0, &value0);
if (rv) {
rv = 0;
goto trace; // 找不到,不处理
}
// 步骤2.1:检查是否需要handoff到其他Worker(多工作台场景)
if (thread_index != nat_value_get_thread_index (&value0)) {
*required_thread_index = nat_value_get_thread_index (&value0);
return 0; // 需要handoff到其他Worker
}
// 步骤2.2:获取会话信息
si = nat_value_get_session_index (&value0);
s0 = pool_elt_at_index (nm->per_thread_data[thread_index].sessions, si);
new_dst_addr0 = s0->in2out.addr.as_u32; // 内部IP(从会话中获取)
new_dst_port0 = s0->in2out.port; // 内部端口(从会话中获取)
vnet_buffer (b0)->sw_if_index[VLIB_TX] = s0->in2out.fib_index; // 内部FIB索引
}
// 步骤3:防循环检查(避免同一个包裹被反复处理)
old_dst_addr0 = ip0->dst_address.as_u32;
old_dst_port0 = tcp0->dst;
if (new_dst_addr0 == old_dst_addr0 && new_dst_port0 == old_dst_port0 &&
vnet_buffer (b0)->sw_if_index[VLIB_TX] == old_sw_if_index) {
return 0; // 没有变化,不处理(避免无限循环)
}
// 步骤4:执行转换(改地址标签)
if (new_dst_addr0) {
// 4.1:修改目标IP地址(外部地址 → 内部地址)
old_dst_addr0 = ip0->dst_address.as_u32;
ip0->dst_address.as_u32 = new_dst_addr0;
// 更新IP校验和(因为IP地址变了)
sum0 = ip0->checksum;
sum0 = ip_csum_update (sum0, old_dst_addr0, new_dst_addr0, ip4_header_t,
dst_address);
ip0->checksum = ip_csum_fold (sum0);
// 4.2:修改目标端口(外部端口 → 内部端口)
old_dst_port0 = tcp0->dst;
if (PREDICT_TRUE (new_dst_port0 != old_dst_port0)) {
if (PREDICT_TRUE (proto0 == NAT_PROTOCOL_TCP)) {
tcp0->dst = new_dst_port0;
// 更新TCP校验和(TCP校验和包含伪首部,需要更新IP地址和端口)
sum0 = tcp0->checksum;
sum0 = ip_csum_update (sum0, old_dst_addr0, new_dst_addr0,
ip4_header_t, dst_address);
sum0 = ip_csum_update (sum0, old_dst_port0, new_dst_port0,
ip4_header_t /* cheat */, length);
tcp0->checksum = ip_csum_fold (sum0);
} else {
// UDP处理
udp0->dst_port = new_dst_port0;
udp0->checksum = 0; // UDP校验和可选,设为0表示不校验
}
}
rv = 1; // 成功处理
}
return rv;
}
处理步骤总结:
-
查找映射:
- 优先查找静态映射(固定地址簿)
- 如果找不到,查找活动会话(临时地址簿)
-
Worker检查:
- 如果会话在其他Worker上,返回需要handoff
- 确保在正确的Worker上处理(多工作台场景)
-
防循环检查:
- 检查目标地址和端口是否有变化
- 如果没有变化,不处理(避免无限循环)
-
执行转换:
- 修改目标IP地址(外部地址 → 内部地址)
- 修改目标端口(外部端口 → 内部端口)
- 更新IP和TCP/UDP校验和
3. Hairpinning节点实现(src/plugins/nat/nat44-ei/nat44_ei.c 第2133-2227行)
生活化理解:
这是"内部转送窗口"的工作流程:
1. 接收包裹(数据包)
2. 调用处理函数检查是否需要转送
3. 如果需要转送到其他工作台,就转送
4. 如果转换成功,就重新贴地址标签,重新路由
5. 记录处理统计
c
VLIB_NODE_FN (nat44_ei_hairpinning_node)
(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
{
// ...
while (n_left_from > 0 && n_left_to_next > 0) {
// ...
// 调用hairpinning处理函数(检查是否需要转送)
if (nat44_ei_hairpinning (vm, node, nm, thread_index, b0, ip0, udp0,
tcp0, proto0, 1, &required_thread_index)) {
next0 = NAT44_EI_HAIRPIN_NEXT_LOOKUP; // 转换成功,需要重新查找路由
next0_resolved = 1;
}
// 如果需要handoff到其他Worker(转送到其他工作台)
if (thread_index != required_thread_index) {
vnet_buffer (b0)->snat.required_thread_index = required_thread_index;
next0 = NAT44_EI_HAIRPIN_NEXT_HANDOFF; // 转发到handoff节点
next0_resolved = 1;
}
// 如果处理成功,更新统计(记录处理了多少个包裹)
if (next0 != NAT44_EI_HAIRPIN_NEXT_DROP)
vlib_increment_simple_counter (
&nm->counters.hairpinning, vm->thread_index, sw_if_index0, 1);
}
// ...
}
节点行为:
- 处理发往本地IP的数据包
- 调用hairpinning函数进行转换
- 如果需要handoff,转发到handoff节点
- 如果转换成功,转发到ip4-lookup重新路由
2.17.5 Hairpinning的启用和禁用
生活化理解:
就像"内部转送窗口"的开关:
- 当有内部接口时,打开窗口(启用)
- 当没有内部接口时,关闭窗口(禁用)
- 使用引用计数,支持多个内部接口
启用Hairpinning(src/plugins/nat/nat44-ei/nat44_ei.c 第600-629行):
c
static_always_inline int
nat44_ei_hairpinning_enable (u8 is_enable)
{
nat44_ei_main_t *nm = &nat44_ei_main;
u32 sw_if_index = 0; // local0接口
if (is_enable) {
nm->hairpin_reg += 1; // 增加引用计数(多一个内部接口)
if (1 == nm->hairpin_reg) {
// 第一次启用,注册Feature(打开"内部转送窗口")
return vnet_feature_enable_disable (
"ip4-local", "nat44-ei-hairpinning", sw_if_index, is_enable, 0, 0);
}
} else {
if (0 == nm->hairpin_reg)
return 1;
nm->hairpin_reg -= 1; // 减少引用计数(少一个内部接口)
if (0 == nm->hairpin_reg) {
// 最后一次禁用,取消注册Feature(关闭"内部转送窗口")
return vnet_feature_enable_disable (
"ip4-local", "nat44-ei-hairpinning", sw_if_index, is_enable, 0, 0);
}
}
return 0;
}
启用时机:
- 添加内部接口时启用(第730行)
- 使用引用计数管理,支持多个内部接口
禁用时机:
- 删除内部接口时禁用(第868行)
- 当引用计数为0时,真正禁用Feature
2.17.6 完整示例:数据包如何通过Hairpinning
场景:内部主机A(10.0.0.2)通过外部地址访问内部服务器B(10.0.0.3)
前提条件:
- 服务器B有静态映射:10.0.0.3:80 → 192.168.2.213:8080
- 主机A只知道外部地址:192.168.2.213:8080
步骤1:IN2OUT方向(内部 → 外部)
1. 主机A发送数据包:
[10.0.0.2:5000] → [192.168.2.213:8080] TCP
2. IN2OUT处理(nat44-ei-in2out节点):
- 查找会话:未找到
- 创建新会话:10.0.0.2:5000 → 192.168.2.213:6000
- 修改源:192.168.2.213:6000
- 目标不变:192.168.2.213:8080
- 转发到ip4-lookup
3. IP路由查找(ip4-lookup节点):
- 查找192.168.2.213的路由
- 找到:本地路由(NAT地址)
- 转发到ip4-local弧
4. Hairpinning处理(nat44-ei-hairpinning节点):
- 查找静态映射:192.168.2.213:8080 → 10.0.0.3:80
- 修改目标:10.0.0.3:80
- 更新校验和
- 转发到ip4-lookup(重新路由)
5. IP路由查找(ip4-lookup节点):
- 查找10.0.0.3的路由
- 找到:内部接口路由
- 转发到内部接口
6. 数据包发送到服务器B:
[192.168.2.213:6000] → [10.0.0.3:80] TCP
步骤2:OUT2IN方向(外部 → 内部,返回数据包)
1. 服务器B发送回复:
[10.0.0.3:80] → [192.168.2.213:6000] TCP
2. OUT2IN处理(nat44-ei-out2in节点):
- 查找会话:192.168.2.213:6000 → 10.0.0.2:5000
- 修改目标:10.0.0.2:5000
- 转发到ip4-lookup
3. IP路由查找:
- 查找10.0.0.2的路由
- 找到:内部接口路由
- 转发到内部接口
4. 数据包发送到主机A:
[10.0.0.3:80] → [10.0.0.2:5000] TCP
关键点:
- 数据包没有真正发送到外部网络
- 在NAT处"发夹"回来,直接发送到内部服务器
- 用户体验好,网络效率高
2.17.7 Hairpinning的关键特性
1. 支持静态映射和动态会话
- 静态映射优先:先查找静态映射(固定地址簿)
- 动态会话支持:也支持动态创建的会话(临时地址簿)
2. 多Worker支持
- Worker handoff:如果会话在其他Worker上,handoff到正确的Worker
- 线程安全:确保在正确的Worker上处理
3. 防循环机制
- 变化检查:检查目标地址和端口是否有变化
- 避免无限循环:如果没有变化,不处理,避免在ip4-local和ip4-lookup之间循环
4. 校验和更新
- IP校验和:更新IP头校验和
- TCP/UDP校验和:更新传输层校验和
- 伪首部:TCP校验和包含伪首部(源IP、目标IP)
2.17.8 Hairpinning的统计和监控
统计计数器(src/plugins/nat/nat44-ei/nat44_ei.c 第430-431行):
c
nat_init_simple_counter (nm->counters.hairpinning, "hairpinning",
"/nat44-ei/hairpinning");
查看统计:
bash
vppctl# show nat44 ei statistics
统计作用:
- 监控Hairpinning流量
- 调试Hairpinning问题
- 性能分析
2.17.9 Hairpinning的限制和注意事项
1. 需要启用Hairpinning功能
- 默认情况下,添加内部接口时会自动启用
- 如果OUT2IN DPO模式启用,Hairpinning可能不可用
2. 需要正确的路由配置
- NAT地址必须在FIB中有路由
- 内部地址必须在FIB中有路由
3. 性能考虑
- Hairpinning会增加处理开销
- 数据包需要经过两次路由查找(转换前后)
- 需要更新校验和
4. 多Worker场景
- 可能需要跨Worker handoff
- 增加Frame Queue的使用
2.17.10 总结
Hairpinning的核心价值:
- 功能完整性:支持内部主机通过外部地址访问内部服务器
- 用户体验:用户可以使用统一的外部地址访问服务
- 网络简化:避免数据包绕到外部网络再回来
- 安全性:内部流量保持在内部网络
Hairpinning的实现要点:
- Feature Arc集成:在ip4-local弧上运行,处理发往本地IP的数据包
- 映射查找:优先静态映射,其次活动会话
- 地址转换:将外部地址转换为内部地址
- 路由重定向:转换后重新路由到内部接口
- Worker支持:支持多Worker场景,需要handoff时进行handoff
Hairpinning的应用场景:
- 内部服务器访问:内部客户端通过外部地址访问内部服务器
- VPN场景:VPN客户端访问内部资源
- 负载均衡:内部客户端通过负载均衡地址访问
- 统一入口:提供统一的外部访问入口
生活化总结:
Hairpinning就像公司内部的"内部转送窗口":
- 当有包裹(数据包)发往公司地址(NAT地址)时
- 窗口工作人员(Hairpinning节点)检查地址簿(映射表)
- 发现是内部地址,直接改地址标签(转换)
- 重新贴标签后,重新路由到内部部门(内部接口)
- 包裹不需要绕到外部再回来,直接在内部转送
第三章:内联函数库 - nat44_ei_inlines.h
3.1 文件概要
文件位置:
src/plugins/nat/nat44-ei/nat44_ei_inlines.h(256行)
主要作用:
提供高性能的内联函数,用于NAT键值计算、会话查找、哈希表操作等核心操作。这些函数在数据包处理的快速路径中被频繁调用,必须非常高效。
3.2 主要功能模块
3.2.1 NAT键值计算
核心函数:
calc_nat_key- 计算64位NAT查找键split_nat_key- 分解64位键值init_nat_k- 初始化NAT键值(仅key)init_nat_kv- 初始化NAT键值对(key + value)
键值结构:
64位键值 = IP地址(32位) | 端口(16位) | FIB索引(13位) | 协议(3位)
功能说明:
calc_nat_key:将IP地址、端口、FIB索引、协议编码成一个64位整数,用于哈希表查找split_nat_key:从64位键值中提取各个字段init_nat_k:初始化查找键(value设为无效值)init_nat_kv:初始化完整的键值对(包含线程索引和会话索引)
代码实现(src/plugins/nat/nat44-ei/nat44_ei_inlines.h 第25-71行):
c
always_inline u64
calc_nat_key (ip4_address_t addr, u16 port, u32 fib_index, u8 proto)
{
ASSERT (fib_index <= (1 << 14) - 1);
ASSERT (proto <= (1 << 3) - 1);
return (u64) addr.as_u32 << 32 | (u64) port << 16 | fib_index << 3 |
(proto & 0x7);
}
always_inline void
split_nat_key (u64 key, ip4_address_t *addr, u16 *port, u32 *fib_index,
nat_protocol_t *proto)
{
if (addr)
addr->as_u32 = key >> 32;
if (port)
*port = (key >> 16) & (u16) ~0;
if (fib_index)
*fib_index = key >> 3 & ((1 << 13) - 1);
if (proto)
*proto = key & 0x7;
}
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 void
init_nat_kv (clib_bihash_kv_8_8_t *kv, ip4_address_t addr, u16 port,
u32 fib_index, nat_protocol_t proto, u32 thread_index,
u32 session_index)
{
init_nat_k (kv, addr, port, fib_index, proto);
kv->value = (u64) thread_index << 32 | session_index;
}
3.2.2 会话键值初始化
函数:
init_nat_i2o_k- 初始化IN2OUT查找键init_nat_i2o_kv- 初始化IN2OUT键值对init_nat_o2i_k- 初始化OUT2IN查找键init_nat_o2i_kv- 初始化OUT2IN键值对nat_value_get_thread_index- 从value中提取线程索引nat_value_get_session_index- 从value中提取会话索引
功能说明:
- IN2OUT方向:使用会话的内部地址+端口制作查找键
- OUT2IN方向:使用会话的外部地址+端口制作查找键
- Value结构:64位值 = 线程索引(32位) | 会话索引(32位)
代码实现(第73-115行):
c
always_inline void
init_nat_i2o_k (clib_bihash_kv_8_8_t *kv, nat44_ei_session_t *s)
{
return init_nat_k (kv, s->in2out.addr, s->in2out.port,
s->in2out.fib_index, s->nat_proto);
}
always_inline void
init_nat_i2o_kv (clib_bihash_kv_8_8_t *kv, nat44_ei_session_t *s,
u32 thread_index, u32 session_index)
{
init_nat_k (kv, s->in2out.addr, s->in2out.port,
s->in2out.fib_index, s->nat_proto);
kv->value = (u64) thread_index << 32 | session_index;
}
always_inline void
init_nat_o2i_k (clib_bihash_kv_8_8_t *kv, nat44_ei_session_t *s)
{
return init_nat_k (kv, s->out2in.addr, s->out2in.port,
s->out2in.fib_index, s->nat_proto);
}
always_inline void
init_nat_o2i_kv (clib_bihash_kv_8_8_t *kv, nat44_ei_session_t *s,
u32 thread_index, u32 session_index)
{
init_nat_k (kv, s->out2in.addr, s->out2in.port,
s->out2in.fib_index, s->nat_proto);
kv->value = (u64) thread_index << 32 | session_index;
}
always_inline u32
nat_value_get_thread_index (clib_bihash_kv_8_8_t *value)
{
return value->value >> 32;
}
always_inline u32
nat_value_get_session_index (clib_bihash_kv_8_8_t *value)
{
return value->value & ~(u32) 0;
}
3.2.3 接口地址检查
函数:nat44_ei_is_interface_addr
功能说明:
检查一个IP地址是否是接口的地址。使用缓存机制避免重复查询,提高性能。
代码实现(第117-149行):
c
always_inline u8
nat44_ei_is_interface_addr (ip4_main_t *im, vlib_node_runtime_t *node,
u32 sw_if_index0, u32 ip4_addr)
{
nat44_ei_runtime_t *rt = (nat44_ei_runtime_t *) node->runtime_data;
u8 ip4_addr_exists;
// 如果接口变了,重新缓存接口地址列表
if (PREDICT_FALSE (rt->cached_sw_if_index != sw_if_index0))
{
ip_lookup_main_t *lm = &im->lookup_main;
ip_interface_address_t *ia;
ip4_address_t *a;
rt->cached_sw_if_index = ~0;
hash_free (rt->cached_presence_by_ip4_address);
// 遍历接口的所有地址,加入哈希表
foreach_ip_interface_address (
lm, ia, sw_if_index0, 1 /* honor unnumbered */, ({
a = ip_interface_address_get_address (lm, ia);
hash_set (rt->cached_presence_by_ip4_address, a->as_u32, 1);
rt->cached_sw_if_index = sw_if_index0;
}));
if (rt->cached_sw_if_index == ~0)
return 0;
}
// 在缓存的地址列表中查找
ip4_addr_exists = !!hash_get (rt->cached_presence_by_ip4_address, ip4_addr);
if (PREDICT_FALSE (ip4_addr_exists))
return 1;
else
return 0;
}
关键点:
- 使用运行时缓存(
rt->cached_presence_by_ip4_address)存储接口地址 - 只有当接口变化时才重新构建缓存
- 使用哈希表实现O(1)查找
3.2.4 会话管理辅助函数
函数列表:
nat44_ei_session_update_lru- 更新会话的LRU列表位置nat44_ei_user_session_increment- 增加用户的会话计数nat44_ei_delete_user_with_no_session- 删除没有会话的用户nat44_ei_session_update_counters- 更新会话统计信息nat_session_get_timeout- 根据协议和状态获取超时时间nat44_ei_maximum_sessions_exceeded- 检查是否超过最大会话数
功能说明:
- LRU维护:将会话移到LRU列表末尾,但避免频繁更新(每秒最多更新一次)
- 用户计数:分别统计静态会话和动态会话数量
- 用户清理:当用户没有会话时,自动删除用户结构
- 统计更新:更新数据包计数、字节计数,并同步到HA系统
- 超时获取:根据协议类型(TCP/UDP/ICMP)和TCP状态返回相应的超时时间
代码实现(第152-245行):
c
always_inline void
nat44_ei_session_update_lru (nat44_ei_main_t *nm, nat44_ei_session_t *s,
u32 thread_index)
{
// 不要更新太频繁,超时是以秒为单位的
if (s->last_heard > s->last_lru_update + 1)
{
clib_dlist_remove (nm->per_thread_data[thread_index].list_pool,
s->per_user_index);
clib_dlist_addtail (nm->per_thread_data[thread_index].list_pool,
s->per_user_list_head_index, s->per_user_index);
s->last_lru_update = s->last_heard;
}
}
always_inline void
nat44_ei_user_session_increment (nat44_ei_main_t *nm, nat44_ei_user_t *u,
u8 is_static)
{
if (u->nsessions + u->nstaticsessions < nm->max_translations_per_user)
{
if (is_static)
u->nstaticsessions++;
else
u->nsessions++;
}
}
always_inline void
nat44_ei_delete_user_with_no_session (nat44_ei_main_t *nm, nat44_ei_user_t *u,
u32 thread_index)
{
clib_bihash_kv_8_8_t kv;
nat44_ei_user_key_t u_key;
nat44_ei_main_per_thread_data_t *tnm =
vec_elt_at_index (nm->per_thread_data, thread_index);
if (u->nstaticsessions == 0 && u->nsessions == 0)
{
u_key.addr.as_u32 = u->addr.as_u32;
u_key.fib_index = u->fib_index;
kv.key = u_key.as_u64;
pool_put_index (tnm->list_pool, u->sessions_per_user_list_head_index);
pool_put (tnm->users, u);
clib_bihash_add_del_8_8 (&tnm->user_hash, &kv, 0);
vlib_set_simple_counter (&nm->total_users, thread_index, 0,
pool_elts (tnm->users));
}
}
always_inline void
nat44_ei_session_update_counters (nat44_ei_session_t *s, f64 now, uword bytes,
u32 thread_index)
{
s->last_heard = now;
s->total_pkts++;
s->total_bytes += bytes;
nat_ha_sref (&s->out2in.addr, s->out2in.port, &s->ext_host_addr,
s->ext_host_port, s->nat_proto, s->out2in.fib_index,
s->total_pkts, s->total_bytes, thread_index,
&s->ha_last_refreshed, now);
}
static_always_inline u32
nat_session_get_timeout (nat_timeouts_t *timeouts, nat_protocol_t proto,
u8 state)
{
switch (proto)
{
case NAT_PROTOCOL_ICMP:
return timeouts->icmp;
case NAT_PROTOCOL_UDP:
return timeouts->udp;
case NAT_PROTOCOL_TCP:
{
if (state)
return timeouts->tcp.transitory;
else
return timeouts->tcp.established;
}
default:
return timeouts->udp;
}
return 0;
}
static_always_inline u8
nat44_ei_maximum_sessions_exceeded (nat44_ei_main_t *nm, u32 thread_index)
{
if (pool_elts (nm->per_thread_data[thread_index].sessions) >=
nm->max_translations_per_thread)
return 1;
return 0;
}
3.3 设计特点
- 内联优化 :所有函数都是
always_inline,减少函数调用开销 - 位操作优化:使用位操作实现高效的键值计算和分解
- 缓存机制:接口地址检查使用运行时缓存,避免重复查询
- 性能关键路径:这些函数在数据包处理的快速路径中被频繁调用
3.4 依赖关系
- 依赖
nat44_ei.h中的数据结构定义 - 依赖
nat44_ei_ha.h中的HA相关函数 - 被所有数据包处理模块使用(IN2OUT、OUT2IN、Hairpinning等)
第四章:内部到外部转换 - nat44_ei_in2out.c
4.1 生活化理解:IN2OUT转换是什么?
想象一下:员工要出门办事
IN2OUT转换就像公司门卫处理员工出门:
- 员工(内部主机):10.0.0.2:5000
- 要访问外部服务器:8.8.8.8:80
- 门卫(NAT):给员工换一个"临时工牌"(外部地址+端口)
- 临时工牌:192.168.2.213:6000
核心任务:
- 检查员工是否有"临时工牌"(查找会话)
- 如果没有,制作一个"临时工牌"(创建会话)
- 把员工的"内部工牌"换成"临时工牌"(转换地址和端口)
- 让员工出门(转发数据包)
4.2 处理流程:Fast Path vs Slow Path
生活化理解:
Fast Path(快速通道):
- 员工已经有"临时工牌"了
- 门卫直接查记录,换工牌,放行
- 非常快,几微秒就完成
Slow Path(慢速通道):
- 员工第一次出门,没有"临时工牌"
- 门卫需要:
- 检查员工档案(创建/查找用户)
- 制作"临时工牌"(分配外部IP和端口)
- 登记到记录本(插入哈希表)
- 换工牌,放行
- 比较慢,需要几十微秒
4.3 核心代码实现
4.3.1 主处理节点:nat44_ei_in2out_node_fn_inline
生活化理解:
这是门卫的"工作流程":
1. 接收一批员工(批量处理数据包)
2. 对每个员工:
- 检查是否有"临时工牌"(查找会话)
- 如果有:快速通道(Fast Path)
- 如果没有:慢速通道(Slow Path)
3. 换工牌(转换地址和端口)
4. 放行(转发到下一节点)
代码实现(src/plugins/nat/nat44-ei/nat44_ei_in2out.c 第1272-1942行):
c
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)
{
// ... 初始化变量 ...
// 批量处理数据包(向量化处理)
while (n_left_from >= 2)
{
// 提取数据包信息
ip0 = (ip4_header_t *) vlib_buffer_get_current (b0);
udp0 = ip4_next_header (ip0);
tcp0 = (tcp_header_t *) udp0;
// 提取源IP、源端口、协议
rx_fib_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX];
proto0 = ip_proto_to_nat_proto (ip0->protocol);
// 构建查找键(用内部地址+端口查找)
init_nat_k (&kv0, ip0->src_address,
vnet_buffer (b0)->ip.reass.l4_src_port,
rx_fib_index0, proto0);
// 查找会话(检查是否有"临时工牌")
if (clib_bihash_search_8_8 (&nm->in2out, &kv0, &value0) != 0)
{
// 没找到会话,进入慢速通道
if (is_slow_path)
{
// 检查是否需要转换
if (nat44_ei_not_translate (...))
goto trace00;
// 创建新会话(制作"临时工牌")
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);
if (next0 == NAT44_EI_IN2OUT_NEXT_DROP)
goto trace00;
}
else
{
// 不是慢速通道,转发到慢速通道节点
next0 = NAT44_EI_IN2OUT_NEXT_SLOW_PATH;
goto trace00;
}
}
else
{
// 找到会话,快速通道(直接使用已有的"临时工牌")
s0 = pool_elt_at_index (nm->per_thread_data[thread_index].sessions,
nat_value_get_session_index (&value0));
}
// 标记数据包已转换
b0->flags |= VNET_BUFFER_F_IS_NATED;
// 转换源IP地址(换工牌)
old_addr0 = ip0->src_address.as_u32;
ip0->src_address = s0->out2in.addr; // 内部地址 → 外部地址
new_addr0 = ip0->src_address.as_u32;
// 更新IP校验和
sum0 = ip0->checksum;
sum0 = ip_csum_update (sum0, old_addr0, new_addr0,
ip4_header_t, src_address);
ip0->checksum = ip_csum_fold (sum0);
// 转换源端口(根据协议类型)
if (proto0 == NAT_PROTOCOL_TCP)
{
old_port0 = tcp0->src;
tcp0->src = s0->out2in.port; // 内部端口 → 外部端口
// 更新TCP校验和
// ...
}
else if (proto0 == NAT_PROTOCOL_UDP)
{
old_port0 = udp0->src_port;
udp0->src_port = s0->out2in.port;
udp0->checksum = 0; // UDP校验和可选
}
// 更新会话统计
nat44_ei_session_update_counters (s0, now, b0->current_length,
thread_index);
// 转发到下一节点
// ...
}
}
关键点:
- 向量化处理:一次处理多个数据包,提高效率
- Fast Path优先:大多数数据包走快速通道
- Slow Path按需:只有新会话才走慢速通道
4.3.2 Slow Path:创建新会话
生活化理解:
门卫制作"临时工牌"的流程:
1. 检查员工档案(查找/创建用户)
2. 分配外部IP和端口(制作"临时工牌")
3. 检查静态映射(是否有固定工牌)
4. 创建会话记录(登记到记录本)
5. 插入哈希表(方便下次查找)
代码实现(第366-494行):
c
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)
{
nat44_ei_user_t *u;
nat44_ei_session_t *s = 0;
clib_bihash_kv_8_8_t kv0;
u8 is_sm = 0;
ip4_address_t sm_addr;
u16 sm_port;
u32 sm_fib_index;
u8 identity_nat;
// 1. 检查是否超过最大会话数
if (nat44_ei_maximum_sessions_exceeded (nm, thread_index))
{
b0->error = node->errors[NAT44_EI_IN2OUT_ERROR_MAX_SESSIONS_EXCEEDED];
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 (...))
{
b0->error = node->errors[NAT44_EI_IN2OUT_ERROR_OUT_OF_PORTS];
return NAT44_EI_IN2OUT_NEXT_DROP;
}
}
else
{
// 没有静态映射,标记为静态映射会话
is_sm = 1;
}
// 3. 查找或创建用户(检查员工档案)
u = nat44_ei_user_get_or_create (nm, &ip0->src_address, rx_fib_index0,
thread_index);
if (!u)
{
b0->error = node->errors[NAT44_EI_IN2OUT_ERROR_CANNOT_CREATE_USER];
return NAT44_EI_IN2OUT_NEXT_DROP;
}
// 4. 分配会话结构(制作"临时工牌")
s = nat44_ei_session_alloc_or_recycle (nm, u, thread_index, now);
if (!s)
{
nat44_ei_delete_user_with_no_session (nm, u, thread_index);
return NAT44_EI_IN2OUT_NEXT_DROP;
}
// 5. 设置会话信息
if (is_sm)
s->flags |= NAT44_EI_SESSION_FLAG_STATIC_MAPPING;
s->in2out.addr = i2o_addr; // 内部地址
s->in2out.port = i2o_port; // 内部端口
s->in2out.fib_index = rx_fib_index0;
s->nat_proto = nat_proto;
s->out2in.addr = sm_addr; // 外部地址
s->out2in.port = sm_port; // 外部端口
s->out2in.fib_index = nm->outside_fib_index;
s->ext_host_addr = ip0->dst_address; // 外部服务器地址
s->ext_host_port = vnet_buffer (b0)->ip.reass.l4_dst_port;
// 6. 插入哈希表(登记到记录本)
init_nat_i2o_kv (&kv0, s, thread_index,
s - nm->per_thread_data[thread_index].sessions);
clib_bihash_add_or_overwrite_stale_8_8 (&nm->in2out, &kv0, ...);
init_nat_o2i_kv (&kv0, s, thread_index,
s - nm->per_thread_data[thread_index].sessions);
clib_bihash_add_or_overwrite_stale_8_8 (&nm->out2in, &kv0, ...);
*sessionp = s;
return next0;
}
关键点:
- 用户管理:每个内部IP对应一个用户,管理该用户的所有会话
- 会话分配:从会话池中分配,如果满了会回收最久未使用的会话
- 哈希表插入:同时插入in2out和out2in两个哈希表,支持双向查找
4.4 协议支持
TCP/UDP处理:
- 转换源IP和源端口
- 更新IP和TCP/UDP校验和
- TCP还需要更新伪首部
ICMP处理:
- 转换ICMP标识符(identifier)
- 支持Echo Request/Reply
- 支持ICMP错误消息(需要特殊处理)
未知协议:
- 只转换IP层地址
- 不转换端口(因为没有端口)
4.5 错误处理
常见错误:
OUT_OF_PORTS:端口耗尽,无法分配新端口MAX_SESSIONS_EXCEEDED:超过最大会话数CANNOT_CREATE_USER:无法创建用户(内存不足)NO_TRANSLATION:不需要转换(目标地址是内部地址)
第五章:外部到内部转换 - nat44_ei_out2in.c
5.1 生活化理解:OUT2IN转换是什么?
想象一下:外部访客要进公司
OUT2IN转换就像公司门卫处理外部访客:
- 外部访客(外部服务器):8.8.8.8:80
- 拿着临时工牌:192.168.2.213:6000
- 门卫(NAT):查记录,把"临时工牌"换回"内部工牌"
- 内部工牌:10.0.0.2:5000
核心任务:
- 检查"临时工牌"是否有效(查找会话)
- 如果没有,检查是否有静态映射(固定访客)
- 把"临时工牌"换成"内部工牌"(转换地址和端口)
- 让访客进入(转发数据包)
5.2 处理流程:静态映射优先
生活化理解:
OUT2IN的特殊性:
- 大多数数据包是返回流量(已有会话)
- 少数数据包是外部发起的连接(需要静态映射)
- 静态映射优先:先查静态映射,再查动态会话
处理流程:
- 提取目标IP和端口(外部地址+端口)
- 先查静态映射(是否有固定访客记录)
- 再查动态会话(是否有临时工牌记录)
- 如果都没有,且没有静态映射,丢弃数据包
- 如果有静态映射,创建会话(仅静态映射支持外部发起)
5.3 核心代码实现
5.3.1 主处理节点:nat44_ei_out2in_node
生活化理解:
门卫处理外部访客的流程:
1. 接收一批访客(批量处理数据包)
2. 对每个访客:
- 检查"临时工牌"(查找会话)
- 如果没有,检查是否有固定访客记录(静态映射)
- 如果有静态映射,创建会话
- 换工牌(转换地址和端口)
3. 放行(转发到下一节点)
代码实现(src/plugins/nat/nat44-ei/nat44_ei_out2in.c 第688-1240行):
c
VLIB_NODE_FN (nat44_ei_out2in_node)
(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame)
{
// ... 初始化变量 ...
// 批量处理数据包
while (n_left_from >= 2)
{
// 提取数据包信息
ip0 = vlib_buffer_get_current (b0);
udp0 = ip4_next_header (ip0);
tcp0 = (tcp_header_t *) udp0;
// 提取目标IP、目标端口、协议
rx_fib_index0 = vnet_buffer (b0)->sw_if_index[VLIB_RX];
proto0 = ip_proto_to_nat_proto (ip0->protocol);
// 构建查找键(用外部地址+端口查找)
init_nat_k (&kv0, ip0->dst_address,
vnet_buffer (b0)->ip.reass.l4_dst_port,
rx_fib_index0, proto0);
// 查找会话(检查是否有"临时工牌"记录)
if (clib_bihash_search_8_8 (&nm->out2in, &kv0, &value0))
{
// 没找到会话,检查静态映射
if (nat44_ei_static_mapping_match (
ip0->dst_address, vnet_buffer (b0)->ip.reass.l4_dst_port,
rx_fib_index0, proto0, &sm_addr0, &sm_port0, &sm_fib_index0,
1, 0, &identity_nat0))
{
// 找到静态映射,创建会话(固定访客)
s0 = create_session_for_static_mapping (
nm, b0, sm_addr0, sm_port0, sm_fib_index0,
ip0->dst_address, vnet_buffer (b0)->ip.reass.l4_dst_port,
rx_fib_index0, proto0, node, thread_index, now);
if (!s0)
{
next0 = NAT44_EI_OUT2IN_NEXT_DROP;
goto trace0;
}
}
else
{
// 没有静态映射,丢弃数据包
if (!nm->forwarding_enabled)
{
b0->error = node->errors[NAT44_EI_OUT2IN_ERROR_NO_TRANSLATION];
next0 = NAT44_EI_OUT2IN_NEXT_DROP;
}
goto trace0;
}
}
else
{
// 找到会话,直接使用(已有"临时工牌"记录)
s0 = pool_elt_at_index (tnm->sessions,
nat_value_get_session_index (&value0));
}
// 转换目标IP地址(换工牌)
old_addr0 = ip0->dst_address.as_u32;
ip0->dst_address = s0->in2out.addr; // 外部地址 → 内部地址
new_addr0 = ip0->dst_address.as_u32;
// 更新IP校验和
sum0 = ip0->checksum;
sum0 = ip_csum_update (sum0, old_addr0, new_addr0,
ip4_header_t, dst_address);
ip0->checksum = ip_csum_fold (sum0);
// 转换目标端口
if (proto0 == NAT_PROTOCOL_TCP)
{
old_port0 = tcp0->dst;
tcp0->dst = s0->in2out.port; // 外部端口 → 内部端口
// 更新TCP校验和
// ...
}
else if (proto0 == NAT_PROTOCOL_UDP)
{
old_port0 = udp0->dst_port;
udp0->dst_port = s0->in2out.port;
udp0->checksum = 0;
}
// 更新会话统计
nat44_ei_session_update_counters (s0, now, b0->current_length,
thread_index);
// 转发到下一节点
// ...
}
}
关键点:
- 静态映射优先:先查静态映射,再查动态会话
- 仅静态映射支持外部发起:动态会话不支持外部发起的连接
- 端点无关特性:同一个外部IP:端口可以映射到不同的内部主机(根据目标地址)
5.3.2 静态映射会话创建:create_session_for_static_mapping
生活化理解:
为固定访客创建会话记录:
1. 查找静态映射(固定访客记录)
2. 创建会话结构
3. 设置会话信息(内部地址+端口)
4. 插入哈希表
代码实现(第202-236行):
c
static nat44_ei_session_t *
create_session_for_static_mapping (nat44_ei_main_t *nm, vlib_buffer_t *b0,
ip4_address_t i2o_addr, u16 i2o_port,
u32 i2o_fib_index, ip4_address_t o2i_addr,
u16 o2i_port, u32 o2i_fib_index,
nat_protocol_t proto,
vlib_node_runtime_t *node, u32 thread_index,
f64 now)
{
nat44_ei_session_t *s;
clib_bihash_kv_8_8_t kv0;
nat44_ei_is_idle_session_ctx_t ctx0;
// 分配会话结构
s = nat44_ei_session_alloc_or_recycle (nm, NULL, thread_index, now);
if (!s)
return NULL;
// 设置会话信息
s->flags |= NAT44_EI_SESSION_FLAG_STATIC_MAPPING;
s->in2out.addr = i2o_addr; // 内部地址
s->in2out.port = i2o_port; // 内部端口
s->in2out.fib_index = i2o_fib_index;
s->out2in.addr = o2i_addr; // 外部地址
s->out2in.port = o2i_port; // 外部端口
s->out2in.fib_index = o2i_fib_index;
s->nat_proto = proto;
s->ext_host_addr = ip0->src_address; // 外部服务器地址
s->ext_host_port = vnet_buffer (b0)->ip.reass.l4_src_port;
// 插入哈希表
ctx0.now = now;
ctx0.thread_index = thread_index;
init_nat_i2o_kv (&kv0, s, thread_index,
s - nm->per_thread_data[thread_index].sessions);
clib_bihash_add_or_overwrite_stale_8_8 (&nm->in2out, &kv0, ...);
init_nat_o2i_kv (&kv0, s, thread_index,
s - nm->per_thread_data[thread_index].sessions);
clib_bihash_add_or_overwrite_stale_8_8 (&nm->out2in, &kv0, ...);
return s;
}
5.4 ICMP错误消息处理
生活化理解:
ICMP错误消息(如Time Exceeded、Destination Unreachable)包含原始IP头:
- 需要从ICMP错误消息中提取原始IP头
- 查找原始IP头对应的会话
- 转换ICMP错误消息中的IP头
处理流程:
- 提取ICMP错误消息中的原始IP头
- 查找原始IP头对应的会话
- 转换原始IP头中的地址和端口
- 更新ICMP校验和
5.5 关键设计特点
- 静态映射优先:提高性能,静态映射查找更快
- 端点无关特性:同一个外部IP:端口可以映射到不同的内部主机(根据目标地址)
- 仅静态映射支持外部发起:动态会话不支持外部发起的连接,提高安全性
第六章:多Worker Handoff - nat44_ei_handoff.c
6.1 生活化理解:为什么需要Handoff?
想象一下:多窗口银行
假设银行有多个窗口(Worker线程),每个窗口有独立的"档案柜"(会话表):
- 窗口1:处理客户A的业务,有客户A的档案
- 窗口2:处理客户B的业务,有客户B的档案
- 窗口3:处理客户C的业务,有客户C的档案
问题场景:
客户A的业务(数据包)被随机分配到窗口2:
- 窗口2没有客户A的档案(会话在窗口1)
- 窗口2需要把业务转给窗口1(Handoff)
- 窗口1处理完业务后,可能还要转回窗口2(返回路径)
为什么需要Handoff?
- 会话亲和性:同一会话的所有数据包必须在同一个Worker上处理
- 避免锁竞争:每个Worker有独立的会话表,不需要加锁
- 性能优化:本地会话表查找,避免跨线程访问
6.2 Handoff机制详解
6.2.1 什么是Handoff?
生活化理解:
Handoff就像银行的"业务转送":
- 当前窗口(Worker A):收到一个业务(数据包)
- 检查档案:发现这个业务的档案在另一个窗口(Worker B)
- 转送业务:把业务放到"转送箱"(Frame Queue)
- 目标窗口(Worker B):从"转送箱"取出业务处理
技术定义:
Handoff是将数据包从一个Worker线程转移到另一个Worker线程的机制,确保数据包在正确的Worker上处理。
6.2.2 Worker选择逻辑
生活化理解:
就像银行根据"客户编号"决定去哪个窗口:
- IN2OUT方向:根据"客户编号"(源IP地址)选择窗口
- OUT2IN方向:根据"业务编号"(目标端口)选择窗口
IN2OUT方向:根据源IP地址哈希
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第1717-1735行):
c
u32
nat44_ei_get_in2out_worker_index (ip4_header_t *ip0, u32 rx_fib_index0,
u8 is_output)
{
nat44_ei_main_t *nm = &nat44_ei_main;
u32 next_worker_index = 0;
u32 hash;
// 基础Worker索引(第一个Worker)
next_worker_index = nm->first_worker_index;
// 计算源IP地址的哈希值(简单哈希算法)
hash = ip0->src_address.as_u32 + (ip0->src_address.as_u32 >> 8) +
(ip0->src_address.as_u32 >> 16) + (ip0->src_address.as_u32 >> 24);
// 根据哈希值选择Worker(使用位运算优化)
if (PREDICT_TRUE (is_pow2 (_vec_len (nm->workers))))
next_worker_index += nm->workers[hash & (_vec_len (nm->workers) - 1)];
else
next_worker_index += nm->workers[hash % _vec_len (nm->workers)];
return next_worker_index;
}
生活化解释:
- 哈希计算:把源IP地址(如10.0.0.2)转换成一个数字(哈希值)
- Worker选择:用哈希值对Worker数量取模,得到Worker索引
- 会话亲和性:同一个源IP的数据包总是选择同一个Worker
例子:
源IP:10.0.0.2
哈希值:10 + 0 + 0 + 2 = 12
Worker数量:4
Worker索引:12 % 4 = 0(选择Worker 0)
源IP:10.0.0.3
哈希值:10 + 0 + 0 + 3 = 13
Worker索引:13 % 4 = 1(选择Worker 1)
OUT2IN方向:根据目标端口选择
代码实现(第1751-1800行):
c
u32
nat44_ei_get_out2in_worker_index (vlib_buffer_t *b, ip4_header_t *ip0,
u32 rx_fib_index0, u8 is_output)
{
nat44_ei_main_t *nm = &nat44_ei_main;
udp_header_t *udp;
u16 port;
clib_bihash_kv_8_8_t kv, value;
nat44_ei_static_mapping_t *m;
u32 proto;
u32 next_worker_index = 0;
// 1. 先检查静态映射(固定客户有固定窗口)
if (PREDICT_FALSE (pool_elts (nm->static_mappings)))
{
init_nat_k (&kv, ip0->dst_address, 0, rx_fib_index0, 0);
if (!clib_bihash_search_8_8 (&nm->static_mapping_by_external, &kv,
&value))
{
m = pool_elt_at_index (nm->static_mappings, value.value);
return m->workers[0]; // 静态映射指定的Worker
}
}
// 2. 根据目标端口选择Worker
proto = ip_proto_to_nat_proto (ip0->protocol);
udp = ip4_next_header (ip0);
port = vnet_buffer (b)->ip.reass.l4_dst_port;
// 3. 未知协议,使用当前Worker
if (PREDICT_FALSE (proto == NAT_PROTOCOL_OTHER))
{
return vlib_get_thread_index ();
}
// 4. 根据端口范围选择Worker
// 端口范围:1024-65535,平均分配给各个Worker
// Worker索引 = (端口 - 1024) / 每Worker端口数
return nat44_ei_get_thread_idx_by_port (port);
}
生活化解释:
- 静态映射优先:固定客户(静态映射)有固定的窗口
- 端口范围分配:端口1024-65535平均分配给各个Worker
- 会话亲和性:同一个端口的数据包总是选择同一个Worker
例子:
目标端口:6000
每Worker端口数:(65535 - 1024) / 4 = 16127
Worker索引:(6000 - 1024) / 16127 = 0(选择Worker 0)
目标端口:20000
Worker索引:(20000 - 1024) / 16127 = 1(选择Worker 1)
6.3 Frame Queue机制简介
6.3.1 什么是Frame Queue?
生活化理解:
Frame Queue就像银行的"业务转送箱":
- 多个转送箱:每个窗口(Worker)有一个转送箱
- 无锁设计:转送箱使用无锁队列,不需要加锁
- 批量处理:一次可以放入多个业务(Frame包含多个数据包)
技术定义:
Frame Queue是VPP中用于跨线程通信的无锁队列机制:
- 生产者:当前Worker线程(放入数据包)
- 消费者:目标Worker线程(取出数据包)
- 无锁设计:使用原子操作和内存屏障,避免锁竞争
详细实现:
Frame Queue的详细实现原理、无锁队列设计、内存屏障使用、缓存行对齐等深入内容,请参考专门的文章:
- 《VPP Frame Queue无锁队列详解:从原理到实现》
6.3.2 Frame Queue的初始化
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第547-560行):
c
if (nm->num_workers > 1)
{
vlib_main_t *vm = vlib_get_main ();
vlib_node_t *node;
// 初始化IN2OUT Frame Queue
if (nm->fq_in2out_index == ~0)
{
node = vlib_get_node_by_name (vm, (u8 *) "nat44-ei-in2out");
nm->fq_in2out_index =
vlib_frame_queue_main_init (node->index, nm->frame_queue_nelts);
}
// 初始化OUT2IN Frame Queue
if (nm->fq_out2in_index == ~0)
{
node = vlib_get_node_by_name (vm, (u8 *) "nat44-ei-out2in");
nm->fq_out2in_index =
vlib_frame_queue_main_init (node->index, nm->frame_queue_nelts);
}
// 初始化IN2OUT Output Frame Queue
if (nm->fq_in2out_output_index == ~0)
{
node = vlib_get_node_by_name (vm, (u8 *) "nat44-ei-in2out-output");
nm->fq_in2out_output_index =
vlib_frame_queue_main_init (node->index, nm->frame_queue_nelts);
}
}
生活化解释:
- 创建转送箱:为每个方向(IN2OUT、OUT2IN)创建一个转送箱
- 转送箱大小 :
frame_queue_nelts(默认值,可配置) - 目标节点:指定数据包转送后要到达的节点(nat44-ei-in2out等)
6.3.3 Frame Queue的使用
入队操作(Enqueue):
当前Worker把数据包放入转送箱:
- 调用
vlib_buffer_enqueue_to_thread函数 - 数据包被放入目标Worker的Frame Queue
- 使用原子操作确保线程安全
出队操作(Dequeue):
目标Worker从转送箱取出数据包:
- Worker主循环检查Frame Queue
- 调用
vlib_frame_queue_dequeue_inline函数 - 数据包被取出并放入目标节点的Frame
关键特性:
- 无锁设计:使用原子操作和内存屏障
- 批量处理:一次处理多个数据包
- 高性能:避免锁竞争,支持百万级数据包/秒
详细实现原理请参考:
- 《VPP Frame Queue无锁队列详解:从原理到实现》
6.4 Worker线程的运行流程
6.4.1 Worker线程的主循环
生活化理解:
Worker线程就像银行的"工作循环":
- 检查转送箱(Frame Queue)是否有业务
- 处理输入业务(Input节点)
- 处理转送来的业务(Frame Queue出队)
- 处理待处理的业务(Pending Frames)
- 重复循环
代码实现(src/vlib/main.c 第1456-1610行):
c
static_always_inline void
vlib_main_or_worker_loop (vlib_main_t *vm, int is_main)
{
vlib_node_main_t *nm = &vm->node_main;
vlib_thread_main_t *tm = vlib_get_thread_main ();
vlib_frame_queue_main_t *fqm;
u32 frame_queue_check_counter = 0;
while (1)
{
// 1. 检查Frame Queue(转送箱)
if (PREDICT_FALSE (vm->check_frame_queues + frame_queue_check_counter))
{
u32 processed = 0;
vlib_frame_queue_dequeue_fn_t *fn;
if (vm->check_frame_queues)
{
frame_queue_check_counter = 100;
vm->check_frame_queues = 0;
}
// 遍历所有Frame Queue,取出数据包
vec_foreach (fqm, tm->frame_queue_mains)
{
fn = fqm->frame_queue_dequeue_fn;
processed += (fn) (vm, fqm);
}
// 如果处理了数据包,继续检查
if (processed)
frame_queue_check_counter = 100;
else
frame_queue_check_counter--;
}
// 2. 处理输入节点(Input节点)
vec_foreach (n, nm->nodes_by_type[VLIB_NODE_TYPE_INPUT])
cpu_time_now = dispatch_node (vm, n, VLIB_NODE_TYPE_INPUT,
VLIB_NODE_STATE_POLLING,
/* frame */ 0, cpu_time_now);
// 3. 处理待处理的Frame(Pending Frames)
for (i = 0; i < _vec_len (nm->pending_frames); i++)
cpu_time_now = dispatch_pending_node (vm, i, cpu_time_now);
vec_set_len (nm->pending_frames, 0);
// 4. 继续循环...
}
}
关键点:
- Frame Queue检查:每100次循环检查一次(可调整)
- 批量处理:一次处理多个数据包
- 优先级:Input节点优先,然后处理Frame Queue
6.4.2 完整的数据包流程
场景:IN2OUT方向的数据包Handoff
步骤1:数据包到达Worker A(错误的Worker)
1. 数据包从接口到达
↓
2. 进入Input节点(如dpdk-input)
↓
3. 进入Feature Arc(如ip4-input)
↓
4. 进入NAT节点(nat44-ei-in2out)
↓
5. NAT节点查找会话:未找到(会话在Worker B)
↓
6. 进入Handoff节点(nat44-ei-in2out-worker-handoff)
步骤2:Handoff节点处理
代码实现(src/plugins/nat/nat44-ei/nat44_ei_handoff.c 第67-280行):
c
static inline uword
nat44_ei_worker_handoff_fn_inline (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t *frame, u8 is_output,
u8 is_in2out)
{
u32 n_enq, n_left_from, *from, do_handoff = 0, same_worker = 0;
u16 thread_indices[VLIB_FRAME_SIZE], *ti = thread_indices;
vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b = bufs;
nat44_ei_main_t *nm = &nat44_ei_main;
u32 fq_index, thread_index = vm->thread_index;
from = vlib_frame_vector_args (frame);
n_left_from = frame->n_vectors;
vlib_get_buffers (vm, from, b, n_left_from);
// 1. 确定Frame Queue索引
if (is_in2out)
{
fq_index = is_output ? nm->fq_in2out_output_index : nm->fq_in2out_index;
}
else
{
fq_index = nm->fq_out2in_index;
}
// 2. 批量处理数据包
while (n_left_from >= 4)
{
ip4_header_t *ip0, *ip1, *ip2, *ip3;
// 提取IP头
ip0 = (ip4_header_t *) vlib_buffer_get_current (b[0]);
// ...
// 3. 保存Feature Arc的next节点(arc_next)
vnet_feature_next (&arc_next0, b[0]);
vnet_buffer2 (b[0])->nat.arc_next = arc_next0;
// 4. 确定目标Worker
if (is_in2out)
{
ti[0] = nat44_ei_get_in2out_worker_index (ip0, rx_fib_index0, is_output);
}
else
{
ti[0] = nat44_ei_get_out2in_worker_index (b[0], ip0, rx_fib_index0, is_output);
}
// 5. 统计:是否需要Handoff
if (ti[0] == thread_index)
same_worker++;
else
do_handoff++;
b += 4;
ti += 4;
n_left_from -= 4;
}
// 6. 将数据包放入Frame Queue
n_enq = vlib_buffer_enqueue_to_thread (vm, node, fq_index, from,
thread_indices, frame->n_vectors, 1);
// 7. 检查拥塞
if (n_enq < frame->n_vectors)
{
vlib_node_increment_counter (vm, node->node_index,
NAT44_EI_HANDOFF_ERROR_CONGESTION_DROP,
frame->n_vectors - n_enq);
}
return frame->n_vectors;
}
步骤3:Worker B从Frame Queue取出数据包
1. Worker B的主循环检查Frame Queue
↓
2. 发现Frame Queue有数据包
↓
3. 调用vlib_frame_queue_dequeue_fn取出数据包
↓
4. 数据包被放入目标节点的Frame(nat44-ei-in2out)
↓
5. 目标节点被调度执行
步骤4:Worker B处理数据包
1. NAT节点(nat44-ei-in2out)处理数据包
↓
2. 查找会话:找到(会话在Worker B)
↓
3. 执行NAT转换
↓
4. 恢复Feature Arc的next节点(从arc_next恢复)
↓
5. 继续Feature Arc处理
↓
6. 转发到下一节点(如ip4-lookup)
完整流程图:
Worker A (错误的Worker):
数据包到达
↓
Input节点
↓
Feature Arc (ip4-input)
↓
NAT节点 (nat44-ei-in2out)
↓ (查找会话:未找到)
Handoff节点 (nat44-ei-in2out-worker-handoff)
↓ (确定目标Worker:Worker B)
Frame Queue入队
↓
[跨线程传输]
Worker B (正确的Worker):
Frame Queue出队
↓
NAT节点 (nat44-ei-in2out)
↓ (查找会话:找到)
NAT转换
↓
恢复Feature Arc (从arc_next恢复)
↓
Feature Arc继续处理
↓
转发到下一节点
6.5 关键设计特点
6.5.1 无锁设计
生活化理解:
就像银行的"转送箱"使用"取号机":
- 生产者:放入业务时,取一个号(原子操作更新tail)
- 消费者:取出业务时,取一个号(原子操作更新head)
- 不需要锁:使用原子操作,避免锁竞争
技术实现:
- 使用
__atomic_store_n和__atomic_load_n实现原子操作 - 使用内存屏障(
__ATOMIC_RELEASE/__ATOMIC_ACQUIRE)确保顺序 - 缓存行对齐,避免false sharing
6.5.2 会话亲和性
生活化理解:
确保同一客户的所有业务都在同一个窗口处理:
- IN2OUT:同一源IP的数据包 → 同一Worker
- OUT2IN:同一目标端口的数据包 → 同一Worker
好处:
- 会话表查找快(本地查找)
- 避免跨线程访问
- 减少锁竞争
6.6 数据包流转流程图
6.6.1 IN2OUT方向数据包流转图
完整流程图:从Rx接收队列到NAT节点,通过Handoff相互流转
┌─────────────────────────────────────────────────────────────────────────┐
│ IN2OUT方向数据包流转图 │
└─────────────────────────────────────────────────────────────────────────┘
Rx接收队列(多个接口并行接收)
│
├─────────────────┬─────────────────┬─────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Worker 0 │ │Worker 1 │ │Worker 2 │ │Worker 3 │
│流水线 │ │流水线 │ │流水线 │ │流水线 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Input节点(dpdk-input等) │
│ 每个Worker并行处理自己的输入队列 │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Feature Arc(ip4-input等) │
│ 每个Worker并行处理自己的Feature Arc │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ NAT节点(nat44-ei-in2out) │
│ 每个Worker并行处理自己的数据包 │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 查找会话:根据源IP地址哈希选择目标Worker │
│ Worker选择逻辑:hash(src_ip) % num_workers │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │┤
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Handoff节点(nat44-ei-in2out-worker-handoff) │
│ 判断:会话是否在当前Worker? │
│ - 是:继续在当前Worker处理 │
│ - 否:Handoff到目标Worker │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
├─────────────────┴─────────────────┴─────────────────┤
│ │
│ 跨Worker Handoff(Frame Queue) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │Worker 0 │ │Worker 1 │ │Worker 2 │ │Worker 3 │
│ │Frame Queue│ │Frame Queue│ │Frame Queue│ │Frame Queue│
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │ │
│ │ │ │ │
└───────┼──────────────┼──────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ 目标Worker从Frame Queue取出数据包 │
│ 数据包被放入NAT节点(nat44-ei-in2out)的Frame │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ NAT节点(nat44-ei-in2out)在目标Worker上处理 │
│ - 查找会话:找到(会话在目标Worker) │
│ - 执行NAT转换:源IP/端口 → 外部IP/端口 │
│ - 恢复Feature Arc的next节点(从arc_next恢复) │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Feature Arc继续处理(ip4-lookup等) │
│ 转发到输出接口 │
└─────────────────────────────────────────────────────┘
关键点说明:
1. 多个Worker并行处理:每个Worker有独立的流水线
2. 会话亲和性:根据源IP地址哈希选择Worker
3. Handoff机制:会话不在当前Worker时,通过Frame Queue转送到目标Worker
4. 无锁设计:Frame Queue使用原子操作,避免锁竞争
6.6.2 OUT2IN方向数据包流转图
完整流程图:从Rx接收队列到NAT节点,通过Handoff相互流转
┌─────────────────────────────────────────────────────────────────────────┐
│ OUT2IN方向数据包流转图 │
└─────────────────────────────────────────────────────────────────────────┘
Rx接收队列(多个接口并行接收)
│
├─────────────────┬─────────────────┬─────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Worker 0 │ │Worker 1 │ │Worker 2 │ │Worker 3 │
│流水线 │ │流水线 │ │流水线 │ │流水线 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Input节点(dpdk-input等) │
│ 每个Worker并行处理自己的输入队列 │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Feature Arc(ip4-input等) │
│ 每个Worker并行处理自己的Feature Arc │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ NAT节点(nat44-ei-out2in) │
│ 每个Worker并行处理自己的数据包 │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 查找会话:根据目标端口选择目标Worker │
│ Worker选择逻辑: │
│ 1. 静态映射优先:静态映射指定的Worker │
│ 2. 端口范围分配:(dst_port - 1024) / port_per_thread │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Handoff节点(nat44-ei-out2in-worker-handoff) │
│ 判断:会话是否在当前Worker? │
│ - 是:继续在当前Worker处理 │
│ - 否:Handoff到目标Worker │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ │ │
├─────────────────┴─────────────────┴─────────────────┤
│ │
│ 跨Worker Handoff(Frame Queue) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │Worker 0 │ │Worker 1 │ │Worker 2 │ │Worker 3 │
│ │Frame Queue│ │Frame Queue│ │Frame Queue│ │Frame Queue│
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │ │
│ │ │ │ │
└───────┼──────────────┼──────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ 目标Worker从Frame Queue取出数据包 │
│ 数据包被放入NAT节点(nat44-ei-out2in)的Frame │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ NAT节点(nat44-ei-out2in)在目标Worker上处理 │
│ - 查找会话:找到(会话在目标Worker) │
│ - 执行NAT转换:外部IP/端口 → 内部IP/端口 │
│ - 恢复Feature Arc的next节点(从arc_next恢复) │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Feature Arc继续处理(ip4-lookup等) │
│ 转发到输出接口 │
└─────────────────────────────────────────────────────┘
关键点说明:
1. 多个Worker并行处理:每个Worker有独立的流水线
2. 会话亲和性:根据目标端口选择Worker(静态映射优先)
3. Handoff机制:会话不在当前Worker时,通过Frame Queue转送到目标Worker
4. 无锁设计:Frame Queue使用原子操作,避免锁竞争
5. 静态映射优先级:静态映射指定的Worker优先于端口范围分配
6.5.3 拥塞控制
生活化理解:
如果转送箱满了,丢弃新的业务:
- 检查队列长度 :
tail - head >= nelts - 丢弃策略 :如果
drop_on_congestion为1,直接丢弃 - 统计计数:记录丢弃的数据包数量
代码实现:
c
if (fq->tail - fq->head >= fq->nelts)
{
if (drop_on_congestion)
{
vlib_buffer_free (vm, node, buffer_indices[i], 1);
vlib_node_increment_counter (vm, node->node_index,
NAT44_EI_HANDOFF_ERROR_CONGESTION_DROP, 1);
}
}
6.6 总结
Handoff机制的核心价值:
- 会话亲和性:确保同一会话的数据包在同一Worker上处理
- 无锁设计:使用Frame Queue避免锁竞争,提高性能
- 负载均衡:通过哈希算法实现负载均衡
- 拥塞控制:检测并处理Frame Queue拥塞
Frame Queue机制的核心价值:
- 跨线程通信:实现无锁的跨线程数据包传输
- 批量处理:一次处理多个数据包,提高效率
- 性能优化:使用原子操作和缓存行对齐,避免false sharing
Worker线程运行流程:
- 主循环:检查Frame Queue → 处理Input节点 → 处理Pending Frames
- Handoff流程:确定目标Worker → 放入Frame Queue → 目标Worker取出处理
- 数据包流程:Input节点 → Feature Arc → NAT节点 → Handoff节点 → Frame Queue → 目标Worker → NAT节点 → Feature Arc继续
第七章:高可用性支持 - nat44_ei_ha.c / nat44_ei_ha.h
7.1 生活化理解:什么是NAT高可用性?
想象一下:银行的主备系统
假设银行有两个分行(两个NAT节点):
- 主分行(Active节点):正常营业,处理所有业务
- 备分行(Passive节点):不营业,但同步所有客户档案(会话)
当主分行故障时:
- 备分行立即接管,继续处理业务
- 因为备分行有所有客户档案,业务可以无缝继续
NAT HA的作用:
- Active节点:处理所有NAT流量,创建和管理会话
- Passive节点:接收会话同步,准备在Active节点故障时接管
- 会话同步:Active节点实时将会话变化同步到Passive节点
7.2 HA架构和拓扑
7.2.1 Active-Passive模式
生活化理解:
就像银行的主备系统:
- Active节点:正常营业,处理所有业务
- Passive节点:待命状态,同步所有档案,准备接管
拓扑结构(根据官方文档):
+-----------------------+
| outside network |
+-----------------------+
/ \
/ \
/ \
/ \
/ \
+---------+ +---------+
| GE0/0/1 | Active Passive | GE0/0/1 |
| | | |
| GE0/0/3|-------------------|GE0/0/3 |
| | sync network | |
| GE0/0/0 | | GE0/0/0 |
+---------+ +---------+
\ /
\ /
\ /
\ /
\ /
+-----------------------+
| inside network |
+-----------------------+
关键点:
- GE0/0/0:内部接口(inside)
- GE0/0/1:外部接口(outside)
- GE0/0/3:同步接口(sync network),用于会话同步
7.2.2 配置示例
Active节点配置(根据官方文档):
bash
# 配置接口
set interface ip address GigabitEthernet0/0/1 10.15.7.101/24 # 外部接口
set interface ip address GigabitEthernet0/0/0 172.16.10.101/24 # 内部接口
set interface ip address GigabitEthernet0/0/3 10.0.0.1/24 # 同步接口
set interface state GigabitEthernet0/0/0 up
set interface state GigabitEthernet0/0/1 up
set interface state GigabitEthernet0/0/3 up
# 配置NAT
set interface nat44 in GigabitEthernet0/0/0 out GigabitEthernet0/0/1
nat44 add address 10.15.7.100
# 配置HA
nat ha listener 10.0.0.1:1234 # 本地监听地址和端口
nat ha failover 10.0.0.2:2345 # 故障转移目标(Passive节点)
Passive节点配置:
bash
# 配置接口
set interface ip address GigabitEthernet0/0/1 10.15.7.102/24
set interface ip address GigabitEthernet0/0/0 172.16.10.102/24
set interface ip address GigabitEthernet0/0/3 10.0.0.2/24
set interface state GigabitEthernet0/0/0 up
set interface state GigabitEthernet0/0/1 up
set interface state GigabitEthernet0/0/3 up
# 配置NAT
set interface nat44 in GigabitEthernet0/0/0 out GigabitEthernet0/0/1
nat44 add address 10.15.7.100
# 配置HA(只配置监听器,不配置failover)
nat ha listener 10.0.0.2:2345 # 本地监听地址和端口
7.3 NAT HA协议详解
7.3.1 协议概述
生活化理解:
就像银行之间的"档案同步协议":
- 传输方式:UDP协议(快速,但不可靠)
- 可靠性保证:使用ACK确认和重传机制
- 同步内容:会话的创建、删除、统计更新
协议特点(根据官方文档):
- UDP传输:会话同步流量通过IPv4 UDP连接传输
- ACK确认:Passive节点收到数据后必须回复ACK
- 重传机制:Active节点记录每个发送的包,维护定时器
- 超时重传:如果定时器超时前没收到ACK,重传该包
7.3.2 协议消息格式
消息头结构(src/plugins/nat/nat44-ei/nat44_ei_ha.c 第56-69行):
c
typedef struct
{
u8 version; // 协议版本(NAT_HA_VERSION = 0x01)
u8 flags; // 标志位(NAT_HA_FLAG_ACK = 0x01)
u16 count; // 事件数量
u32 sequence_number; // 序列号
u32 thread_index; // 线程索引(事件来源的线程)
} __attribute__ ((packed)) nat_ha_message_header_t;
生活化解释:
- version:协议版本号,用于兼容性检查
- flags :标志位,ACK消息设置
NAT_HA_FLAG_ACK - count:消息中包含多少个事件(0表示纯ACK消息)
- sequence_number:序列号,用于ACK确认和重传
- thread_index:事件来源的Worker线程索引
事件数据结构(第71-90行):
c
typedef struct
{
u8 event_type; // 事件类型(ADD/DEL/REFRESH)
u8 protocol; // 协议类型(TCP/UDP/ICMP)
u16 flags; // 会话标志
u32 in_addr; // 内部IP地址
u32 out_addr; // 外部IP地址
u16 in_port; // 内部端口
u16 out_port; // 外部端口
u32 eh_addr; // 外部主机IP地址
u32 ehn_addr; // 外部主机NAT IP地址
u16 eh_port; // 外部主机端口
u16 ehn_port; // 外部主机NAT端口
u32 fib_index; // FIB索引
u32 total_pkts; // 总数据包数
u64 total_bytes; // 总字节数
} __attribute__ ((packed)) nat_ha_event_t;
生活化解释:
- event_type:事件类型(1=ADD,2=DEL,3=REFRESH)
- 协议和地址信息:完整的会话信息
- 统计信息:数据包数和字节数(用于REFRESH事件)
7.3.3 事件类型
三种事件类型:
-
NAT_HA_ADD(会话添加)
- 当Active节点创建新会话时发送
- Passive节点收到后创建相同的会话
-
NAT_HA_DEL(会话删除)
- 当Active节点删除会话时发送
- Passive节点收到后删除对应的会话
-
NAT_HA_REFRESH(会话刷新)
- 定期发送,更新会话统计信息
- 保持Passive节点的统计信息与Active节点一致
7.4 会话同步流程详解
7.4.1 发送端:Active节点发送会话事件
生活化理解:
就像主分行实时向备分行发送"档案变更通知":
- 创建新档案(ADD事件)
- 删除旧档案(DEL事件)
- 更新档案统计(REFRESH事件)
步骤1:创建会话时发送ADD事件
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第494行):
c
// 在slow_path中创建会话后
nat_ha_sadd (&s->in2out.addr, s->in2out.port, &s->out2in.addr,
s->out2in.port, &s->ext_host_addr, s->ext_host_port,
&s->ext_host_nat_addr, s->ext_host_nat_port,
s->nat_proto, s->in2out.fib_index, s->flags, thread_index, 0);
步骤2:nat_ha_sadd函数构造事件
代码实现(src/plugins/nat/nat44-ei/nat44_ei_ha.c 第833-857行):
c
void
nat_ha_sadd (ip4_address_t * in_addr, u16 in_port, ip4_address_t * out_addr,
u16 out_port, ip4_address_t * eh_addr, u16 eh_port,
ip4_address_t * ehn_addr, u16 ehn_port, u8 proto, u32 fib_index,
u16 flags, u32 thread_index, u8 is_resync)
{
nat_ha_event_t event;
skip_if_disabled (); // 如果HA未启用,直接返回
// 构造ADD事件
clib_memset (&event, 0, sizeof (event));
event.event_type = NAT_HA_ADD;
event.flags = clib_host_to_net_u16 (flags);
event.in_addr = in_addr->as_u32;
event.in_port = in_port;
event.out_addr = out_addr->as_u32;
event.out_port = out_port;
event.eh_addr = eh_addr->as_u32;
event.eh_port = eh_port;
event.ehn_addr = ehn_addr->as_u32;
event.ehn_port = ehn_port;
event.fib_index = clib_host_to_net_u32 (fib_index);
event.protocol = proto;
// 添加到事件队列
nat_ha_event_add (&event, 0, thread_index, is_resync);
}
步骤3:nat_ha_event_add函数将事件添加到缓冲区
代码实现(第724-830行):
c
static_always_inline void
nat_ha_event_add (nat_ha_event_t *event, u8 do_flush, u32 session_thread_index,
u8 is_resync)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
u32 vlib_thread_index = vlib_get_thread_index ();
nat_ha_per_thread_data_t *td = &ha->per_thread_data[vlib_thread_index];
vlib_main_t *vm = vlib_get_main_by_index (vlib_thread_index);
vlib_buffer_t *b = 0;
vlib_frame_t *f;
u32 bi = ~0, offset;
// 1. 获取或创建状态同步缓冲区
b = td->state_sync_buffer;
if (PREDICT_FALSE (b == 0))
{
if (do_flush)
return;
// 分配新缓冲区
if (vlib_buffer_alloc (vm, &bi, 1) != 1)
{
nat_elog_warn (nm, "HA NAT state sync can't allocate buffer");
return;
}
b = td->state_sync_buffer = vlib_get_buffer (vm, bi);
clib_memset (vnet_buffer (b), 0, sizeof (*vnet_buffer (b)));
offset = 0;
}
else
{
bi = vlib_get_buffer_index (vm, b);
offset = td->state_sync_next_event_offset;
}
// 2. 获取或创建Frame
f = td->state_sync_frame;
if (PREDICT_FALSE (f == 0))
{
u32 *to_next;
f = vlib_get_frame_to_node (vm, ip4_lookup_node.index);
td->state_sync_frame = f;
to_next = vlib_frame_vector_args (f);
to_next[0] = bi;
f->n_vectors = 1;
}
// 3. 如果是第一个事件,创建消息头
if (PREDICT_FALSE (td->state_sync_count == 0))
nat_ha_header_create (b, &offset, session_thread_index);
// 4. 添加事件到缓冲区
if (PREDICT_TRUE (do_flush == 0))
{
// 检查缓冲区空间是否足够
if (offset + sizeof (nat_ha_event_t) > b->buffer_size)
{
// 缓冲区满了,先发送当前缓冲区
nat_ha_event_add (0, 1, session_thread_index, is_resync);
// 重新开始
return nat_ha_event_add (event, 0, session_thread_index, is_resync);
}
// 复制事件数据到缓冲区
clib_memcpy ((u8 *) vlib_buffer_get_current (b) + offset, event,
sizeof (nat_ha_event_t));
offset += sizeof (nat_ha_event_t);
td->state_sync_count++;
td->state_sync_next_event_offset = offset;
}
else
{
// 5. 刷新缓冲区(发送)
if (td->state_sync_count > 0)
{
nat_ha_send (f, b, is_resync, vlib_thread_index);
td->state_sync_buffer = 0;
td->state_sync_frame = 0;
td->state_sync_count = 0;
td->state_sync_next_event_offset = 0;
}
}
}
生活化解释:
- 缓冲区管理:每个Worker线程有一个缓冲区,累积多个事件
- 批量发送:多个事件打包成一个UDP包发送,提高效率
- 空间检查:如果缓冲区满了,先发送,再继续添加
步骤4:nat_ha_send函数发送消息
代码实现(第696-721行):
c
static inline void
nat_ha_send (vlib_frame_t *f, vlib_buffer_t *b, u8 is_resync,
u32 vlib_thread_index)
{
nat_ha_main_t *ha = &nat_ha_main;
nat_ha_per_thread_data_t *td = &ha->per_thread_data[vlib_thread_index];
nat_ha_message_header_t *h;
ip4_header_t *ip;
udp_header_t *udp;
vlib_main_t *vm = vlib_get_main_by_index (vlib_thread_index);
// 1. 获取IP和UDP头
ip = vlib_buffer_get_current (b);
udp = ip4_next_header (ip);
h = (nat_ha_message_header_t *) (udp + 1);
// 2. 设置消息头
h->count = clib_host_to_net_u16 (td->state_sync_count);
// 3. 设置IP和UDP头
ip->length = clib_host_to_net_u16 (b->current_length);
ip->checksum = ip4_header_checksum (ip);
udp->length = clib_host_to_net_u16 (b->current_length - sizeof (*ip));
// 4. 添加到重传队列(等待ACK)
nat_ha_resend_queue_add (vm, h->sequence_number, (u8 *) ip,
b->current_length, is_resync, vlib_thread_index);
// 5. 发送到IP查找节点
vlib_put_frame_to_node (vm, ip4_lookup_node.index, f);
}
关键点:
- 序列号:每个消息有唯一的序列号,用于ACK确认
- 重传队列:发送后立即加入重传队列,等待ACK
- IP路由:通过VPP的IP路由系统发送UDP包
7.4.2 接收端:Passive节点处理会话事件
生活化理解:
就像备分行接收"档案变更通知"并更新档案:
- 接收UDP消息
- 解析事件
- 更新本地会话表
- 回复ACK确认
步骤1:UDP消息到达Passive节点
代码实现(src/plugins/nat/nat44-ei/nat44_ei_ha.c 第494-527行):
c
int
nat_ha_set_listener (vlib_main_t *vm, ip4_address_t *addr, u16 port,
u32 path_mtu)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
// 1. 取消注册旧的UDP端口
if (ha->src_port)
udp_unregister_dst_port (vm, ha->src_port, 1);
// 2. 设置监听地址和端口
ha->src_ip_address.as_u32 = addr->as_u32;
ha->src_port = port;
ha->state_sync_path_mtu = path_mtu;
if (port)
{
// 3. 注册UDP端口(多Worker时先到handoff节点)
if (ha->num_workers > 1)
{
if (ha->fq_index == ~0)
ha->fq_index = vlib_frame_queue_main_init (ha->ha_node_index, 0);
udp_register_dst_port (vm, port, ha->ha_handoff_node_index, 1);
}
else
{
udp_register_dst_port (vm, port, ha->ha_node_index, 1);
}
nat_elog_info_X1 (nm, "HA listening on port %d for state sync", "i4",
port);
}
return 0;
}
生活化解释:
- UDP注册:注册UDP端口,当有消息到达时,路由到HA节点
- 多Worker支持:如果有多个Worker,先到handoff节点,再分发到正确的Worker
步骤2:HA节点处理消息
代码实现(第1030-1150行):
c
static uword
nat_ha_node_fn (vlib_main_t * vm, vlib_node_runtime_t * node,
vlib_frame_t * frame)
{
u32 n_left_from, *from, next_index, *to_next;
f64 now = vlib_time_now (vm);
u32 thread_index = vm->thread_index;
u32 pkts_processed = 0;
ip4_header_t *ip0;
udp_header_t *udp0;
nat_ha_message_header_t *h0;
nat_ha_event_t *e0;
u16 event_count0;
from = vlib_frame_vector_args (frame);
n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
// 1. 获取数据包
b0 = vlib_get_buffer (vm, from[0]);
h0 = vlib_buffer_get_current (b0);
vlib_buffer_advance (b0, -sizeof (*udp0));
udp0 = vlib_buffer_get_current (b0);
vlib_buffer_advance (b0, -sizeof (*ip0));
ip0 = vlib_buffer_get_current (b0);
// 2. 检查协议版本
if (h0->version != NAT_HA_VERSION)
{
b0->error = node->errors[NAT_HA_ERROR_BAD_VERSION];
goto done0;
}
event_count0 = clib_net_to_host_u16 (h0->count);
// 3. 处理ACK消息(纯ACK,没有事件)
if (!event_count0 && (h0->flags & NAT_HA_FLAG_ACK))
{
nat_ha_ack_recv (h0->sequence_number, thread_index);
b0->error = node->errors[NAT_HA_ERROR_PROCESSED];
goto done0;
}
// 4. 处理事件消息
e0 = (nat_ha_event_t *) (h0 + 1);
/* 处理每个事件 */
while (event_count0)
{
nat_ha_event_process (e0, now, thread_index);
event_count0--;
e0 = (nat_ha_event_t *) ((u8 *) e0 + sizeof (nat_ha_event_t));
}
// 5. 回复ACK
b0->current_length = sizeof (*ip0) + sizeof (*udp0) + sizeof (*h0);
// 交换源地址和目标地址
src_addr0 = ip0->src_address.data_u32;
dst_addr0 = ip0->dst_address.data_u32;
ip0->src_address.data_u32 = dst_addr0;
ip0->dst_address.data_u32 = src_addr0;
// 交换源端口和目标端口
src_port0 = udp0->src_port;
dst_port0 = udp0->dst_port;
udp0->src_port = dst_port0;
udp0->dst_port = src_port0;
// 设置ACK标志
h0->flags = NAT_HA_FLAG_ACK;
h0->count = 0;
// 更新IP和UDP头
ip0->length = clib_host_to_net_u16 (b0->current_length);
ip0->checksum = ip4_header_checksum (ip0);
udp0->length = clib_host_to_net_u16 (b0->current_length - sizeof (*ip0));
udp0->checksum = 0;
// 发送ACK
vlib_put_frame_to_node (vm, NAT_HA_NEXT_IP4_LOOKUP, ...);
}
}
关键点:
- 版本检查:检查协议版本,确保兼容性
- ACK处理:如果是纯ACK消息,直接处理ACK
- 事件处理:遍历所有事件,逐个处理
- ACK回复:处理完事件后,立即回复ACK
步骤3:处理具体事件
代码实现(第628-673行):
c
static_always_inline void
nat_ha_event_process (nat_ha_event_t * event, f64 now, u32 thread_index)
{
switch (event->event_type)
{
case NAT_HA_ADD:
nat_ha_recv_add (event, now, thread_index);
break;
case NAT_HA_DEL:
nat_ha_recv_del (event, thread_index);
break;
case NAT_HA_REFRESH:
nat_ha_recv_refresh (event, now, thread_index);
break;
}
}
ADD事件处理(第565-586行):
c
static_always_inline void
nat_ha_recv_add (nat_ha_event_t * event, f64 now, u32 thread_index)
{
nat_ha_main_t *ha = &nat_ha_main;
ip4_address_t in_addr, out_addr, eh_addr, ehn_addr;
u32 fib_index;
u16 flags;
vlib_increment_simple_counter (&ha->counters[NAT_HA_COUNTER_RECV_ADD],
thread_index, 0, 1);
// 提取事件数据
in_addr.as_u32 = event->in_addr;
out_addr.as_u32 = event->out_addr;
eh_addr.as_u32 = event->eh_addr;
ehn_addr.as_u32 = event->ehn_addr;
fib_index = clib_net_to_host_u32 (event->fib_index);
flags = clib_net_to_host_u16 (event->flags);
// 调用回调函数创建会话
nat44_ei_ha_sadd (&in_addr, event->in_port, &out_addr, event->out_port,
&eh_addr, event->eh_port, &ehn_addr, event->ehn_port,
event->protocol, fib_index, flags, thread_index);
}
生活化解释:
- 提取数据:从事件中提取会话信息
- 创建会话 :调用
nat44_ei_ha_sadd在Passive节点创建相同的会话 - 统计计数:更新接收ADD事件的统计
nat44_ei_ha_sadd函数(src/plugins/nat/nat44-ei/nat44_ei_ha.c 第173-252行):
c
static_always_inline void
nat44_ei_ha_sadd (ip4_address_t *in_addr, u16 in_port, ip4_address_t *out_addr,
u16 out_port, ip4_address_t *eh_addr, u16 eh_port,
ip4_address_t *ehn_addr, u16 ehn_port, u8 proto,
u32 fib_index, u16 flags, u32 thread_index)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat44_ei_main_per_thread_data_t *tnm = &nm->per_thread_data[thread_index];
nat44_ei_user_t *u;
nat44_ei_session_t *s;
clib_bihash_kv_8_8_t kv;
vlib_main_t *vm = vlib_get_main ();
f64 now = vlib_time_now (vm);
// 1. 如果不是静态映射,标记外部地址和端口为已使用
if (!(flags & NAT44_EI_SESSION_FLAG_STATIC_MAPPING))
{
if (nat44_ei_set_outside_address_and_port (nm->addresses, thread_index,
*out_addr, out_port, proto))
return;
}
// 2. 查找或创建用户
u = nat44_ei_user_get_or_create (nm, in_addr, fib_index, thread_index);
if (!u)
return;
// 3. 分配会话结构
s = nat44_ei_session_alloc_or_recycle (nm, u, thread_index, now);
if (!s)
return;
// 4. 设置会话信息
s->out2in.addr.as_u32 = out_addr->as_u32;
s->out2in.port = out_port;
s->nat_proto = proto;
s->last_heard = now;
s->flags = flags;
s->ext_host_addr.as_u32 = eh_addr->as_u32;
s->ext_host_port = eh_port;
// 5. 确定外部FIB索引
// ...(FIB查找逻辑)
// 6. 插入哈希表
init_nat_o2i_kv (&kv, s, thread_index, s - tnm->sessions);
clib_bihash_add_del_8_8 (&nm->out2in, &kv, 1);
s->in2out.addr.as_u32 = in_addr->as_u32;
s->in2out.port = in_port;
s->in2out.fib_index = fib_index;
init_nat_i2o_kv (&kv, s, thread_index, s - tnm->sessions);
clib_bihash_add_del_8_8 (&nm->in2out, &kv, 1);
}
生活化解释:
- 完整复制:Passive节点创建与Active节点完全相同的会话
- 哈希表插入:同时插入in2out和out2in两个哈希表
- 地址管理:标记外部地址和端口为已使用,避免冲突
7.5 ACK确认和重传机制
7.5.1 ACK确认机制
生活化理解:
就像快递的"签收确认":
- 发送方(Active):发送包裹(UDP消息),等待签收(ACK)
- 接收方(Passive):收到包裹,立即签收(回复ACK)
ACK处理流程:
步骤1:Passive节点收到消息后回复ACK
代码实现(第1107-1138行):
c
// 处理完所有事件后,回复ACK
b0->current_length = sizeof (*ip0) + sizeof (*udp0) + sizeof (*h0);
// 交换源地址和目标地址(回复给发送方)
src_addr0 = ip0->src_address.data_u32;
dst_addr0 = ip0->dst_address.data_u32;
ip0->src_address.data_u32 = dst_addr0;
ip0->dst_address.data_u32 = src_addr0;
// 交换源端口和目标端口
src_port0 = udp0->src_port;
dst_port0 = udp0->dst_port;
udp0->src_port = dst_port0;
udp0->dst_port = src_port0;
// 设置ACK标志
h0->flags = NAT_HA_FLAG_ACK;
h0->count = 0; // ACK消息没有事件
// 更新IP和UDP头
ip0->length = clib_host_to_net_u16 (b0->current_length);
ip0->checksum = ip4_header_checksum (ip0);
udp0->length = clib_host_to_net_u16 (b0->current_length - sizeof (*ip0));
udp0->checksum = 0;
// 发送ACK
vlib_put_frame_to_node (vm, NAT_HA_NEXT_IP4_LOOKUP, ...);
步骤2:Active节点接收ACK
代码实现(第339-367行):
c
static_always_inline void
nat_ha_ack_recv (u32 seq, u32 thread_index)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
nat_ha_per_thread_data_t *td = &ha->per_thread_data[thread_index];
u32 i;
// 1. 在重传队列中查找对应的序列号
vec_foreach_index (i, td->resend_queue)
{
if (td->resend_queue[i].seq != seq)
continue;
// 2. 找到对应的消息,更新统计
vlib_increment_simple_counter (&ha->counters[NAT_HA_COUNTER_RECV_ACK],
thread_index, 0, 1);
// 3. 如果是Resync消息,更新Resync计数
if (td->resend_queue[i].is_resync)
{
clib_atomic_fetch_sub (&ha->resync_ack_count, 1);
nat_ha_resync_fin ();
}
// 4. 从重传队列中删除(ACK已收到)
vec_free (td->resend_queue[i].data);
vec_del1 (td->resend_queue, i);
nat_elog_debug_X1 (nm, "ACK for seq %d received", "i4",
clib_net_to_host_u32 (seq));
return;
}
}
生活化解释:
- 查找序列号:在重传队列中查找对应的序列号
- 删除缓存:ACK收到后,从重传队列中删除,释放内存
- Resync处理:如果是Resync消息,更新Resync计数
7.5.2 重传机制
生活化理解:
就像快递的"重发机制":
- 发送包裹:发送后开始计时(2秒)
- 等待签收:如果2秒内没收到签收,重发
- 最多重试:最多重试3次,如果3次都没收到,放弃
重传队列结构(第100-113行):
c
typedef struct
{
u32 seq; // 序列号
u32 retry_count; // 重试次数
f64 retry_timer; // 下次重试时间
u8 is_resync; // 是否是Resync消息
u8 *data; // 消息数据(用于重传)
} nat_ha_resend_entry_t;
重传扫描函数(第370-440行):
c
static void
nat_ha_resend_scan (vlib_main_t *vm, u32 thread_index)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
nat_ha_per_thread_data_t *td = &ha->per_thread_data[thread_index];
u32 i, *del, *to_delete = 0;
vlib_buffer_t *b = 0;
vlib_frame_t *f;
u32 bi, *to_next;
ip4_header_t *ip;
f64 now = vlib_time_now (vm);
// 遍历重传队列
vec_foreach_index (i, td->resend_queue)
{
// 1. 检查是否到了重试时间
if (td->resend_queue[i].retry_timer > now)
continue;
// 2. 检查是否超过最大重试次数(NAT_HA_RETRIES = 3)
if (td->resend_queue[i].retry_count >= NAT_HA_RETRIES)
{
nat_elog_notice_X1 (nm, "seq %d missed", "i4",
clib_net_to_host_u32 (td->resend_queue[i].seq));
// 如果是Resync消息,更新Resync计数
if (td->resend_queue[i].is_resync)
{
clib_atomic_fetch_add (&ha->resync_ack_missed, 1);
clib_atomic_fetch_sub (&ha->resync_ack_count, 1);
nat_ha_resync_fin ();
}
// 标记为删除
vec_add1 (to_delete, i);
vlib_increment_simple_counter (&ha->counters
[NAT_HA_COUNTER_MISSED_COUNT],
thread_index, 0, 1);
continue;
}
// 3. 重传消息
nat_elog_debug_X1 (nm, "state sync seq %d resend", "i4",
clib_net_to_host_u32 (td->resend_queue[i].seq));
td->resend_queue[i].retry_count++;
vlib_increment_simple_counter (&ha->counters[NAT_HA_COUNTER_RETRY_COUNT],
thread_index, 0, 1);
// 4. 分配缓冲区,复制消息数据
if (vlib_buffer_alloc (vm, &bi, 1) != 1)
{
nat_elog_warn (nm, "HA NAT state sync can't allocate buffer");
return;
}
b = vlib_get_buffer (vm, bi);
b->current_length = vec_len (td->resend_queue[i].data);
b->flags |= VLIB_BUFFER_TOTAL_LENGTH_VALID;
b->flags |= VNET_BUFFER_F_LOCALLY_ORIGINATED;
ip = vlib_buffer_get_current (b);
clib_memcpy (ip, td->resend_queue[i].data,
vec_len (td->resend_queue[i].data));
// 5. 发送重传消息
f = vlib_get_frame_to_node (vm, ip4_lookup_node.index);
to_next = vlib_frame_vector_args (f);
to_next[0] = bi;
f->n_vectors = 1;
vlib_put_frame_to_node (vm, ip4_lookup_node.index, f);
// 6. 更新重试时间(2秒后再次重试)
td->resend_queue[i].retry_timer = now + 2.0;
}
// 7. 删除超过最大重试次数的条目
vec_foreach (del, to_delete)
{
vec_free (td->resend_queue[*del].data);
vec_del1 (td->resend_queue, *del);
}
vec_free (to_delete);
}
关键点:
- 重试时间 :初始重试时间为发送后2秒(
now + 2.0) - 最大重试次数 :
NAT_HA_RETRIES = 3,最多重试3次 - 数据缓存:重传队列中保存完整的消息数据,用于重传
- 定时扫描:Worker节点定期调用此函数扫描重传队列
Worker节点定期扫描(第912-927行):
c
static uword
nat_ha_worker_fn (vlib_main_t * vm, vlib_node_runtime_t * rt,
vlib_frame_t * f)
{
u32 thread_index = vm->thread_index;
if (plugin_enabled () == 0)
return 0;
// 1. 刷新当前正在构建的HA数据(发送缓冲区)
nat_ha_event_add (0, 1, thread_index, 0);
// 2. 扫描重传队列,重传未ACK的消息
nat_ha_resend_scan (vm, thread_index);
return 0;
}
生活化解释:
- 定期刷新:定期发送缓冲区中的数据(即使没满也发送)
- 重传扫描:检查重传队列,重传超时的消息
7.6 完整会话同步流程
7.6.1 会话创建流程
完整流程:
Active节点(Worker A):
1. 创建新会话(slow_path)
↓
2. 调用nat_ha_sadd
↓
3. nat_ha_event_add:添加到缓冲区
↓
4. Worker节点定期刷新:nat_ha_send
↓
5. 构造UDP消息(包含序列号)
↓
6. 添加到重传队列
↓
7. 通过IP路由发送UDP包
↓
[UDP传输]
↓
Passive节点:
8. UDP消息到达(nat_ha_node_fn)
↓
9. 解析消息头,提取事件
↓
10. nat_ha_event_process:处理每个事件
↓
11. nat_ha_recv_add:创建会话
↓
12. nat44_ei_ha_sadd:在Passive节点创建相同会话
↓
13. 回复ACK(交换源地址和目标地址)
↓
[ACK传输]
↓
Active节点:
14. 接收ACK(nat_ha_node_fn)
↓
15. nat_ha_ack_recv:从重传队列删除
↓
完成!
7.6.2 会话删除流程
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第1360行):
c
// 在删除会话时调用
nat_ha_sdel (&s->out2in.addr, s->out2in.port, &s->ext_host_addr,
s->ext_host_port, s->nat_proto, s->out2in.fib_index,
thread_index);
nat_ha_sdel函数(第859-876行):
c
void
nat_ha_sdel (ip4_address_t *out_addr, u16 out_port, ip4_address_t *eh_addr,
u16 eh_port, u8 proto, u32 fib_index, u32 session_thread_index)
{
nat_ha_event_t event;
skip_if_disabled ();
// 构造DEL事件
clib_memset (&event, 0, sizeof (event));
event.event_type = NAT_HA_DEL;
event.out_addr = out_addr->as_u32;
event.out_port = out_port;
event.eh_addr = eh_addr->as_u32;
event.eh_port = eh_port;
event.fib_index = clib_host_to_net_u32 (fib_index);
event.protocol = proto;
// 添加到事件队列
nat_ha_event_add (&event, 0, session_thread_index, 0);
}
Passive节点处理DEL事件(第588-604行):
c
static_always_inline void
nat_ha_recv_del (nat_ha_event_t * event, u32 thread_index)
{
nat_ha_main_t *ha = &nat_ha_main;
ip4_address_t out_addr, eh_addr;
u32 fib_index;
vlib_increment_simple_counter (&ha->counters[NAT_HA_COUNTER_RECV_DEL],
thread_index, 0, 1);
out_addr.as_u32 = event->out_addr;
eh_addr.as_u32 = event->eh_addr;
fib_index = clib_net_to_host_u32 (event->fib_index);
// 调用回调函数删除会话
nat44_ei_ha_sdel (&out_addr, event->out_port, &eh_addr, event->eh_port,
event->protocol, fib_index, thread_index);
}
nat44_ei_ha_sdel函数(第254-273行):
c
static_always_inline void
nat44_ei_ha_sdel (ip4_address_t *out_addr, u16 out_port,
ip4_address_t *eh_addr, u16 eh_port, u8 proto, u32 fib_index,
u32 thread_index)
{
nat44_ei_main_t *nm = &nat44_ei_main;
clib_bihash_kv_8_8_t kv, value;
nat44_ei_session_t *s;
nat44_ei_main_per_thread_data_t *tnm;
// 1. 查找会话
init_nat_k (&kv, *out_addr, out_port, fib_index, proto);
if (clib_bihash_search_8_8 (&nm->out2in, &kv, &value))
return; // 找不到会话
// 2. 获取会话
ASSERT (thread_index == nat_value_get_thread_index (&value));
tnm = vec_elt_at_index (nm->per_thread_data, thread_index);
s = pool_elt_at_index (tnm->sessions, nat_value_get_session_index (&value));
// 3. 释放会话数据
nat44_ei_free_session_data_v2 (nm, s, thread_index, 1);
// 4. 删除会话
nat44_ei_delete_session (nm, s, thread_index);
}
7.6.3 会话刷新流程
生活化理解:
就像定期更新"档案统计":
- 定期发送 :每隔一定时间(
session_refresh_interval)发送统计更新 - 更新统计:更新数据包数和字节数,保持Passive节点与Active节点一致
代码实现(第878-903行):
c
void
nat_ha_sref (ip4_address_t * out_addr, u16 out_port,
ip4_address_t * eh_addr, u16 eh_port, u8 proto, u32 fib_index,
u32 total_pkts, u64 total_bytes, u32 thread_index,
f64 * last_refreshed, f64 now)
{
nat_ha_main_t *ha = &nat_ha_main;
nat_ha_event_t event;
skip_if_disabled ();
// 1. 检查是否到了刷新时间
if ((*last_refreshed + ha->session_refresh_interval) > now)
return; // 还没到刷新时间
// 2. 更新最后刷新时间
*last_refreshed = now;
// 3. 构造REFRESH事件
clib_memset (&event, 0, sizeof (event));
event.event_type = NAT_HA_REFRESH;
event.out_addr = out_addr->as_u32;
event.out_port = out_port;
event.eh_addr = eh_addr->as_u32;
event.eh_port = eh_port;
event.fib_index = clib_host_to_net_u32 (fib_index);
event.protocol = proto;
event.total_pkts = clib_host_to_net_u32 (total_pkts);
event.total_bytes = clib_host_to_net_u64 (total_bytes);
// 4. 添加到事件队列
nat_ha_event_add (&event, 0, thread_index, 0);
}
调用时机(src/plugins/nat/nat44-ei/nat44_ei_inlines.h 第211-222行):
c
always_inline void
nat44_ei_session_update_counters (nat44_ei_session_t *s, f64 now, uword bytes,
u32 thread_index)
{
s->last_heard = now;
s->total_pkts++;
s->total_bytes += bytes;
// 同步到HA系统(会检查刷新间隔)
nat_ha_sref (&s->out2in.addr, s->out2in.port, &s->ext_host_addr,
s->ext_host_port, s->nat_proto, s->out2in.fib_index,
s->total_pkts, s->total_bytes, thread_index,
&s->ha_last_refreshed, now);
}
Passive节点处理REFRESH事件(第606-626行):
c
static_always_inline void
nat_ha_recv_refresh (nat_ha_event_t * event, f64 now, u32 thread_index)
{
nat_ha_main_t *ha = &nat_ha_main;
ip4_address_t out_addr, eh_addr;
u32 fib_index, total_pkts;
u64 total_bytes;
vlib_increment_simple_counter (&ha->counters[NAT_HA_COUNTER_RECV_REFRESH],
thread_index, 0, 1);
out_addr.as_u32 = event->out_addr;
eh_addr.as_u32 = event->eh_addr;
fib_index = clib_net_to_host_u32 (event->fib_index);
total_pkts = clib_net_to_host_u32 (event->total_pkts);
total_bytes = clib_net_to_host_u64 (event->total_bytes);
// 调用回调函数更新统计
nat44_ei_ha_sref (&out_addr, event->out_port, &eh_addr, event->eh_port,
event->protocol, fib_index, total_pkts, total_bytes,
thread_index);
}
nat44_ei_ha_sref函数(第275-294行):
c
static_always_inline void
nat44_ei_ha_sref (ip4_address_t *out_addr, u16 out_port,
ip4_address_t *eh_addr, u16 eh_port, u8 proto, u32 fib_index,
u32 total_pkts, u64 total_bytes, u32 thread_index)
{
nat44_ei_main_t *nm = &nat44_ei_main;
clib_bihash_kv_8_8_t kv, value;
nat44_ei_session_t *s;
nat44_ei_main_per_thread_data_t *tnm;
tnm = vec_elt_at_index (nm->per_thread_data, thread_index);
// 1. 查找会话
init_nat_k (&kv, *out_addr, out_port, fib_index, proto);
if (clib_bihash_search_8_8 (&nm->out2in, &kv, &value))
return; // 找不到会话
// 2. 获取会话
s = pool_elt_at_index (tnm->sessions, nat_value_get_session_index (&value));
// 3. 更新统计信息
s->total_pkts = total_pkts;
s->total_bytes = total_bytes;
}
7.7 Resync(重新同步)机制
7.7.1 什么是Resync?
生活化理解:
就像备分行需要"重新同步所有档案":
- 场景:Passive节点刚启动,或者更换了新的Passive节点
- 需求:需要将Active节点的所有现有会话同步到Passive节点
- 方法:遍历所有会话,逐个发送ADD事件
7.7.2 Resync流程
代码实现(第564-693行):
c
int
nat_ha_resync (u32 client_index, u32 pid,
nat_ha_resync_event_cb_t event_callback)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
u32 thread_index;
nat44_ei_main_per_thread_data_t *tnm;
nat44_ei_session_t *s;
u32 si;
// 1. 检查HA是否启用
if (!ha->enabled)
return -1;
// 2. 检查是否已经在Resync中
if (ha->in_resync)
return -1;
// 3. 初始化Resync状态
ha->in_resync = 1;
ha->resync_ack_count = 0;
ha->resync_ack_missed = 0;
ha->event_callback = event_callback;
ha->client_index = client_index;
ha->pid = pid;
// 4. 遍历所有Worker线程
for (thread_index = 0; thread_index < vec_len (nm->per_thread_data);
thread_index++)
{
tnm = vec_elt_at_index (nm->per_thread_data, thread_index);
// 5. 遍历该线程的所有会话
pool_foreach (s, tnm->sessions, ({
// 6. 发送ADD事件(is_resync=1)
nat_ha_sadd (&s->in2out.addr, s->in2out.port,
&s->out2in.addr, s->out2in.port,
&s->ext_host_addr, s->ext_host_port,
&s->ext_host_nat_addr, s->ext_host_nat_port,
s->nat_proto, s->in2out.fib_index, s->flags,
thread_index, 1 /* is_resync */);
// 7. 增加Resync ACK计数
clib_atomic_fetch_add (&ha->resync_ack_count, 1);
}));
// 8. 刷新该线程的缓冲区
nat_ha_event_add (0, 1, thread_index, 1);
}
return 0;
}
生活化解释:
- 遍历所有会话:遍历所有Worker线程的所有会话
- 发送ADD事件:为每个会话发送ADD事件(标记为Resync)
- ACK计数:统计需要ACK的消息数量
- 完成检测:当所有ACK都收到后,Resync完成
Resync完成检测(第296-317行):
c
static void
nat_ha_resync_fin (void)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
// 1. 检查是否还有未ACK的消息
if (ha->resync_ack_count)
return; // 还有未ACK的消息,继续等待
// 2. Resync完成
ha->in_resync = 0;
// 3. 检查是否有丢失的消息
if (ha->resync_ack_missed)
{
nat_elog_info (nm, "resync completed with result FAILED");
}
else
{
nat_elog_info (nm, "resync completed with result SUCCESS");
}
// 4. 调用回调函数通知完成
if (ha->event_callback)
ha->event_callback (ha->client_index, ha->pid, ha->resync_ack_missed);
}
7.8 HA初始化和配置
7.8.1 HA初始化
代码实现(第472-492行):
c
void
nat_ha_init (vlib_main_t * vm, u32 num_workers, u32 num_threads)
{
nat_ha_main_t *ha = &nat_ha_main;
clib_memset (ha, 0, sizeof (*ha));
// 1. 设置节点索引
nat_ha_set_node_indexes (ha, vm);
// 2. 初始化Frame Queue索引
ha->fq_index = ~0;
// 3. 设置Worker数量
ha->num_workers = num_workers;
vec_validate (ha->per_thread_data, num_threads);
// 4. 初始化统计计数器
#define _(N, s, v) \
ha->counters[v].name = s; \
ha->counters[v].stat_segment_name = "/nat44-ei/ha/" s; \
vlib_validate_simple_counter (&ha->counters[v], 0); \
vlib_zero_simple_counter (&ha->counters[v], 0);
foreach_nat_ha_counter
#undef _
}
7.8.2 设置监听器和故障转移
设置监听器(第494-527行):
c
int
nat_ha_set_listener (vlib_main_t *vm, ip4_address_t *addr, u16 port,
u32 path_mtu)
{
nat44_ei_main_t *nm = &nat44_ei_main;
nat_ha_main_t *ha = &nat_ha_main;
// 1. 取消注册旧的UDP端口
if (ha->src_port)
udp_unregister_dst_port (vm, ha->src_port, 1);
// 2. 设置监听地址和端口
ha->src_ip_address.as_u32 = addr->as_u32;
ha->src_port = port;
ha->state_sync_path_mtu = path_mtu;
if (port)
{
// 3. 注册UDP端口
if (ha->num_workers > 1)
{
// 多Worker:先到handoff节点
if (ha->fq_index == ~0)
ha->fq_index = vlib_frame_queue_main_init (ha->ha_node_index, 0);
udp_register_dst_port (vm, port, ha->ha_handoff_node_index, 1);
}
else
{
// 单Worker:直接到HA节点
udp_register_dst_port (vm, port, ha->ha_node_index, 1);
}
}
return 0;
}
设置故障转移(第539-552行):
c
int
nat_ha_set_failover (vlib_main_t *vm, ip4_address_t *addr, u16 port,
u32 session_refresh_interval)
{
nat_ha_main_t *ha = &nat_ha_main;
// 1. 设置故障转移地址和端口
ha->dst_ip_address.as_u32 = addr->as_u32;
ha->dst_port = port;
ha->session_refresh_interval = session_refresh_interval;
// 2. 触发Process节点(启动定期刷新)
vlib_process_signal_event (vm, ha->ha_process_node_index, 1, 0);
return 0;
}
7.9 关键设计特点
7.9.1 可靠性保证
生活化理解:
虽然UDP不可靠,但通过ACK和重传机制保证可靠性:
- ACK确认:每个消息必须收到ACK
- 重传机制:如果2秒内没收到ACK,重传
- 最大重试:最多重试3次,避免无限重传
7.9.2 批量发送
生活化理解:
就像快递的"批量发货":
- 累积事件:多个事件累积在缓冲区
- 批量发送:缓冲区满了或定期刷新时,批量发送
- 提高效率:减少UDP包数量,提高网络效率
7.9.3 增量同步
生活化理解:
只同步变化的会话:
- ADD事件:只在新会话创建时发送
- DEL事件:只在会话删除时发送
- REFRESH事件:定期发送统计更新
- 减少开销:不重复发送未变化的会话
7.10 总结
NAT HA的核心价值:
- 高可用性:Active节点故障时,Passive节点可以无缝接管
- 会话一致性:Passive节点与Active节点保持会话一致
- 可靠性:通过ACK和重传机制保证消息可靠传输
- 性能优化:批量发送和增量同步,减少网络开销
完整流程总结:
- 配置阶段:Active和Passive节点配置HA监听器和故障转移
- 运行阶段:Active节点实时同步会话变化到Passive节点
- 故障切换:Active节点故障时,Passive节点接管(需要外部机制触发)
- Resync:新Passive节点启动时,可以触发Resync同步所有会话
第八章:DPO支持 - nat44_ei_dpo.c / nat44_ei_dpo.h
8.1 生活化理解:DPO模式 vs 普通模式
想象一下:快递的"快速通道"和"普通通道"
假设快递公司有两种处理方式:
- 普通通道(Feature Arc模式):包裹经过多个检查站(Feature节点),每个检查站都要检查和处理
- 快速通道(DPO模式):包裹直接送到目的地,跳过所有检查站
关键区别:
| 特性 | 普通模式(Feature Arc) | DPO模式 |
|---|---|---|
| 路径 | ip4-input → Feature Arc → classify → out2in → Feature Arc继续 | ip4-input → ip4-lookup → out2in |
| 节点数 | 5个节点 | 3个节点 |
| 分类开销 | 需要classify节点判断 | 不需要,FIB直接路由 |
| 性能 | 较慢 | 较快 |
| 灵活性 | 高,可动态配置 | 低,需要预先配置 |
| 适用场景 | 需要灵活配置 | 性能要求高 |
生活化比喻:
- 普通模式:就像"普通快递",包裹要经过多个中转站,每个站都要检查
- DPO模式:就像"直达快递",包裹直接送到目的地,跳过所有中转站
8.2 关键分支点:ip4-lookup节点的路由决策
8.2.1 分支判断的位置
关键点:数据包在ip4-lookup节点进行分支判断
当数据包到达ip4-lookup节点时,会进行FIB查找:
- 如果FIB条目使用NAT DPO :直接转发到
nat44-ei-out2in节点(DPO模式) - 如果FIB条目没有NAT DPO:继续Feature Arc处理(普通模式)
代码位置(src/vnet/ip/ip4_forward.h 第327-354行):
c
// ip4-lookup节点进行FIB查找
lbi0 = ip4_fib_forwarding_lookup (vnet_buffer (b[0])->ip.fib_index, dst_addr0);
lb0 = load_balance_get (lbi0);
// 从Load Balance中获取DPO
dpo0 = load_balance_get_bucket_i (lb0, 0);
// 关键:根据DPO的next_node转发
next[0] = dpo0->dpoi_next_node; // 如果是NAT DPO,这里指向nat44-ei-out2in节点
vnet_buffer (b[0])->ip.adj_index[VLIB_TX] = dpo0->dpoi_index;
生活化理解:
就像"快递分拣中心":
1. 包裹到达分拣中心(ip4-lookup节点)
2. 查看地址簿(FIB查找)
3. 如果地址簿标记了"直达通道"(NAT DPO):
- 直接送到目的地(nat44-ei-out2in节点)
4. 如果地址簿没有标记:
- 走普通通道(Feature Arc)
8.2.2 完整的分支流程图
OUT2IN方向数据包的分支流程:
┌─────────────────────────────────────────────────────────────────────────┐
│ OUT2IN方向数据包分支流程图 │
└─────────────────────────────────────────────────────────────────────────┘
外部主机发送数据包(目标IP = NAT地址,如10.15.7.100)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 外部接口接收 │
│ ip4-input节点 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ╔═══════════════════════════════════╗ │
│ ║ 关键分支点:ip4-lookup节点 ║ │
│ ║ 进行FIB查找,决定走哪条路径 ║ │
│ ╚═══════════════════════════════════╝ │
│ │
│ FIB查找:目标IP = 10.15.7.100 │
│ │ │
│ ├────────────────────────┬─────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 找到FIB条目 找到FIB条目 未找到FIB条目 │
│ 使用NAT DPO 使用普通Adj (丢弃) │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ DPO模式路径 │ │ 普通模式路径 │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────────────────────┐
│ DPO模式路径 │ │ 普通模式路径(Feature Arc) │
│ │ │ │
│ 直接转发到: │ │ 继续Feature Arc处理: │
│ nat44-ei-out2in │ │ ↓ │
│ 节点 │ │ ip4-unicast Feature Arc │
│ │ │ ↓ │
│ 执行NAT转换 │ │ nat44-ei-classify节点(分类判断) │
│ │ │ ↓ │
│ 继续转发 │ │ nat44-ei-out2in节点(NAT转换) │
│ │ │ ↓ │
│ │ │ 继续Feature Arc │
│ │ │ ↓ │
│ │ │ ip4-lookup节点(再次查找) │
│ │ │ ↓ │
│ │ │ 继续转发 │
└──────────────────┘ └──────────────────────────────────────────┘
│ │
└────────────┬───────────┘
│
▼
┌──────────────┐
│ 内部网络转发 │
└──────────────┘
关键点说明:
1. 分支判断发生在ip4-lookup节点,通过FIB查找决定路径
2. DPO模式:FIB条目使用NAT DPO,直接转发到NAT节点
3. 普通模式:FIB条目使用普通Adj,继续Feature Arc处理
4. DPO模式减少2个节点跳转,提高性能
8.2.3 DPO模式 vs 普通模式的详细对比
节点跳转对比:
普通模式(Feature Arc):
┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────┐
│ip4-input│ → │Feature Arc │ → │nat44-ei- │ → │Feature Arc │ → │ip4- │
│ │ │(ip4-unicast)│ │classify │ │继续 │ │lookup │
└─────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └─────────┘
节点1 节点2 节点3 节点4 节点5
DPO模式:
┌─────────┐ ┌──────────────┐ ┌──────────────┐
│ip4-input│ → │ip4-lookup │ → │nat44-ei- │
│ │ │(FIB查找) │ │out2in │
└─────────┘ └──────────────┘ └──────────────┘
节点1 节点2 节点3
性能提升:减少2个节点跳转(classify节点和Feature Arc继续处理)
性能对比表:
| 指标 | 普通模式 | DPO模式 | 提升 |
|---|---|---|---|
| 节点数 | 5个 | 3个 | 减少40% |
| 分类开销 | 需要 | 不需要 | 消除 |
| Feature Arc开销 | 需要 | 不需要 | 消除 |
| 延迟 | 较高 | 较低 | 降低 |
| 吞吐量 | 较低 | 较高 | 提升 |
8.3 DPO概念详解
8.2.1 DPO的定义
根据VPP官方文档(src/vnet/dpo/dpo.h 第16-25行):
c
/**
* @brief
* A Data-Path Object is an object that represents actions that are
* applied to packets are they are switched through VPP's data-path.
*
* The DPO can be considered to be like is a base class that is specialised
* by other objects to provide concreate actions
*
* The VLIB graph nodes are graph of DPO types, the DPO graph is a graph of
* instances.
*/
生活化解释:
- DPO:数据平面对象,表示对数据包执行的操作
- 基类概念:DPO是基类,其他对象(如NAT DPO)是特化
- 节点图 vs 实例图:VLIB节点图是类型图,DPO图是实例图
根据VPP官方文档(docs/developer/corefeatures/fib/dataplane.rst):
The data-plane graph is composed of generic data-path objects (DPOs). A parent
DPO is identified by the tuple:{type,index,next_node}. The next_node parameter
is the index of the VLIB node to which the packets should be sent next, this is
present to maximise performance - it is important to ensure that the parent does
not need to be read whilst processing the child.
生活化解释:
- DPO标识 :通过
{type, index, next_node}三元组标识 - next_node:下一个VLIB节点的索引,用于性能优化
- 避免读取父节点:处理子节点时不需要读取父节点,提高性能
8.2.2 常见的DPO类型
根据VPP官方文档,常见的DPO类型包括:
- Load-balance:ECMP集合中的选择
- Adjacency:应用重写并通过接口转发
- MPLS-label:添加MPLS标签
- Lookup:在不同表中执行查找
- NAT DPO:NAT转换(NAT44-EI特有)
生活化理解:
- Load-balance:像"分拣中心",决定包裹走哪条路
- Adjacency:像"打包站",修改包裹并发送
- MPLS-label:像"贴标签",给包裹贴上标签
- Lookup:像"查地址簿",查找下一个目的地
- NAT DPO:像"地址转换站",转换地址并转发
8.4 NAT DPO的实现原理
8.4.1 DPO如何安装到FIB表?
关键步骤:添加NAT地址时安装DPO
当添加NAT地址到地址池时,如果启用了DPO模式,会在FIB表中为该地址安装NAT DPO:
添加NAT地址(如10.15.7.100)
↓
检查是否启用out2in_dpo模式
↓
如果启用:
↓
在FIB表中添加/32前缀条目
↓
使用NAT DPO作为转发路径
↓
FIB条目:10.15.7.100/32 → NAT DPO → nat44-ei-out2in节点
生活化理解:
就像在"地址簿"中登记"VIP通道":
1. 添加地址到地址簿(FIB表)
2. 标记这个地址使用"直达通道"(NAT DPO)
3. 当快递到达时,查看地址簿,发现是"直达通道",直接送到目的地
8.5 NAT DPO的实现代码
8.5.1 DPO模块初始化
代码实现(src/plugins/nat/nat44-ei/nat44_ei_dpo.c 第67-71行):
c
void
nat_dpo_module_init (void)
{
nat_dpo_type = dpo_register_new_type (&nat_dpo_vft, nat_nodes);
}
生活化解释:
- 注册DPO类型:向VPP的DPO框架注册新的DPO类型
- 虚拟函数表:提供DPO的操作函数(lock、unlock、format)
- 节点映射:指定DPO对应的VLIB节点
DPO虚拟函数表(第46-50行):
c
const static dpo_vft_t nat_dpo_vft = {
.dv_lock = nat_dpo_lock, // 锁定DPO(增加引用计数)
.dv_unlock = nat_dpo_unlock, // 解锁DPO(减少引用计数)
.dv_format = format_nat_dpo, // 格式化显示DPO
};
生活化解释:
- lock/unlock:管理DPO的引用计数,防止在使用时被删除
- format:用于调试和显示,格式化DPO的信息
节点映射(第52-65行):
c
const static char *const nat_ip4_nodes[] = {
"nat44-ei-out2in", // IP4协议对应的节点
NULL,
};
const static char *const nat_ip6_nodes[] = {
NULL, // IP6协议不支持
};
const static char *const *const nat_nodes[DPO_PROTO_NUM] = {
[DPO_PROTO_IP4] = nat_ip4_nodes,
[DPO_PROTO_IP6] = nat_ip6_nodes,
[DPO_PROTO_MPLS] = NULL,
};
生活化解释:
- 节点映射:指定DPO类型对应的VLIB节点
- 协议支持:只支持IP4协议,不支持IP6和MPLS
- 节点名称 :
nat44-ei-out2in,这是OUT2IN方向的NAT节点
8.5.2 DPO创建
代码实现(第21-25行):
c
void
nat_dpo_create (dpo_proto_t dproto, u32 aftr_index, dpo_id_t *dpo)
{
dpo_set (dpo, nat_dpo_type, dproto, aftr_index);
}
生活化解释:
- dproto:数据平面协议(IP4/IP6/MPLS等)
- aftr_index:AFTR索引(NAT DPO中未使用,设为0)
- dpo:输出的DPO标识
dpo_set函数的作用:
- 设置DPO的类型、协议和索引
- 创建DPO实例,用于FIB表中
8.5.3 DPO格式化显示
代码实现(第27-34行):
c
u8 *
format_nat_dpo (u8 *s, va_list *args)
{
index_t index = va_arg (*args, index_t);
CLIB_UNUSED (u32 indent) = va_arg (*args, u32);
return (format (s, "NAT44 out2in: AFTR:%d", index));
}
生活化解释:
- 格式化函数:用于调试和显示DPO信息
- 显示内容:显示"NAT44 out2in: AFTR:0"
- 调试用途 :在
show fib等命令中显示DPO信息
8.5.4 DPO锁定和解锁
代码实现(第36-44行):
c
static void
nat_dpo_lock (dpo_id_t *dpo)
{
// NAT DPO不需要特殊的锁定操作
}
static void
nat_dpo_unlock (dpo_id_t *dpo)
{
// NAT DPO不需要特殊的解锁操作
}
生活化解释:
- 空实现:NAT DPO不需要特殊的锁定/解锁操作
- 引用计数:由DPO框架自动管理
- 简化设计:NAT DPO是简单的转发对象,不需要复杂的状态管理
8.6 FIB集成:将DPO安装到FIB表
8.6.1 添加NAT地址时安装DPO
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第1339-1361行):
c
void
nat44_ei_add_del_address_dpo (ip4_address_t addr, u8 is_add)
{
nat44_ei_main_t *nm = &nat44_ei_main;
dpo_id_t dpo_v4 = DPO_INVALID;
fib_prefix_t pfx = {
.fp_proto = FIB_PROTOCOL_IP4,
.fp_len = 32, // /32前缀(单个IP地址)
.fp_addr.ip4.as_u32 = addr.as_u32,
};
if (is_add)
{
// 1. 创建NAT DPO
nat_dpo_create (DPO_PROTO_IP4, 0, &dpo_v4);
// 2. 将DPO添加到FIB表
fib_table_entry_special_dpo_add (0, &pfx, nm->fib_src_hi,
FIB_ENTRY_FLAG_EXCLUSIVE, &dpo_v4);
// 3. 重置DPO(释放引用)
dpo_reset (&dpo_v4);
}
else
{
// 删除FIB表中的DPO条目
fib_table_entry_special_remove (0, &pfx, nm->fib_src_hi);
}
}
生活化解释:
- 创建DPO:为NAT地址创建DPO实例
- 添加到FIB:在FIB表(表0,即默认表)中添加/32前缀条目
- EXCLUSIVE标志:独占标志,确保该条目不会被其他路由覆盖
- FIB源 :使用
nm->fib_src_hi(高优先级FIB源)
关键函数说明:
nat_dpo_create:创建NAT DPO实例fib_table_entry_special_dpo_add:在FIB表中添加特殊条目,使用DPO作为转发路径dpo_reset:重置DPO,释放引用计数
8.6.2 调用时机
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第481-494行):
c
// 在添加NAT地址时调用
if (nm->out2in_dpo)
{
nat44_ei_add_del_address_dpo (*addr, 1);
}
生活化解释:
- 条件检查 :只有当
out2in_dpo模式启用时才安装DPO - 添加地址时 :在
nat44_ei_add_address函数中调用 - 删除地址时 :在删除地址时调用
nat44_ei_add_del_address_dpo(addr, 0)
8.7 数据包在ip4-lookup节点的处理流程
8.7.1 ip4-lookup节点的FIB查找逻辑
代码实现(src/vnet/ip/ip4_forward.h 第327-354行):
c
// ip4-lookup节点进行FIB查找
lbi0 = ip4_fib_forwarding_lookup (vnet_buffer (b[0])->ip.fib_index, dst_addr0);
lb0 = load_balance_get (lbi0);
// 从Load Balance中获取DPO
if (lb0->lb_n_buckets > 1)
{
// 多路径:使用流哈希选择
hash_c0 = ip4_compute_flow_hash (ip0, flow_hash_config0);
dpo0 = load_balance_get_fwd_bucket (lb0, hash_c0 & (lb0->lb_n_buckets_minus_1));
}
else
{
// 单路径:直接获取
dpo0 = load_balance_get_bucket_i (lb0, 0);
}
// 关键:根据DPO的next_node转发
next[0] = dpo0->dpoi_next_node; // 如果是NAT DPO,这里指向nat44-ei-out2in节点
vnet_buffer (b[0])->ip.adj_index[VLIB_TX] = dpo0->dpoi_index;
生活化理解:
就像"快递分拣中心"的处理流程:
1. 查看地址簿(FIB查找)
2. 找到地址对应的"处理方式"(DPO)
3. 如果是"直达通道"(NAT DPO):
- 直接送到目的地(nat44-ei-out2in节点)
4. 如果是"普通通道"(普通Adj):
- 继续走普通流程(Feature Arc)
8.7.2 DPO节点映射
代码实现(src/plugins/nat/nat44-ei/nat44_ei_dpo.c 第52-65行):
c
const static char *const nat_ip4_nodes[] = {
"nat44-ei-out2in", // IP4协议对应的节点
NULL,
};
const static char *const *const nat_nodes[DPO_PROTO_NUM] = {
[DPO_PROTO_IP4] = nat_ip4_nodes,
[DPO_PROTO_IP6] = nat_ip6_nodes,
[DPO_PROTO_MPLS] = NULL,
};
生活化理解:
就像"地址簿中的标记":
- NAT DPO类型 → 映射到"nat44-ei-out2in"节点
- 当ip4-lookup节点找到NAT DPO时,就知道要转发到哪个节点
8.7.3 完整的数据包处理流程对比
DPO模式完整流程:
外部主机发送数据包(目标IP = NAT地址)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 外部接口接收 │
│ ip4-input节点 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ╔═══════════════════════════════════╗ │
│ ║ 关键分支点:ip4-lookup节点 ║ │
│ ║ FIB查找:目标IP = NAT地址 ║ │
│ ║ 找到FIB条目:使用NAT DPO ║ │
│ ║ next_node = nat44-ei-out2in ║ │
│ ╚═══════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ nat44-ei-out2in节点(NAT转换) │
│ - 查找会话 │
│ - 执行NAT转换:外部IP/端口 → 内部IP/端口 │
│ - 继续转发 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 内部网络转发 │
└─────────────────────────────────────────────────────────────────┘
节点数:3个(ip4-input → ip4-lookup → nat44-ei-out2in)
普通模式完整流程:
外部主机发送数据包(目标IP = NAT地址)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 外部接口接收 │
│ ip4-input节点 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Feature Arc(ip4-unicast) │
│ - 遍历所有Feature节点 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ nat44-ei-classify节点(分类判断) │
│ - 判断是否需要NAT处理 │
│ - 判断是IN2OUT还是OUT2IN │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ nat44-ei-out2in节点(NAT转换) │
│ - 查找会话 │
│ - 执行NAT转换:外部IP/端口 → 内部IP/端口 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 继续Feature Arc处理 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ip4-lookup节点(FIB查找) │
│ - 查找内部IP的路由 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 内部网络转发 │
└─────────────────────────────────────────────────────────────────┘
节点数:5个(ip4-input → Feature Arc → classify → out2in → Feature Arc继续 → ip4-lookup)
8.8 DPO的启用和禁用
8.8.1 启用DPO模式
配置方式:
bash
# 启用NAT44-EI插件,并启用out2in_dpo模式
nat44 ei plugin enable sessions 10000 users 1000 out2in-dpo
代码实现(src/plugins/nat/nat44-ei/nat44_ei.c 第522行):
c
nm->out2in_dpo = c.out2in_dpo;
生活化解释:
- 配置标志 :
out2in_dpo标志控制是否启用DPO模式 - 启用时机:在插件启用时设置
- 影响范围:只影响OUT2IN方向
8.8.2 DPO模式的限制
代码实现(第644-648行和第785-789行):
c
if (nm->out2in_dpo && !is_inside)
{
nat44_ei_log_err ("error unsupported");
return VNET_API_ERROR_UNSUPPORTED;
}
生活化解释:
- 限制 :当启用
out2in_dpo模式时,不能将外部接口配置为输出特性接口 - 原因:DPO模式通过FIB表直接路由,不需要输出特性接口
- 冲突:输出特性接口和DPO模式是两种不同的转发方式,不能同时使用
8.9 DPO与Feature Arc的对比总结
8.9.1 性能对比总结
Feature Arc方式:
- 优点:灵活,可以动态添加/删除特性
- 缺点:需要经过多个节点,性能较低
- 适用场景:需要灵活配置的场景
DPO方式:
- 优点:直接路由,性能高
- 缺点:需要预先配置,灵活性较低
- 适用场景:性能要求高的场景
8.9.2 使用建议
生活化理解:
就像选择"普通通道"还是"快速通道":
- 普通通道(Feature Arc):适合需要灵活配置的场景
- 快速通道(DPO):适合性能要求高的场景
建议:
- 高性能场景 :启用
out2in_dpo模式 - 灵活配置场景:使用Feature Arc方式
- 混合场景:可以根据实际需求选择
8.10 关键设计特点
8.10.1 性能优化
生活化理解:
就像快递的"直达通道":
- 减少跳转:数据包直接路由到NAT节点
- 跳过分类:不需要经过classify节点
- 提高效率:减少节点处理时间
8.10.2 FIB集成
生活化理解:
就像在"地址簿"中直接标记"VIP通道":
- FIB表集成:NAT地址直接添加到FIB表
- 快速查找:FIB查找直接返回DPO
- 无缝集成:与VPP的FIB系统深度集成
8.10.3 可选特性
生活化理解:
就像"可选服务":
- 可选启用:通过配置标志启用/禁用
- 向后兼容:不影响现有的Feature Arc方式
- 灵活选择:根据需求选择使用方式
8.10 总结
NAT DPO的核心价值:
- 性能提升:通过FIB表直接路由,减少节点跳转
- 简化路径:跳过Feature Arc的分类节点
- FIB集成:与VPP的FIB系统深度集成
- 可选特性:可以根据需求启用/禁用
使用场景:
- 高性能场景:需要高性能的OUT2IN转发
- 简单配置:不需要复杂的Feature Arc配置
- FIB路由:适合使用FIB表进行路由的场景
实现要点:
- DPO注册:向VPP的DPO框架注册NAT DPO类型
- FIB安装:在添加NAT地址时,将DPO安装到FIB表
- 节点映射:指定DPO对应的VLIB节点(nat44-ei-out2in)
- 路径优化:数据包通过FIB表直接路由到NAT节点
第九章:CLI命令处理 - nat44_ei_cli.c
9.1 文件概要
文件位置:
src/plugins/nat/nat44-ei/nat44_ei_cli.c(2091行)
主要作用:
实现所有CLI命令的处理,提供用户友好的命令行接口。
9.2 主要功能模块
9.2.1 插件管理命令
命令:
nat44 ei plugin enable- 启用插件nat44 ei plugin disable- 禁用插件show nat44 ei config- 显示配置
9.2.2 地址池管理命令
命令:
nat44 ei add address <addr> [vrf <vrf-id>]- 添加地址nat44 ei del address <addr>- 删除地址show nat44 ei addresses- 显示地址池
9.2.3 接口管理命令
命令:
set interface nat44 ei in <interface>- 设置内部接口set interface nat44 ei out <interface>- 设置外部接口set interface nat44 ei output-feature <interface>- 设置输出特性接口show nat44 ei interfaces- 显示接口配置
9.2.4 静态映射命令
命令:
nat44 ei add static mapping- 添加静态映射nat44 ei del static mapping- 删除静态映射show nat44 ei static mappings- 显示静态映射
9.2.5 会话管理命令
命令:
show nat44 ei sessions [verbose]- 显示会话show nat44 ei users [verbose]- 显示用户clear nat44 ei sessions- 清除会话
9.2.6 Worker管理命令
命令:
nat44 ei set workers <worker-list>- 设置Worker线程show nat44 ei workers- 显示Worker信息
9.2.7 HA管理命令
命令:
nat44 ei ha set listener- 设置HA监听器nat44 ei ha set failover- 设置HA故障转移show nat44 ei ha- 显示HA配置
9.2.8 统计命令
命令:
show nat44 ei statistics- 显示统计信息clear nat44 ei statistics- 清除统计信息
9.2.9 格式化函数
函数:
format_nat44_ei_session- 格式化会话显示format_nat44_ei_user- 格式化用户显示format_nat44_ei_address- 格式化地址显示format_nat44_ei_static_mapping- 格式化静态映射显示
9.3 关键设计特点
- 用户友好:提供清晰的命令语法和帮助信息
- 详细输出:支持verbose模式显示详细信息
- 错误处理:完善的错误检查和提示
- 格式化输出:美观的表格和列表格式
9.4 依赖关系
- 依赖
nat44_ei.h中的核心数据结构 - 依赖
nat44_ei_ha.h中的HA接口 - 依赖VPP的CLI框架
第十章:API处理 - nat44_ei_api.c
10.1 文件概要
文件位置:
src/plugins/nat/nat44-ei/nat44_ei_api.c(1469行)
主要作用:
处理所有API消息,提供程序化接口供外部应用调用。
10.2 主要功能模块
10.2.1 API消息处理
消息类型:
nat44_ei_plugin_enable_disable- 启用/禁用插件nat44_ei_show_running_config- 显示运行配置nat44_ei_set_workers- 设置Worker线程nat44_ei_worker_details- Worker详细信息nat44_ei_add_del_address- 添加/删除地址nat44_ei_add_del_interface- 添加/删除接口nat44_ei_add_del_static_mapping- 添加/删除静态映射nat44_ei_user_details- 用户详细信息nat44_ei_session_details- 会话详细信息nat44_ei_ha_set_listener- 设置HA监听器nat44_ei_ha_set_failover- 设置HA故障转移- 等等...
10.2.2 API注册
函数:
nat44_ei_api_hookup- 注册所有API消息处理函数
10.2.3 消息转换
功能:
- 将API消息转换为内部函数调用
- 处理网络字节序转换
- 验证参数有效性
- 生成回复消息
10.2.4 详细信息发送
功能:
send_nat_worker_details- 发送Worker详细信息send_nat_user_details- 发送用户详细信息send_nat_session_details- 发送会话详细信息- 支持分页和流式传输
10.3 关键设计特点
- 类型安全:使用生成的API类型定义
- 异步处理:支持异步API调用
- 错误处理:完善的错误码和错误消息
- 批量操作:支持批量查询和操作
10.4 依赖关系
- 依赖
nat44_ei.api生成的API定义 - 依赖
nat44_ei.h中的核心数据结构 - 依赖
nat44_ei_ha.h中的HA接口 - 依赖VPP的API框架
第十一章:API定义以及使用说明 - nat44_ei.api
11.1 文件概要
文件位置:
src/plugins/nat/nat44-ei/nat44_ei.api(948行)
主要作用:
定义所有API消息的格式和结构,VPP的API生成工具会根据此文件生成C代码、Python绑定等。
API版本:
option version = "1.1.1"- 当前API版本号
导入依赖:
vnet/ip/ip_types.api- IP类型定义(如IP地址、前缀等)vnet/interface_types.api- 接口类型定义(如接口索引等)plugins/nat/lib/nat_types.api- NAT通用类型定义
11.2 配置标志枚举
枚举定义:
c
enum nat44_ei_config_flags : u8
{
NAT44_EI_NONE = 0x00, // 无标志
NAT44_EI_STATIC_MAPPING_ONLY = 0x01, // 仅静态映射模式
NAT44_EI_CONNECTION_TRACKING = 0x02, // 连接跟踪
NAT44_EI_OUT2IN_DPO = 0x04, // OUT2IN DPO模式
NAT44_EI_ADDR_ONLY_MAPPING = 0x08, // 仅地址映射(无端口)
NAT44_EI_IF_INSIDE = 0x10, // 内部接口标志
NAT44_EI_IF_OUTSIDE = 0x20, // 外部接口标志
NAT44_EI_STATIC_MAPPING = 0x40, // 静态映射标志
};
生活化理解:
就像"功能开关":
- NAT44_EI_STATIC_MAPPING_ONLY:只允许静态映射,不允许动态映射
- NAT44_EI_CONNECTION_TRACKING:启用连接跟踪(用于有状态防火墙)
- NAT44_EI_OUT2IN_DPO:使用DPO模式(快速路径)
- NAT44_EI_ADDR_ONLY_MAPPING:只映射IP地址,不映射端口
11.3 插件管理API
11.3.1 nat44_ei_plugin_enable_disable - 启用/禁用插件
生活化理解:
就像"打开/关闭NAT功能的总开关"。
使用场景:
- 初始化:系统启动时启用NAT插件
- 重新配置:需要修改配置时,先禁用再启用
- 故障恢复:出现问题时,禁用并重新启用
API定义:
c
autoreply define nat44_ei_plugin_enable_disable {
option in_progress;
u32 client_index;
u32 context;
u32 inside_vrf; // 内部VRF ID
u32 outside_vrf; // 外部VRF ID
u32 users; // 每线程最大用户数
u32 user_memory; // 用户哈希表内存(覆盖默认值)
u32 sessions; // 每线程最大会话数
u32 session_memory; // 会话哈希表内存(覆盖默认值)
u32 user_sessions; // 每用户最大会话数
bool enable; // true=启用,false=禁用
vl_api_nat44_ei_config_flags_t flags; // 配置标志
};
Python使用示例:
python
import vpp_papi
# 连接到VPP
vpp = vpp_papi.VPPApiClient()
vpp.connect("vpp")
# 启用NAT插件
vpp.api.nat44_ei_plugin_enable_disable(
inside_vrf=0, # 内部VRF(默认0)
outside_vrf=0, # 外部VRF(默认0)
users=10240, # 每线程最大用户数
sessions=10240, # 每线程最大会话数
user_sessions=10240, # 每用户最大会话数
enable=True, # 启用
flags=0 # 无特殊标志
)
# 启用NAT插件(仅静态映射模式)
vpp.api.nat44_ei_plugin_enable_disable(
users=1024,
sessions=10240,
user_sessions=10240,
enable=True,
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_STATIC_MAPPING_ONLY
)
# 禁用NAT插件
vpp.api.nat44_ei_plugin_enable_disable(enable=False)
CLI使用示例:
bash
# 启用NAT插件(使用默认配置)
vpp# nat44 ei plugin enable
# 启用NAT插件(指定参数)
vpp# nat44 ei plugin enable sessions 10240 users 1024
# 禁用NAT插件
vpp# nat44 ei plugin disable
参数说明:
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
inside_vrf |
u32 | 内部网络VRF ID | 0 |
outside_vrf |
u32 | 外部网络VRF ID | 0 |
users |
u32 | 每线程最大用户数 | 1024 |
sessions |
u32 | 每线程最大会话数 | 10*1024 |
user_sessions |
u32 | 每用户最大会话数 | 等于sessions |
enable |
bool | true=启用,false=禁用 | - |
flags |
flags | 配置标志(见枚举) | 0 |
注意事项:
- 启用前:确保已添加NAT地址池
- 禁用时:会清除所有会话和用户
- 内存分配 :
user_memory和session_memory用于覆盖默认的哈希表大小计算
11.3.2 nat44_ei_show_running_config - 显示运行配置
生活化理解:
就像"查看NAT的当前配置"。
使用场景:
- 配置检查:查看当前NAT配置是否正确
- 故障排查:检查配置是否有问题
- 配置备份:保存当前配置用于恢复
API定义:
c
define nat44_ei_show_running_config
{
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_show_running_config_reply
{
option in_progress;
u32 context;
i32 retval;
u32 inside_vrf;
u32 outside_vrf;
u32 users;
u32 sessions;
u32 user_sessions;
u32 user_buckets;
u32 translation_buckets;
bool forwarding_enabled;
bool ipfix_logging_enabled;
vl_api_nat_timeouts_t timeouts;
vl_api_nat_log_level_t log_level;
vl_api_nat44_ei_config_flags_t flags;
};
Python使用示例:
python
# 获取运行配置
reply = vpp.api.nat44_ei_show_running_config()
print(f"Inside VRF: {reply.inside_vrf}")
print(f"Outside VRF: {reply.outside_vrf}")
print(f"Max users per thread: {reply.users}")
print(f"Max sessions per thread: {reply.sessions}")
print(f"Max sessions per user: {reply.user_sessions}")
print(f"Forwarding enabled: {reply.forwarding_enabled}")
print(f"IPFIX logging enabled: {reply.ipfix_logging_enabled}")
CLI使用示例:
bash
vpp# show nat44 ei config
NAT44-EI plugin enabled
inside VRF: 0
outside VRF: 0
users: 1024
sessions: 10240
user sessions: 10240
forwarding: disabled
IPFIX logging: disabled
11.4 地址管理API
11.4.1 nat44_ei_add_del_address_range - 添加/删除地址范围
生活化理解:
就像"给NAT分配可用的公网IP地址池"。
使用场景:
- 初始配置:系统启动时添加NAT地址池
- 地址扩展:需要更多公网IP时添加地址范围
- 地址回收:不再需要某些地址时删除
- 多租户:为不同租户(VRF)分配不同的地址池
API定义:
c
autoreply define nat44_ei_add_del_address_range {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t first_ip_address; // 起始IP地址
vl_api_ip4_address_t last_ip_address; // 结束IP地址
u32 vrf_id; // VRF ID(~0表示独立于VRF)
bool is_add; // true=添加,false=删除
};
Python使用示例:
python
# 添加单个IP地址
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.7.100",
last_ip_address="10.15.7.100",
vrf_id=0xFFFFFFFF, # ~0,独立于VRF
is_add=True
)
# 添加IP地址范围
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.7.100",
last_ip_address="10.15.7.200", # 100个IP地址
vrf_id=0xFFFFFFFF,
is_add=True
)
# 为特定租户(VRF 10)添加地址
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.8.100",
last_ip_address="10.15.8.200",
vrf_id=10, # 租户VRF ID
is_add=True
)
# 删除地址
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.7.100",
last_ip_address="10.15.7.100",
vrf_id=0xFFFFFFFF,
is_add=False
)
CLI使用示例:
bash
# 添加单个IP地址
vpp# nat44 ei add address 10.15.7.100
# 添加IP地址范围
vpp# nat44 ei add address 10.15.7.100 - 10.15.7.200
# 为特定租户添加地址
vpp# nat44 ei add address 10.15.8.100 tenant-vrf 10
# 删除地址
vpp# nat44 ei add address 10.15.7.100 del
实际应用场景:
场景1:单租户环境
python
# 公司只有一个部门,使用一个地址池
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="203.0.113.1",
last_ip_address="203.0.113.10", # 10个公网IP
vrf_id=0xFFFFFFFF,
is_add=True
)
场景2:多租户环境
python
# 公司有多个部门,每个部门使用不同的地址池
# 部门A(VRF 10)
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="203.0.113.1",
last_ip_address="203.0.113.5",
vrf_id=10,
is_add=True
)
# 部门B(VRF 20)
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="203.0.113.6",
last_ip_address="203.0.113.10",
vrf_id=20,
is_add=True
)
注意事项:
- 地址范围:可以是单个IP(first==last)或IP范围
- VRF隔离:不同VRF的地址池相互独立
- 删除限制:如果地址正在使用(有活跃会话),删除可能失败
- DPO模式 :如果启用
out2in_dpo,地址会自动添加到FIB表
11.4.2 nat44_ei_address_dump - 转储地址列表
生活化理解:
就像"查看NAT地址池中所有已分配的地址"。
使用场景:
- 配置检查:查看已添加的地址
- 地址统计:查看地址使用情况
- 故障排查:检查地址配置是否正确
API定义:
c
define nat44_ei_address_dump {
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_address_details {
option in_progress;
u32 context;
vl_api_ip4_address_t ip_address; // IP地址
u32 vrf_id; // VRF ID
};
Python使用示例:
python
# 转储所有地址
addresses = vpp.api.nat44_ei_address_dump()
for addr in addresses:
print(f"IP: {addr.ip_address}, VRF: {addr.vrf_id}")
CLI使用示例:
bash
vpp# show nat44 ei addresses
NAT44 pool addresses:
10.15.7.100
tenant VRF independent
10 busy udp ports
2 busy tcp ports
0 busy icmp ports
10.15.7.101
tenant VRF: 10
5 busy udp ports
1 busy tcp ports
0 busy icmp ports
11.5 接口管理API
11.5.1 nat44_ei_interface_add_del_feature - 添加/删除接口特性
生活化理解:
就像"告诉NAT哪些接口是内部网络,哪些是外部网络"。
使用场景:
- 初始配置:系统启动时配置内部和外部接口
- 接口变更:接口配置变化时更新
- 多接口:支持多个内部接口和外部接口
API定义:
c
autoreply define nat44_ei_interface_add_del_feature {
option in_progress;
u32 client_index;
u32 context;
bool is_add; // true=添加,false=删除
vl_api_nat44_ei_config_flags_t flags; // NAT44_EI_IF_INSIDE或NAT44_EI_IF_OUTSIDE
vl_api_interface_index_t sw_if_index; // 接口索引
};
Python使用示例:
python
# 配置内部接口
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=1, # GigabitEthernet0/0/0
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_INSIDE,
is_add=True
)
# 配置外部接口
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=2, # GigabitEthernet0/0/1
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_OUTSIDE,
is_add=True
)
# 删除接口配置
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=1,
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_INSIDE,
is_add=False
)
CLI使用示例:
bash
# 配置内部接口
vpp# set interface nat44 ei in GigabitEthernet0/0/0
# 配置外部接口
vpp# set interface nat44 ei out GigabitEthernet0/0/1
# 删除接口配置
vpp# set interface nat44 ei in GigabitEthernet0/0/0 del
实际应用场景:
场景1:典型企业网络
python
# 内部网络接口(连接内网)
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=internal_if_index,
flags=NAT44_EI_IF_INSIDE,
is_add=True
)
# 外部网络接口(连接互联网)
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=external_if_index,
flags=NAT44_EI_IF_OUTSIDE,
is_add=True
)
场景2:多内部接口
python
# 多个内部接口(如多个VLAN)
for if_index in [1, 2, 3]: # 三个内部接口
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=if_index,
flags=NAT44_EI_IF_INSIDE,
is_add=True
)
注意事项:
- 接口方向:必须明确指定是内部(INSIDE)还是外部(OUTSIDE)
- Feature Arc:接口会添加到相应的Feature Arc
- 删除限制:如果接口有活跃会话,删除可能失败
11.5.2 nat44_ei_interface_dump - 转储接口列表
生活化理解:
就像"查看哪些接口配置了NAT"。
使用场景:
- 配置检查:查看已配置的接口
- 故障排查:检查接口配置是否正确
API定义:
c
define nat44_ei_interface_dump {
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_interface_details {
option in_progress;
u32 context;
vl_api_nat44_ei_config_flags_t flags; // INSIDE/OUTSIDE标志
vl_api_interface_index_t sw_if_index; // 接口索引
};
Python使用示例:
python
# 转储所有接口
interfaces = vpp.api.nat44_ei_interface_dump()
for iface in interfaces:
if iface.flags & NAT44_EI_IF_INSIDE:
print(f"Inside interface: {iface.sw_if_index}")
if iface.flags & NAT44_EI_IF_OUTSIDE:
print(f"Outside interface: {iface.sw_if_index}")
CLI使用示例:
bash
vpp# show nat44 ei interfaces
NAT44 interfaces:
GigabitEthernet0/0/0 in
GigabitEthernet0/0/1 out
11.6 静态映射API
11.6.1 nat44_ei_add_del_static_mapping - 添加/删除静态映射
生活化理解:
就像"给内部服务器分配固定的公网IP和端口"。
使用场景:
- 服务器发布:将内部服务器发布到公网
- 端口转发:将公网端口映射到内部服务器
- 地址映射:将公网IP映射到内部IP(无端口)
API定义:
c
autoreply define nat44_ei_add_del_static_mapping {
option in_progress;
u32 client_index;
u32 context;
bool is_add; // true=添加,false=删除
vl_api_nat44_ei_config_flags_t flags; // NAT44_EI_ADDR_ONLY_MAPPING等
vl_api_ip4_address_t local_ip_address; // 内部IP地址
vl_api_ip4_address_t external_ip_address; // 外部IP地址
u8 protocol; // 协议(TCP=6, UDP=17, ICMP=1)
u16 local_port; // 内部端口
u16 external_port; // 外部端口
vl_api_interface_index_t external_sw_if_index; // 外部接口(可选)
u32 vrf_id; // VRF ID
string tag[64]; // 标签(用于标识)
};
Python使用示例:
场景1:端口映射(Web服务器)
python
# 将公网IP:80映射到内部服务器192.168.1.10:8080
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.10",
external_ip_address="10.15.7.100",
local_port=8080,
external_port=80,
protocol=6, # TCP
vrf_id=0,
tag="web-server"
)
场景2:地址映射(无端口)
python
# 将公网IP映射到内部IP(所有端口)
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.20",
external_ip_address="10.15.7.101",
local_port=0,
external_port=0,
protocol=0,
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_ADDR_ONLY_MAPPING,
vrf_id=0,
tag="full-nat"
)
场景3:使用外部接口(动态IP)
python
# 使用外部接口的IP地址(适用于动态IP场景)
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.30",
external_ip_address="0.0.0.0", # 忽略,使用接口IP
external_sw_if_index=2, # 外部接口索引
local_port=22,
external_port=2222,
protocol=6, # TCP
vrf_id=0,
tag="ssh-server"
)
CLI使用示例:
bash
# 端口映射(Web服务器)
vpp# nat44 ei add static mapping tcp local 192.168.1.10:8080 external 10.15.7.100:80
# 地址映射(无端口)
vpp# nat44 ei add static mapping local 192.168.1.20 external 10.15.7.101
# 使用外部接口
vpp# nat44 ei add static mapping tcp local 192.168.1.30:22 external GigabitEthernet0/0/1:2222
# 删除静态映射
vpp# nat44 ei del static mapping tcp local 192.168.1.10:8080 external 10.15.7.100:80
实际应用场景:
场景1:企业Web服务器发布
python
# 内部Web服务器:192.168.1.10:8080
# 公网访问:203.0.113.1:80
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.10",
external_ip_address="203.0.113.1",
local_port=8080,
external_port=80,
protocol=6, # TCP
tag="company-website"
)
场景2:游戏服务器端口范围映射
python
# 游戏服务器需要多个端口
for port in range(27015, 27020): # 5个端口
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.100",
external_ip_address="203.0.113.2",
local_port=port,
external_port=port,
protocol=17, # UDP
tag=f"game-server-{port}"
)
场景3:FTP服务器(主动模式)
python
# FTP服务器需要控制端口和数据端口
# 控制端口
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.50",
external_ip_address="203.0.113.3",
local_port=21,
external_port=21,
protocol=6, # TCP
tag="ftp-control"
)
# 数据端口范围(FTP主动模式需要)
for port in range(50000, 50100):
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.50",
external_ip_address="203.0.113.3",
local_port=port,
external_port=port,
protocol=6, # TCP
tag=f"ftp-data-{port}"
)
参数说明:
| 参数 | 类型 | 说明 | 必需 |
|---|---|---|---|
local_ip_address |
ip4 | 内部IP地址 | 是 |
external_ip_address |
ip4 | 外部IP地址(或0.0.0.0使用接口IP) | 是 |
local_port |
u16 | 内部端口(地址映射时为0) | 否 |
external_port |
u16 | 外部端口(地址映射时为0) | 否 |
protocol |
u8 | 协议(TCP=6, UDP=17, ICMP=1) | 端口映射时必需 |
external_sw_if_index |
u32 | 外部接口索引(~0表示不使用) | 否 |
vrf_id |
u32 | VRF ID | 是 |
tag |
string | 标签(用于标识,最大64字符) | 否 |
flags |
flags | NAT44_EI_ADDR_ONLY_MAPPING等 | 否 |
注意事项:
- 地址映射 :设置
NAT44_EI_ADDR_ONLY_MAPPING标志,端口和协议参数被忽略 - 端口映射:必须指定协议和端口
- 接口IP :如果
external_sw_if_index有效,external_ip_address被忽略 - 删除:删除时必须提供与添加时相同的参数
11.6.2 nat44_ei_static_mapping_dump - 转储静态映射列表
生活化理解:
就像"查看所有已配置的静态映射"。
使用场景:
- 配置检查:查看已配置的静态映射
- 故障排查:检查静态映射是否正确
- 配置备份:保存静态映射配置
API定义:
c
define nat44_ei_static_mapping_dump {
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_static_mapping_details {
option in_progress;
u32 context;
vl_api_nat44_ei_config_flags_t flags;
vl_api_ip4_address_t local_ip_address;
vl_api_ip4_address_t external_ip_address;
u8 protocol;
u16 local_port;
u16 external_port;
vl_api_interface_index_t external_sw_if_index;
u32 vrf_id;
string tag[64];
};
Python使用示例:
python
# 转储所有静态映射
mappings = vpp.api.nat44_ei_static_mapping_dump()
for mapping in mappings:
if mapping.flags & NAT44_EI_ADDR_ONLY_MAPPING:
print(f"Address mapping: {mapping.local_ip_address} -> {mapping.external_ip_address}")
else:
print(f"Port mapping: {mapping.local_ip_address}:{mapping.local_port} -> "
f"{mapping.external_ip_address}:{mapping.external_port} "
f"(protocol={mapping.protocol}, tag={mapping.tag})")
CLI使用示例:
bash
vpp# show nat44 ei static mappings
NAT44 static mappings:
tcp local 192.168.1.10:8080 external 10.15.7.100:80 vrf 0 tag web-server
udp local 192.168.1.20 external 10.15.7.101 vrf 0 tag full-nat
11.7 查询API
11.7.1 nat44_ei_user_dump - 转储用户列表
生活化理解:
就像"查看所有使用NAT的内部用户"。
使用场景:
- 用户统计:查看有多少内部用户在使用NAT
- 故障排查:检查特定用户是否有会话
- 监控:监控NAT用户数量
API定义:
c
define nat44_ei_user_dump {
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_user_details {
option in_progress;
u32 context;
u32 vrf_id; // VRF ID
vl_api_ip4_address_t ip_address; // 用户IP地址
u32 nsessions; // 动态会话数
u32 nstaticsessions; // 静态会话数
};
Python使用示例:
python
# 转储所有用户
users = vpp.api.nat44_ei_user_dump()
print(f"Total users: {len(users)}")
for user in users:
print(f"User: {user.ip_address}, VRF: {user.vrf_id}, "
f"Dynamic sessions: {user.nsessions}, "
f"Static sessions: {user.nstaticsessions}")
CLI使用示例:
bash
vpp# show nat44 ei users
NAT44 users:
192.168.1.10 vrf 0 3 sessions 0 static sessions
192.168.1.20 vrf 0 5 sessions 1 static sessions
192.168.1.30 vrf 0 0 sessions 0 static sessions
实际应用场景:
场景1:用户监控
python
# 定期检查用户数量
users = vpp.api.nat44_ei_user_dump()
if len(users) > 1000:
print("Warning: Too many NAT users!")
# 发送告警
场景2:用户会话统计
python
# 统计每个用户的会话数
users = vpp.api.nat44_ei_user_dump()
total_sessions = 0
for user in users:
total_sessions += user.nsessions
total_sessions += user.nstaticsessions
print(f"Total sessions: {total_sessions}")
11.7.2 nat44_ei_user_session_dump - 转储用户会话列表
生活化理解:
就像"查看某个用户的所有NAT会话"。
使用场景:
- 用户诊断:查看特定用户的会话详情
- 故障排查:检查用户会话是否正常
- 会话监控:监控用户会话状态
API定义:
c
define nat44_ei_user_session_dump {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t ip_address; // 用户IP地址
u32 vrf_id; // VRF ID
};
define nat44_ei_user_session_details {
option in_progress;
u32 context;
vl_api_ip4_address_t outside_ip_address; // 外部IP地址
u16 outside_port; // 外部端口
vl_api_ip4_address_t inside_ip_address; // 内部IP地址
u16 inside_port; // 内部端口
u16 protocol; // 协议
vl_api_nat44_ei_config_flags_t flags; // 标志(如STATIC_MAPPING)
u64 last_heard; // 最后活动时间
u64 total_bytes; // 总字节数
u32 total_pkts; // 总数据包数
vl_api_ip4_address_t ext_host_address; // 外部主机IP
u16 ext_host_port; // 外部主机端口
};
Python使用示例:
python
# 查看用户192.168.1.10的所有会话
sessions = vpp.api.nat44_ei_user_session_dump(
ip_address="192.168.1.10",
vrf_id=0
)
print(f"User 192.168.1.10 has {len(sessions)} sessions:")
for session in sessions:
print(f" {session.inside_ip_address}:{session.inside_port} -> "
f"{session.outside_ip_address}:{session.outside_port} "
f"(protocol={session.protocol}, "
f"bytes={session.total_bytes}, pkts={session.total_pkts})")
CLI使用示例:
bash
vpp# show nat44 ei user sessions 192.168.1.10
NAT44 user 192.168.1.10 sessions:
192.168.1.10:6303 -> 10.15.7.100:12345 tcp bytes 1024 pkts 10
192.168.1.10:6304 -> 10.15.7.100:12346 udp bytes 2048 pkts 20
192.168.1.10:6305 -> 10.15.7.100:12347 icmp bytes 64 pkts 1
实际应用场景:
场景1:用户会话诊断
python
# 诊断用户无法访问外网的问题
user_ip = "192.168.1.10"
sessions = vpp.api.nat44_ei_user_session_dump(
ip_address=user_ip,
vrf_id=0
)
if len(sessions) == 0:
print(f"User {user_ip} has no sessions - NAT may not be working")
else:
print(f"User {user_ip} has {len(sessions)} active sessions")
for session in sessions:
if session.total_pkts == 0:
print(f"Warning: Session {session.outside_port} has no packets")
场景2:会话流量统计
python
# 统计用户的总流量
user_ip = "192.168.1.10"
sessions = vpp.api.nat44_ei_user_session_dump(
ip_address=user_ip,
vrf_id=0
)
total_bytes = sum(s.total_bytes for s in sessions)
total_pkts = sum(s.total_pkts for s in sessions)
print(f"User {user_ip} total: {total_bytes} bytes, {total_pkts} packets")
11.7.3 nat44_ei_user_session_v2_dump - 转储用户会话列表(V2版本)
生活化理解:
就像"查看用户会话的增强版本"(包含时间差信息)。
与V1的区别:
- V1 :只提供
last_heard(绝对时间) - V2 :提供
last_heard和time_since_last_heard(时间差)
使用场景:
- 会话超时检查:更容易判断会话是否超时
- 监控:监控会话活动时间
API定义:
c
define nat44_ei_user_session_v2_dump {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t ip_address;
u32 vrf_id;
};
define nat44_ei_user_session_v2_details {
option in_progress;
u32 context;
vl_api_ip4_address_t outside_ip_address;
u16 outside_port;
vl_api_ip4_address_t inside_ip_address;
u16 inside_port;
u16 protocol;
vl_api_nat44_ei_config_flags_t flags;
u64 last_heard; // 最后活动时间(绝对时间)
u64 time_since_last_heard; // 距离最后活动的时间(秒)
u64 total_bytes;
u32 total_pkts;
vl_api_ip4_address_t ext_host_address;
u16 ext_host_port;
};
Python使用示例:
python
# 查看用户会话(V2版本)
sessions = vpp.api.nat44_ei_user_session_v2_dump(
ip_address="192.168.1.10",
vrf_id=0
)
for session in sessions:
# 检查会话是否超时(UDP默认300秒)
if session.time_since_last_heard > 300:
print(f"Warning: Session {session.outside_port} may be timed out")
print(f"Session: {session.inside_port} -> {session.outside_port}, "
f"idle for {session.time_since_last_heard} seconds")
11.8 会话管理API
11.8.1 nat44_ei_del_session - 删除会话
生活化理解:
就像"强制删除某个NAT会话"。
使用场景:
- 故障恢复:删除异常的会话
- 安全控制:强制断开特定连接
- 测试:测试会话删除功能
API定义:
c
autoreply define nat44_ei_del_session {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t address; // IP地址(内部或外部)
u8 protocol; // 协议
u16 port; // 端口
u32 vrf_id; // VRF ID
vl_api_nat44_ei_config_flags_t flags; // NAT44_EI_IF_INSIDE等
vl_api_ip4_address_t ext_host_address; // 外部主机IP(可选)
u16 ext_host_port; // 外部主机端口(可选)
};
Python使用示例:
python
# 删除内部用户的会话(通过内部IP和端口)
vpp.api.nat44_ei_del_session(
address="192.168.1.10",
protocol=6, # TCP
port=6303,
vrf_id=0,
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_INSIDE
)
# 删除外部会话(通过外部IP和端口)
vpp.api.nat44_ei_del_session(
address="10.15.7.100",
protocol=6, # TCP
port=12345,
vrf_id=0,
flags=0 # 外部会话
)
# 删除会话(指定外部主机)
vpp.api.nat44_ei_del_session(
address="10.15.7.100",
protocol=6,
port=12345,
vrf_id=0,
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_EXT_HOST_VALID,
ext_host_address="203.0.113.1",
ext_host_port=80
)
CLI使用示例:
bash
# 删除会话(通过内部IP和端口)
vpp# nat44 ei del session inside 192.168.1.10:6303 tcp
# 删除会话(通过外部IP和端口)
vpp# nat44 ei del session outside 10.15.7.100:12345 tcp
注意事项:
- 地址类型 :通过
flags指定是内部地址还是外部地址 - 外部主机 :如果指定外部主机,需要设置
NAT44_EI_IF_EXT_HOST_VALID标志 - 精确匹配:必须提供准确的IP、端口、协议才能删除
11.9 高级功能API
11.9.1 nat44_ei_set_timeouts - 设置会话超时
生活化理解:
就像"设置NAT会话的过期时间"。
使用场景:
- 性能优化:根据应用特点调整超时时间
- 安全控制:缩短超时时间提高安全性
- 长连接支持:延长TCP超时时间支持长连接
API定义:
c
autoreply define nat44_ei_set_timeouts {
option in_progress;
u32 client_index;
u32 context;
u32 udp; // UDP超时(秒,默认300)
u32 tcp_established; // TCP已建立连接超时(秒,默认7440)
u32 tcp_transitory; // TCP瞬态状态超时(秒,默认240)
u32 icmp; // ICMP超时(秒,默认60)
};
Python使用示例:
python
# 设置自定义超时时间
vpp.api.nat44_ei_set_timeouts(
udp=180, # UDP:3分钟(默认5分钟)
tcp_established=3600, # TCP已建立:1小时(默认2小时)
tcp_transitory=120, # TCP瞬态:2分钟(默认4分钟)
icmp=30 # ICMP:30秒(默认1分钟)
)
CLI使用示例:
bash
vpp# nat44 ei set timeouts udp 180 tcp-established 3600 tcp-transitory 120 icmp 30
实际应用场景:
场景1:Web服务器(短超时)
python
# Web服务器通常连接时间短,可以设置较短的超时
vpp.api.nat44_ei_set_timeouts(
udp=60, # UDP:1分钟
tcp_established=300, # TCP:5分钟
tcp_transitory=60, # TCP瞬态:1分钟
icmp=30 # ICMP:30秒
)
场景2:长连接应用(长超时)
python
# 数据库连接等长连接应用
vpp.api.nat44_ei_set_timeouts(
udp=300, # UDP:5分钟
tcp_established=7200, # TCP:2小时
tcp_transitory=240, # TCP瞬态:4分钟
icmp=60 # ICMP:1分钟
)
注意事项:
- 默认值:如果不设置,使用默认值
- 影响范围:影响所有新创建的会话
- 已存在会话:不影响已存在的会话
11.9.2 nat44_ei_forwarding_enable_disable - 启用/禁用转发
生活化理解:
就像"允许或禁止未匹配的流量通过"。
使用场景:
- 调试:允许未匹配的流量通过,便于调试
- 安全策略:禁止未匹配的流量,提高安全性
- 混合模式:某些场景需要允许转发
API定义:
c
autoreply define nat44_ei_forwarding_enable_disable {
option in_progress;
u32 client_index;
u32 context;
bool enable; // true=启用转发,false=禁用转发
};
Python使用示例:
python
# 启用转发(允许未匹配的流量通过)
vpp.api.nat44_ei_forwarding_enable_disable(enable=True)
# 禁用转发(丢弃未匹配的流量)
vpp.api.nat44_ei_forwarding_enable_disable(enable=False)
CLI使用示例:
bash
# 启用转发
vpp# nat44 ei forwarding enable
# 禁用转发
vpp# nat44 ei forwarding disable
实际应用场景:
场景1:调试模式
python
# 调试时启用转发,查看未匹配的流量
vpp.api.nat44_ei_forwarding_enable_disable(enable=True)
# ...进行测试...
vpp.api.nat44_ei_forwarding_enable_disable(enable=False) # 恢复正常
场景2:安全策略
python
# 生产环境禁用转发,提高安全性
vpp.api.nat44_ei_forwarding_enable_disable(enable=False)
11.9.3 nat44_ei_set_mss_clamping - 设置TCP MSS重写
生活化理解:
就像"限制TCP数据包的最大大小"。
使用场景:
- MTU问题:解决MTU不匹配导致的TCP问题
- 性能优化:优化TCP性能
- 兼容性:兼容某些网络环境
API定义:
c
autoreply define nat44_ei_set_mss_clamping {
option in_progress;
u32 client_index;
u32 context;
u16 mss_value; // MSS值(字节)
bool enable; // true=启用,false=禁用
};
Python使用示例:
python
# 启用MSS Clamping,设置MSS为1400字节
vpp.api.nat44_ei_set_mss_clamping(
mss_value=1400,
enable=True
)
# 禁用MSS Clamping
vpp.api.nat44_ei_set_mss_clamping(enable=False)
CLI使用示例:
bash
# 启用MSS Clamping
vpp# nat44 ei set mss-clamping 1400
# 禁用MSS Clamping
vpp# nat44 ei set mss-clamping disable
11.10 高可用性API
11.10.1 nat44_ei_ha_set_listener - 设置HA监听器
生活化理解:
就像"告诉NAT节点监听HA同步消息的地址和端口"。
使用场景:
- HA配置:配置Passive节点接收会话同步
- 故障切换:配置新的Passive节点
API定义:
c
autoreply define nat44_ei_ha_set_listener {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t ip_address; // 本地IP地址
u16 port; // 本地UDP端口
u32 path_mtu; // 路径MTU
};
Python使用示例:
python
# 配置Passive节点监听器
vpp.api.nat44_ei_ha_set_listener(
ip_address="10.0.0.2", # 同步接口IP
port=2345, # UDP端口
path_mtu=1500 # 路径MTU
)
CLI使用示例:
bash
vpp# nat ha listener 10.0.0.2:2345
11.10.2 nat44_ei_ha_set_failover - 设置HA故障转移
生活化理解:
就像"告诉Active节点Passive节点的地址和端口"。
使用场景:
- HA配置:配置Active节点发送会话同步
- 故障切换:配置新的Passive节点
API定义:
c
autoreply define nat44_ei_ha_set_failover {
option in_progress;
u32 client_index;
u32 context;
vl_api_ip4_address_t ip_address; // 故障转移节点IP地址
u16 port; // 故障转移节点UDP端口
u32 session_refresh_interval; // 会话刷新间隔(秒)
};
Python使用示例:
python
# 配置Active节点故障转移目标
vpp.api.nat44_ei_ha_set_failover(
ip_address="10.0.0.2", # Passive节点IP
port=2345, # Passive节点端口
session_refresh_interval=60 # 每60秒刷新一次会话统计
)
CLI使用示例:
bash
vpp# nat ha failover 10.0.0.2:2345
11.10.3 nat44_ei_ha_resync - 重新同步HA
生活化理解:
就像"将Active节点的所有会话重新同步到Passive节点"。
使用场景:
- 新Passive节点:新的Passive节点启动时,需要同步所有会话
- 故障恢复:Passive节点故障恢复后,重新同步
- 配置变更:更换Passive节点时
API定义:
c
autoreply define nat44_ei_ha_resync
{
option in_progress;
u32 client_index;
u32 context;
u8 want_resync_event; // 是否接收resync完成事件
u32 pid; // 客户端进程ID
};
Python使用示例:
python
# 触发Resync
vpp.api.nat44_ei_ha_resync(
want_resync_event=1, # 接收完成事件
pid=os.getpid() # 当前进程ID
)
# 注册事件回调
def handle_resync_completed(event):
if event.missed_count == 0:
print("Resync completed successfully")
else:
print(f"Resync completed with {event.missed_count} missed messages")
vpp.register_event_callback(handle_resync_completed)
CLI使用示例:
bash
vpp# nat ha resync
11.11 Worker管理API
11.11.1 nat44_ei_set_workers - 设置Worker线程
生活化理解:
就像"指定哪些Worker线程处理NAT流量"。
使用场景:
- 性能优化:指定高性能的Worker线程
- 资源隔离:将NAT流量隔离到特定Worker
- 负载均衡:调整Worker分配
API定义:
c
autoreply define nat44_ei_set_workers {
option in_progress;
u32 client_index;
u32 context;
u64 worker_mask; // Worker位掩码
};
Python使用示例:
python
# 使用Worker 0和Worker 1(位掩码:0b11 = 3)
vpp.api.nat44_ei_set_workers(worker_mask=3)
# 使用Worker 0、1、2(位掩码:0b111 = 7)
vpp.api.nat44_ei_set_workers(worker_mask=7)
CLI使用示例:
bash
vpp# nat44 ei set workers 0,1
11.11.2 nat44_ei_worker_dump - 转储Worker列表
生活化理解:
就像"查看所有NAT Worker线程的信息"。
使用场景:
- 配置检查:查看Worker配置
- 性能分析:分析Worker负载
API定义:
c
define nat44_ei_worker_dump {
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_worker_details {
option in_progress;
u32 context;
u32 worker_index; // Worker索引
u32 lcore_id; // CPU核心ID
string name[64]; // Worker名称
};
Python使用示例:
python
# 转储所有Worker
workers = vpp.api.nat44_ei_worker_dump()
for worker in workers:
print(f"Worker {worker.worker_index}: {worker.name}, "
f"lcore {worker.lcore_id}")
11.12 Frame Queue配置API
11.12.1 nat44_ei_set_fq_options - 设置Frame Queue选项
生活化理解:
就像"设置Worker之间转送数据包的队列大小"。
使用场景:
- 性能优化:根据流量调整队列大小
- 拥塞控制:防止队列溢出
API定义:
c
autoreply define nat44_ei_set_fq_options {
option in_progress;
u32 client_index;
u32 context;
u32 frame_queue_nelts; // Frame Queue元素数量
};
Python使用示例:
python
# 设置Frame Queue大小为1024
vpp.api.nat44_ei_set_fq_options(frame_queue_nelts=1024)
CLI使用示例:
bash
vpp# nat44 ei set fq-options 1024
11.12.2 nat44_ei_show_fq_options - 显示Frame Queue选项
生活化理解:
就像"查看当前Frame Queue的配置"。
API定义:
c
define nat44_ei_show_fq_options
{
option in_progress;
u32 client_index;
u32 context;
};
define nat44_ei_show_fq_options_reply
{
option in_progress;
u32 context;
i32 retval;
u32 frame_queue_nelts;
};
Python使用示例:
python
# 查看Frame Queue配置
reply = vpp.api.nat44_ei_show_fq_options()
print(f"Frame Queue size: {reply.frame_queue_nelts}")
11.13 IPFIX日志API
11.13.1 nat44_ei_ipfix_enable_disable - 启用/禁用IPFIX日志
生活化理解:
就像"启用/禁用NAT事件的日志记录"。
使用场景:
- 审计:记录NAT事件用于审计
- 分析:分析NAT流量模式
- 合规:满足合规要求
API定义:
c
autoreply define nat44_ei_ipfix_enable_disable {
option in_progress;
u32 client_index;
u32 context;
u32 domain_id; // 观察域ID
u16 src_port; // 源端口号
bool enable; // true=启用,false=禁用
};
Python使用示例:
python
# 启用IPFIX日志
vpp.api.nat44_ei_ipfix_enable_disable(
domain_id=1,
src_port=4739,
enable=True
)
# 禁用IPFIX日志
vpp.api.nat44_ei_ipfix_enable_disable(enable=False)
CLI使用示例:
bash
# 启用IPFIX日志
vpp# nat44 ei ipfix logging domain 1 src-port 4739
# 禁用IPFIX日志
vpp# nat44 ei ipfix logging disable
11.14 完整配置示例
11.14.1 典型企业网络配置
场景: 企业网络,需要NAT功能
python
import vpp_papi
# 连接到VPP
vpp = vpp_papi.VPPApiClient()
vpp.connect("vpp")
# 1. 启用NAT插件
vpp.api.nat44_ei_plugin_enable_disable(
inside_vrf=0,
outside_vrf=0,
users=10240,
sessions=102400,
user_sessions=10240,
enable=True,
flags=0
)
# 2. 添加NAT地址池
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="203.0.113.1",
last_ip_address="203.0.113.10",
vrf_id=0xFFFFFFFF,
is_add=True
)
# 3. 配置内部接口
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=1, # 内部接口
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_INSIDE,
is_add=True
)
# 4. 配置外部接口
vpp.api.nat44_ei_interface_add_del_feature(
sw_if_index=2, # 外部接口
flags=vpp_papi.VppEnum.vl_api_nat44_ei_config_flags_t.NAT44_EI_IF_OUTSIDE,
is_add=True
)
# 5. 设置超时时间
vpp.api.nat44_ei_set_timeouts(
udp=300,
tcp_established=3600,
tcp_transitory=240,
icmp=60
)
print("NAT44-EI configured successfully!")
11.14.2 服务器发布配置
场景: 将内部服务器发布到公网
python
# 1. Web服务器(HTTP)
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.10",
external_ip_address="203.0.113.1",
local_port=8080,
external_port=80,
protocol=6, # TCP
vrf_id=0,
tag="web-server"
)
# 2. HTTPS服务器
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.10",
external_ip_address="203.0.113.1",
local_port=8443,
external_port=443,
protocol=6, # TCP
vrf_id=0,
tag="https-server"
)
# 3. SSH服务器
vpp.api.nat44_ei_add_del_static_mapping(
is_add=True,
local_ip_address="192.168.1.20",
external_ip_address="203.0.113.2",
local_port=22,
external_port=2222,
protocol=6, # TCP
vrf_id=0,
tag="ssh-server"
)
11.14.3 HA配置示例
场景: 配置Active-Passive HA
python
# Active节点配置
# 1. 配置监听器(接收同步消息)
vpp.api.nat44_ei_ha_set_listener(
ip_address="10.0.0.1",
port=1234,
path_mtu=1500
)
# 2. 配置故障转移目标(Passive节点)
vpp.api.nat44_ei_ha_set_failover(
ip_address="10.0.0.2",
port=2345,
session_refresh_interval=60
)
# Passive节点配置
# 1. 配置监听器(接收同步消息)
vpp.api.nat44_ei_ha_set_listener(
ip_address="10.0.0.2",
port=2345,
path_mtu=1500
)
# 2. 触发Resync(同步所有会话)
vpp.api.nat44_ei_ha_resync(
want_resync_event=1,
pid=os.getpid()
)
11.15 API使用最佳实践
11.15.1 错误处理
生活化理解:
就像"检查操作是否成功"。
python
# 检查API返回值
reply = vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.7.100",
last_ip_address="10.15.7.100",
vrf_id=0xFFFFFFFF,
is_add=True
)
if reply.retval != 0:
print(f"Error: Failed to add address, retval={reply.retval}")
# 处理错误
else:
print("Address added successfully")
11.15.2 批量操作
生活化理解:
就像"一次性配置多个项目"。
python
# 批量添加地址
addresses = ["10.15.7.100", "10.15.7.101", "10.15.7.102"]
for addr in addresses:
vpp.api.nat44_ei_add_del_address_range(
first_ip_address=addr,
last_ip_address=addr,
vrf_id=0xFFFFFFFF,
is_add=True
)
11.15.3 配置检查
生活化理解:
就像"配置后检查是否正确"。
python
# 添加地址后检查
vpp.api.nat44_ei_add_del_address_range(
first_ip_address="10.15.7.100",
last_ip_address="10.15.7.100",
vrf_id=0xFFFFFFFF,
is_add=True
)
# 检查地址是否添加成功
addresses = vpp.api.nat44_ei_address_dump()
found = False
for addr in addresses:
if addr.ip_address == "10.15.7.100":
found = True
break
if not found:
print("Error: Address not found after adding")
11.16 总结
API分类:
- 插件管理:启用/禁用、配置查询
- 地址管理:添加/删除地址、地址查询
- 接口管理:配置内部/外部接口
- 静态映射:添加/删除静态映射
- 查询:用户查询、会话查询
- 会话管理:删除会话
- 高级功能:超时设置、转发控制、MSS Clamping
- 高可用性:HA配置、Resync
- Worker管理:Worker配置
- IPFIX:日志配置
使用建议:
- 先启用插件:在使用其他API前,先启用插件
- 添加地址:在配置接口前,先添加NAT地址
- 配置接口:明确指定内部和外部接口
- 检查返回值:始终检查API返回值
- 批量操作:使用循环进行批量操作
- 配置验证:配置后使用dump API验证
常见错误:
- 未启用插件:使用API前未启用插件
- 地址未添加:配置接口前未添加地址
- 接口方向错误:内部/外部接口配置错误
- 参数不匹配:删除时参数与添加时不匹配
- VRF不匹配:地址和接口的VRF不匹配
第十二章:文件依赖关系和调用关系
12.1 依赖关系图
nat44_ei.api
|
v
(API生成工具生成代码)
|
v
nat44_ei_api.c <--------┐
| |
v |
nat44_ei_cli.c |
| |
v |
nat44_ei_inlines.h <----┘
|
+---------------+---------------+
| | |
v v v
nat44_ei_in2out.c nat44_ei_out2in.c nat44_ei_handoff.c
| | |
+---------------+---------------+
|
v
nat44_ei_ha.h / nat44_ei_ha.c
|
v
nat44_ei_dpo.h / nat44_ei_dpo.c
|
v
nat44_ei.h
|
v
nat44_ei.c
12.2 核心依赖层次
12.2.1 最底层:核心定义(nat44_ei.h / nat44_ei.c)
作用:
- 定义所有核心数据结构
- 实现插件初始化和基础管理功能
- 提供全局主结构体
被依赖:
- 所有其他模块都依赖此文件
12.2.2 第二层:工具库(nat44_ei_inlines.h)
作用:
- 提供高性能内联函数
- NAT键值计算和查找辅助函数
依赖:
nat44_ei.h
被依赖:
- 所有数据包处理模块
12.2.3 第三层:数据包处理(nat44_ei_in2out.c / nat44_ei_out2in.c)
作用:
- 实现NAT转换的核心逻辑
依赖:
nat44_ei.hnat44_ei_inlines.h- NAT库文件
被依赖:
- CLI和API模块(用于查询)
12.2.4 第四层:高级特性(nat44_ei_ha.c / nat44_ei_dpo.c)
作用:
- 实现高可用性和DPO支持
依赖:
nat44_ei.hnat44_ei_inlines.h
被依赖:
nat44_ei.c(在会话管理中调用)
12.2.5 第五层:接口层(nat44_ei_cli.c / nat44_ei_api.c)
作用:
- 提供用户接口(CLI和API)
依赖:
- 所有底层模块
被依赖:
- 外部应用(通过API)
12.3 调用关系
12.3.1 初始化调用链
VPP启动
|
v
nat44_ei_init (nat44_ei.c)
|
+---> nat44_ei_set_node_indexes
|
+---> nat_dpo_module_init (nat44_ei_dpo.c)
|
+---> nat_ha_init (nat44_ei_ha.c)
|
+---> nat44_ei_api_hookup (nat44_ei_api.c)
12.3.2 数据包处理调用链
IN2OUT路径:
ip4-input
|
v
ip4-sv-reassembly-feature
|
v
nat44-ei-classify (nat44_ei.c)
|
v
nat44-ei-in2out (nat44_ei_in2out.c)
|
+---> nat44_ei_in2out_node_fn_inline
| |
| +---> nat44_ei_session_lookup_in2out (nat44_ei_inlines.h)
| |
| +---> slow_path (nat44_ei_in2out.c)
| |
| +---> nat44_ei_user_get_or_create (nat44_ei.c)
| |
| +---> nat44_ei_session_alloc_or_recycle (nat44_ei.c)
| |
| +---> nat_ha_sadd (nat44_ei_ha.c) [如果启用HA]
|
v
ip4-lookup
OUT2IN路径:
ip4-input
|
v
ip4-sv-reassembly-feature
|
v
nat44-ei-classify (nat44_ei.c)
|
v
nat44-ei-out2in (nat44_ei_out2in.c)
|
+---> nat44_ei_out2in_node_fn_inline
| |
| +---> nat44_ei_static_mapping_match (nat44_ei_inlines.h)
| |
| +---> nat44_ei_session_lookup_out2in (nat44_ei_inlines.h)
| |
| +---> slow_path (nat44_ei_out2in.c)
|
v
ip4-lookup
12.3.3 CLI命令调用链
用户输入CLI命令
|
v
VPP CLI框架
|
v
nat44_ei_cli.c中的命令处理函数
|
+---> nat44_ei_plugin_enable (nat44_ei.c)
|
+---> nat44_ei_add_address (nat44_ei.c)
|
+---> nat44_ei_add_interface (nat44_ei.c)
|
+---> nat44_ei_add_static_mapping (nat44_ei.c)
|
+---> 查询函数(访问nat44_ei_main全局结构)
12.3.4 API调用链
外部应用发送API消息
|
v
VPP API框架
|
v
nat44_ei_api.c中的消息处理函数
|
+---> 调用nat44_ei.c中的相应函数
|
+---> 生成回复消息
|
v
返回给外部应用
12.4 数据流
12.4.1 会话创建流程
数据包到达
|
v
nat44-ei-in2out节点
|
v
查找会话(nat44_ei_inlines.h)
|
v (未找到)
|
slow_path (nat44_ei_in2out.c)
|
+---> 获取/创建用户 (nat44_ei.c)
|
+---> 分配会话 (nat44_ei.c)
|
+---> 分配地址和端口 (nat44_ei.c)
|
+---> 插入哈希表 (nat44_ei.c)
|
+---> HA同步 (nat44_ei_ha.c) [如果启用]
|
v
执行NAT转换
12.4.2 会话查找流程
数据包到达
|
v
构建查找键 (nat44_ei_inlines.h)
|
v
查找IN2OUT/OUT2IN哈希表 (nat44_ei.c)
|
v (找到)
|
获取会话结构 (nat44_ei.c)
|
v
执行NAT转换
|
v
更新会话统计
12.5 关键接口
12.5.1 核心接口(nat44_ei.h)
管理接口:
nat44_ei_plugin_enable- 启用插件nat44_ei_plugin_disable- 禁用插件nat44_ei_add_interface- 添加接口nat44_ei_add_address- 添加地址nat44_ei_add_static_mapping- 添加静态映射
查询接口:
- 通过访问
nat44_ei_main全局结构查询状态
12.5.2 内联函数接口(nat44_ei_inlines.h)
查找接口:
nat44_ei_session_lookup_in2out- IN2OUT查找nat44_ei_session_lookup_out2in- OUT2IN查找nat44_ei_static_mapping_match- 静态映射匹配
工具接口:
calc_nat_key- 计算键值init_nat_k- 初始化键值
12.5.3 HA接口(nat44_ei_ha.h)
管理接口:
nat_ha_enable- 启用HAnat_ha_set_listener- 设置监听器nat_ha_set_failover- 设置故障转移
同步接口:
nat_ha_sadd- 会话添加nat_ha_sdel- 会话删除nat_ha_sref- 会话刷新
12.5.4 DPO接口(nat44_ei_dpo.h)
管理接口:
nat_dpo_create- 创建DPOnat_dpo_module_init- 初始化模块
12.6 总结
- 层次清晰:文件组织遵循清晰的层次结构
- 职责分离:每个文件有明确的职责
- 依赖合理:依赖关系单向,避免循环依赖
- 接口明确:通过头文件定义清晰的接口
- 可扩展性:模块化设计便于扩展和维护
后续章节说明
本文档目前提供了每个文件的概要和主要作用。后续将逐步展开每个章节的详细内容,包括:
- 详细代码分析:逐函数、逐代码块分析
- 数据流追踪:完整的数据包处理流程
- 设计模式:使用的设计模式和最佳实践
- 性能优化:性能优化技巧和原理
- 调试技巧:调试方法和工具使用
- 实战案例:结合实际场景的代码分析
每个章节的详细内容将在后续版本中逐步添加。