进阶:VPP NAT44-EI 全面源码解析

目录

  • 第一章:概述
  • [第二章:核心主文件 - 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)
  • 第十二章:文件依赖关系和调用关系

学习本章可能需要提前了解的文章

NAT 技术全景解析:从入门到精通

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

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 关键设计特点

  1. 全局单例模式 :使用 nat44_ei_main_t nat44_ei_main 作为全局主结构,所有模块共享
  2. 多线程支持 :通过 per_thread_data 数组管理每个线程的数据,避免锁竞争
  3. 哈希表优化 :使用 clib_bihash_8_8_t 实现O(1)查找性能,支持多线程安全
  4. 端口位图管理:使用位图(bitmap)高效管理65536个端口的分配状态
  5. LRU机制:使用双向链表实现会话的LRU管理,自动回收最旧会话
  6. 向量化处理:使用VPP的向量化框架,批量处理数据包
  7. 预测优化 :使用 PREDICT_TRUE / PREDICT_FALSE 提示编译器优化分支
  8. 引用计数:外部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:

  1. 5元组不一致:NAT转换后,双向数据包的5元组完全不同
  2. 会话查找需求:OUT2IN方向需要根据外部IP:端口查找会话,RSS无法保证找到正确的Worker
  3. 状态一致性:NAT是有状态的,需要保证同一会话的所有数据包在同一Worker上处理
  4. 用户管理:需要按用户(内部IP)管理会话,RSS无法提供这种粒度

NAT的解决方案:

  1. IN2OUT:根据源IP哈希选择Worker,确保同一用户在同一Worker
  2. OUT2IN:根据目标端口选择Worker,端口分配时已考虑Worker分配
  3. 端口分配策略:每个Worker分配固定范围的端口,便于反向查找
  4. 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需要知道"这个数据包应该走哪个门"

生活化例子:

想象你是一个门卫,负责:

  1. 检查每个进出的人(数据包)
  2. 决定是否需要换证件(NAT转换)
  3. 决定应该走哪个门(路由决策)

场景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中的核心作用:

  1. 路由决策:告诉NAT数据包应该走哪个接口
  2. 接口识别:通过FIB查找确定接口类型(内部/外部),判断是否需要NAT
  3. 地址登记:将NAT地址登记到FIB,确保OUT2IN数据包能被正确识别
  4. 多VRF支持:使用FIB索引区分不同VRF中的会话
  5. 性能优化: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;
}

处理步骤总结:

  1. 查找映射

    • 优先查找静态映射(固定地址簿)
    • 如果找不到,查找活动会话(临时地址簿)
  2. Worker检查

    • 如果会话在其他Worker上,返回需要handoff
    • 确保在正确的Worker上处理(多工作台场景)
  3. 防循环检查

    • 检查目标地址和端口是否有变化
    • 如果没有变化,不处理(避免无限循环)
  4. 执行转换

    • 修改目标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的核心价值:

  1. 功能完整性:支持内部主机通过外部地址访问内部服务器
  2. 用户体验:用户可以使用统一的外部地址访问服务
  3. 网络简化:避免数据包绕到外部网络再回来
  4. 安全性:内部流量保持在内部网络

Hairpinning的实现要点:

  1. Feature Arc集成:在ip4-local弧上运行,处理发往本地IP的数据包
  2. 映射查找:优先静态映射,其次活动会话
  3. 地址转换:将外部地址转换为内部地址
  4. 路由重定向:转换后重新路由到内部接口
  5. Worker支持:支持多Worker场景,需要handoff时进行handoff

Hairpinning的应用场景:

  1. 内部服务器访问:内部客户端通过外部地址访问内部服务器
  2. VPN场景:VPN客户端访问内部资源
  3. 负载均衡:内部客户端通过负载均衡地址访问
  4. 统一入口:提供统一的外部访问入口

生活化总结:

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 设计特点

  1. 内联优化 :所有函数都是 always_inline,减少函数调用开销
  2. 位操作优化:使用位操作实现高效的键值计算和分解
  3. 缓存机制:接口地址检查使用运行时缓存,避免重复查询
  4. 性能关键路径:这些函数在数据包处理的快速路径中被频繁调用

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

核心任务:

  1. 检查员工是否有"临时工牌"(查找会话)
  2. 如果没有,制作一个"临时工牌"(创建会话)
  3. 把员工的"内部工牌"换成"临时工牌"(转换地址和端口)
  4. 让员工出门(转发数据包)

4.2 处理流程:Fast Path vs Slow Path

生活化理解:

Fast Path(快速通道)

  • 员工已经有"临时工牌"了
  • 门卫直接查记录,换工牌,放行
  • 非常快,几微秒就完成

Slow Path(慢速通道)

  • 员工第一次出门,没有"临时工牌"
  • 门卫需要:
    1. 检查员工档案(创建/查找用户)
    2. 制作"临时工牌"(分配外部IP和端口)
    3. 登记到记录本(插入哈希表)
    4. 换工牌,放行
  • 比较慢,需要几十微秒

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

核心任务:

  1. 检查"临时工牌"是否有效(查找会话)
  2. 如果没有,检查是否有静态映射(固定访客)
  3. 把"临时工牌"换成"内部工牌"(转换地址和端口)
  4. 让访客进入(转发数据包)

5.2 处理流程:静态映射优先

生活化理解:

OUT2IN的特殊性:

  • 大多数数据包是返回流量(已有会话)
  • 少数数据包是外部发起的连接(需要静态映射)
  • 静态映射优先:先查静态映射,再查动态会话

处理流程:

  1. 提取目标IP和端口(外部地址+端口)
  2. 先查静态映射(是否有固定访客记录)
  3. 再查动态会话(是否有临时工牌记录)
  4. 如果都没有,且没有静态映射,丢弃数据包
  5. 如果有静态映射,创建会话(仅静态映射支持外部发起)

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头

处理流程:

  1. 提取ICMP错误消息中的原始IP头
  2. 查找原始IP头对应的会话
  3. 转换原始IP头中的地址和端口
  4. 更新ICMP校验和

5.5 关键设计特点

  1. 静态映射优先:提高性能,静态映射查找更快
  2. 端点无关特性:同一个外部IP:端口可以映射到不同的内部主机(根据目标地址)
  3. 仅静态映射支持外部发起:动态会话不支持外部发起的连接,提高安全性

第六章:多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?

  1. 会话亲和性:同一会话的所有数据包必须在同一个Worker上处理
  2. 避免锁竞争:每个Worker有独立的会话表,不需要加锁
  3. 性能优化:本地会话表查找,避免跨线程访问

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把数据包放入转送箱:

  1. 调用vlib_buffer_enqueue_to_thread函数
  2. 数据包被放入目标Worker的Frame Queue
  3. 使用原子操作确保线程安全

出队操作(Dequeue):

目标Worker从转送箱取出数据包:

  1. Worker主循环检查Frame Queue
  2. 调用vlib_frame_queue_dequeue_inline函数
  3. 数据包被取出并放入目标节点的Frame

关键特性:

  • 无锁设计:使用原子操作和内存屏障
  • 批量处理:一次处理多个数据包
  • 高性能:避免锁竞争,支持百万级数据包/秒

详细实现原理请参考:

  • 《VPP Frame Queue无锁队列详解:从原理到实现》

6.4 Worker线程的运行流程

6.4.1 Worker线程的主循环

生活化理解:

Worker线程就像银行的"工作循环":

  1. 检查转送箱(Frame Queue)是否有业务
  2. 处理输入业务(Input节点)
  3. 处理转送来的业务(Frame Queue出队)
  4. 处理待处理的业务(Pending Frames)
  5. 重复循环

代码实现(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机制的核心价值:

  1. 会话亲和性:确保同一会话的数据包在同一Worker上处理
  2. 无锁设计:使用Frame Queue避免锁竞争,提高性能
  3. 负载均衡:通过哈希算法实现负载均衡
  4. 拥塞控制:检测并处理Frame Queue拥塞

Frame Queue机制的核心价值:

  1. 跨线程通信:实现无锁的跨线程数据包传输
  2. 批量处理:一次处理多个数据包,提高效率
  3. 性能优化:使用原子操作和缓存行对齐,避免false sharing

Worker线程运行流程:

  1. 主循环:检查Frame Queue → 处理Input节点 → 处理Pending Frames
  2. Handoff流程:确定目标Worker → 放入Frame Queue → 目标Worker取出处理
  3. 数据包流程: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确认和重传机制
  • 同步内容:会话的创建、删除、统计更新

协议特点(根据官方文档):

  1. UDP传输:会话同步流量通过IPv4 UDP连接传输
  2. ACK确认:Passive节点收到数据后必须回复ACK
  3. 重传机制:Active节点记录每个发送的包,维护定时器
  4. 超时重传:如果定时器超时前没收到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 事件类型

三种事件类型:

  1. NAT_HA_ADD(会话添加)

    • 当Active节点创建新会话时发送
    • Passive节点收到后创建相同的会话
  2. NAT_HA_DEL(会话删除)

    • 当Active节点删除会话时发送
    • Passive节点收到后删除对应的会话
  3. NAT_HA_REFRESH(会话刷新)

    • 定期发送,更新会话统计信息
    • 保持Passive节点的统计信息与Active节点一致

7.4 会话同步流程详解

7.4.1 发送端:Active节点发送会话事件

生活化理解:

就像主分行实时向备分行发送"档案变更通知":

  1. 创建新档案(ADD事件)
  2. 删除旧档案(DEL事件)
  3. 更新档案统计(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节点处理会话事件

生活化理解:

就像备分行接收"档案变更通知"并更新档案:

  1. 接收UDP消息
  2. 解析事件
  3. 更新本地会话表
  4. 回复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的核心价值:

  1. 高可用性:Active节点故障时,Passive节点可以无缝接管
  2. 会话一致性:Passive节点与Active节点保持会话一致
  3. 可靠性:通过ACK和重传机制保证消息可靠传输
  4. 性能优化:批量发送和增量同步,减少网络开销

完整流程总结:

  1. 配置阶段:Active和Passive节点配置HA监听器和故障转移
  2. 运行阶段:Active节点实时同步会话变化到Passive节点
  3. 故障切换:Active节点故障时,Passive节点接管(需要外部机制触发)
  4. 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类型包括:

  1. Load-balance:ECMP集合中的选择
  2. Adjacency:应用重写并通过接口转发
  3. MPLS-label:添加MPLS标签
  4. Lookup:在不同表中执行查找
  5. 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源)

关键函数说明:

  1. nat_dpo_create:创建NAT DPO实例
  2. fib_table_entry_special_dpo_add:在FIB表中添加特殊条目,使用DPO作为转发路径
  3. 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的核心价值:

  1. 性能提升:通过FIB表直接路由,减少节点跳转
  2. 简化路径:跳过Feature Arc的分类节点
  3. FIB集成:与VPP的FIB系统深度集成
  4. 可选特性:可以根据需求启用/禁用

使用场景:

  1. 高性能场景:需要高性能的OUT2IN转发
  2. 简单配置:不需要复杂的Feature Arc配置
  3. FIB路由:适合使用FIB表进行路由的场景

实现要点:

  1. DPO注册:向VPP的DPO框架注册NAT DPO类型
  2. FIB安装:在添加NAT地址时,将DPO安装到FIB表
  3. 节点映射:指定DPO对应的VLIB节点(nat44-ei-out2in)
  4. 路径优化:数据包通过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 关键设计特点

  1. 用户友好:提供清晰的命令语法和帮助信息
  2. 详细输出:支持verbose模式显示详细信息
  3. 错误处理:完善的错误检查和提示
  4. 格式化输出:美观的表格和列表格式

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 关键设计特点

  1. 类型安全:使用生成的API类型定义
  2. 异步处理:支持异步API调用
  3. 错误处理:完善的错误码和错误消息
  4. 批量操作:支持批量查询和操作

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_memorysession_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_heardtime_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分类:

  1. 插件管理:启用/禁用、配置查询
  2. 地址管理:添加/删除地址、地址查询
  3. 接口管理:配置内部/外部接口
  4. 静态映射:添加/删除静态映射
  5. 查询:用户查询、会话查询
  6. 会话管理:删除会话
  7. 高级功能:超时设置、转发控制、MSS Clamping
  8. 高可用性:HA配置、Resync
  9. Worker管理:Worker配置
  10. IPFIX:日志配置

使用建议:

  1. 先启用插件:在使用其他API前,先启用插件
  2. 添加地址:在配置接口前,先添加NAT地址
  3. 配置接口:明确指定内部和外部接口
  4. 检查返回值:始终检查API返回值
  5. 批量操作:使用循环进行批量操作
  6. 配置验证:配置后使用dump API验证

常见错误:

  1. 未启用插件:使用API前未启用插件
  2. 地址未添加:配置接口前未添加地址
  3. 接口方向错误:内部/外部接口配置错误
  4. 参数不匹配:删除时参数与添加时不匹配
  5. 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.h
  • nat44_ei_inlines.h
  • NAT库文件

被依赖:

  • CLI和API模块(用于查询)
12.2.4 第四层:高级特性(nat44_ei_ha.c / nat44_ei_dpo.c)

作用:

  • 实现高可用性和DPO支持

依赖:

  • nat44_ei.h
  • nat44_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 - 启用HA
  • nat_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 - 创建DPO
  • nat_dpo_module_init - 初始化模块

12.6 总结

  1. 层次清晰:文件组织遵循清晰的层次结构
  2. 职责分离:每个文件有明确的职责
  3. 依赖合理:依赖关系单向,避免循环依赖
  4. 接口明确:通过头文件定义清晰的接口
  5. 可扩展性:模块化设计便于扩展和维护

后续章节说明

本文档目前提供了每个文件的概要和主要作用。后续将逐步展开每个章节的详细内容,包括:

  1. 详细代码分析:逐函数、逐代码块分析
  2. 数据流追踪:完整的数据包处理流程
  3. 设计模式:使用的设计模式和最佳实践
  4. 性能优化:性能优化技巧和原理
  5. 调试技巧:调试方法和工具使用
  6. 实战案例:结合实际场景的代码分析

每个章节的详细内容将在后续版本中逐步添加。

相关推荐
Lynnxiaowen9 小时前
今天我们继续学习Kubernetes内容pod资源对象
运维·学习·容器·kubernetes·云计算
diudiu_339 小时前
web漏洞--认证缺陷
java·前端·网络
小李独爱秋11 小时前
计算机网络经典问题透视:TCP的“误判”——非拥塞因素导致的分组丢失
服务器·网络·tcp/ip·计算机网络·智能路由器·php
方山子哦12 小时前
来郑州上班3周搞了两个项目!
网络
white-persist12 小时前
【攻防世界】reverse | re1-100 详细题解 WP
c语言·开发语言·网络·汇编·python·算法·网络安全
飞行增长手记12 小时前
什么是IP纯净度?为什么它能决定账号安全与访问效率?
网络
普普通通的南瓜13 小时前
一年期免费IP证书,为公网IP地址提供HTTPS加密
网络·网络协议·tcp/ip·安全·http·金融·https
谷粒.13 小时前
测试数据管理难题的7种破解方案
运维·开发语言·网络·人工智能·python
The star"'13 小时前
ceph(5-8)
运维·ceph·云计算