RT-Thread lwIP NAT 组件实战:在 RTOS 中为多网口设备实现 IPv4 网络地址转换

原创2026-10-03 12:07:141,702 阅读
文章标签:操作系统嵌入式RTOS物联网

RT-Thread lwIP NAT 组件实战:在 RTOS 中为多网口设备实现 IPv4 网络地址转换

本篇技术指南以 RT-Thread 仓库中 components/net/lwip-nat/README.md 为核心,系统讲解 lwIP NAT 组件的启用方式、NAT 表项配置结构与完整添加流程,并结合 ipv4_nat.h 与 ipv4_nat.c 的源码实现,深入剖析其针对 TCP、UDP、ICMP 的连接跟踪、源地址/端口改写、校验和增量修正以及超时回收机制。读完本文,你将能够在 RT-Thread 多网口设备(如以太网 + PPP/4G 模块)上独立配置并启用 NAT 转发,实现内网与外网的地址转换互通。

一、组件概览与典型应用场景

lwIP NAT 是 RT-Thread 网络组件中一个面向 IPv4 的网络地址转换模块,代码位于 components/net/lwip-nat,由 ipv4_nat.h、ipv4_nat.c、SConscript 与 README.md 四个文件构成。它基于 lwIP 1.4.1 的 IP 协议栈实现,支持 TCP、UDP 与 ICMP(ping) 三类报文的地址转换。

在嵌入式场景中,NAT 组件最典型的应用是:设备拥有一个可接入公网的接口(例如通过 PPP 拨号、4G/5G 模块获得动态公网地址的接口),同时挂载一个内部局域网接口(例如以太网 EMAC),此时希望内网设备通过该网关共享一个公网 IP 上网。该组件即可承担内网地址到公网地址的翻译工作,省去额外部署专用路由器/网关的硬件成本。

二、启用组件:两个宏缺一不可

要使用 lwIP NAT 组件,必须在 rtconfig.h 中同时满足两个编译开关:

  • RT_USING_LWIP:启用 RT-Thread 的 lwIP 协议栈;
  • LWIP_USING_NAT:启用 NAT 模块本身。

这一点可从构建脚本得到印证:components/net/lwip-nat/SConscript 中 DefineGroup('lwIP', src, depend = ['RT_USING_LWIP', 'LWIP_USING_NAT'], ...) 表明:只有同时定义这两个宏,ipv4_nat.c 才会被纳入编译。

同时,整个实现文件与头文件都使用条件编译保护,例如 ipv4_nat.h 与 ipv4_nat.c 均以 #ifdef LWIP_USING_NAT 包裹全部代码,未定义该宏时组件整体为空实现,不会引入任何运行时开销。因此在 rtconfig.h 中添加如下配置即可:

#define RT_USING_LWIP
#define LWIP_USING_NAT

提示:LWIP_USING_NAT 依赖 lwIP 侧的 IP_NAT 开关,NAT 的两个入口函数 ip_nat_input() / ip_nat_out() 正是通过 components/net/lwip/lwip-1.4.1/src/core/ipv4/ip.c 中的 #if IP_NAT 分支被声明并接入 IP 输入路径的。

三、核心数据结构:ip_nat_entry_t

NAT 规则通过 ip_nat_entry_t 结构体描述,其定义位于 ipv4_nat.h:

字段 类型 含义
source_net ip_addr_t 需要进行 NAT 转换的源网络(内网地址段)
source_netmask ip_addr_t 源网络的子网掩码
dest_net ip_addr_t 目的网络地址段
dest_netmask ip_addr_t 目的网络的子网掩码
out_if struct netif * 出口接口:报文被转换后从此接口发出(外网侧)
in_if struct netif * 入口接口:内网侧对应的网络接口

理解 out_if 与 in_if 是配置的关键:out_if 是报文改写后实际发出的外网接口,其 ip_addr 会成为转换后报文的源地址;in_if 用于响应报文(从外网回到内网方向)的送出,即 NAT 逆转换完成后把报文投递回内网所用的接口。

四、添加 NAT 表项:完整配置示例

README 中给出了一段可复用的 NAT 表项添加代码,其语义为:当报文从 213.129.231.168/29 网络发往目的网络 10.0.0.0/24 时执行地址转换。原文注释也特别指出这是一个"非典型"示例——大多数实际场景中源网络是内网、目的网络才是外网,方向正好相反,但配置流程完全一致:

ip_nat_entry_t nat_entry;

nat_entry.out_if = (struct netif *)&emac_if1;   /* 出口接口(外网侧) */
nat_entry.in_if  = (struct netif *)&emac_if2;   /* 入口接口(内网侧) */
IP4_ADDR(&nat_entry.source_net, 213, 129, 231, 168);    /* 源网络地址 */
IP4_ADDR(&nat_entry.source_netmask, 255, 255, 255, 248); /* 源网络掩码,即 /29 */
IP4_ADDR(&nat_entry.dest_net, 10, 0, 0, 0);              /* 目的网络地址 */
IP4_ADDR(&nat_entry.dest_netmask, 255, 0, 0, 0);         /* 目的网络掩码,即 /24 */
ip_nat_add(&_nat_entry);

几点说明:

  • IP4_ADDR 是 lwIP 提供的按字节构造 IPv4 地址的宏,参数依次为地址指针与四个十进制数字段;
  • 注意 README 原示例中 dest_netmask 一行误写为 source_netmask(源码注释 ipv4_nat.c 中同样存在该笔误),实际应以 255.0.0.0 作为 dest_net 的掩码,否则 dest_net 无法正确解析;
  • 上面示例中的 emac_if1、emac_if2 为示意性接口变量,实际项目中应替换为 rt_device_find/网卡注册后获得的真实 struct netif 指针,例如通过 eth_dev 与 netif 的关联获取;
  • 建议在网络接口初始化完成、IP 地址配置就绪之后再调用 ip_nat_add()。

表项的底层实现:链表式配置存储

从源码看,ip_nat_add()(ipv4_nat.c)将传入的 ip_nat_entry_t 拷贝进动态分配的 ip_nat_conf_t 节点(mem_malloc),并挂在全局链表 ip_nat_cfg 尾部;内存不足时返回 ERR_MEM,成功返回 ERR_OK。与之对应,ip_nat_remove()(ipv4_nat.c)按六元组(源网络、源掩码、目的网络、目的掩码、out_if、in_if)精确匹配后,先复位该表项关联的所有状态表条目,再从链表中摘除并释放内存。

规则匹配逻辑(ip_nat_shallnat(),ipv4_nat.c):遍历配置链表,只要报文的目的地址命中 dest_net/dest_netmask,或源地址命中 source_net/source_netmask,即认为该报文需要 NAT。因此你也可以用该组件实现仅针对特定目的网络的单向转换策略。

五、运行时工作原理:状态表 + 双向挂钩

NAT 组件通过 ip_nat_init() 初始化,并在 lwIP 的 IP 输入路径中挂接两个钩子完成双向转换(见 components/net/lwip/lwip-1.4.1/src/core/ipv4/ip.c):

  • 出口方向(内网 → 外网):当报文目的地址不归属本机时,ip_input() 调用 ip_nat_out()(ip.c 第 505 行)进行正向转换;
  • 入口方向(外网 → 内网):在本机向上层协议栈递交前,ip_nat_input()(ip.c 第 578 行)检查是否为已建立 NAT 会话的响应报文,若是则执行逆转换并直接经 in_if 发出,被"消费"的报文不再进入上层。

1. 出口转换与连接跟踪

ip_nat_out()(ipv4_nat.c)先调用 ip_nat_shallnat() 判定是否需要转换,然后按协议分流:

  • TCP/UDP:调用 ip_nat_tcp_lookup_outgoing() / ip_nat_udp_lookup_outgoing()(入口参数 allocate=1)在对应状态表中按四元组(源 IP、目的 IP、源端口、目的端口)查找会话;未命中时分配一个空闲表项,记录原始 sport/dport,并为该连接分配新的源端口 nport。源码默认新端口从 LWIP_NAT_DEFAULT_TCP_SOURCE_PORT / LWIP_NAT_DEFAULT_UDP_SOURCE_PORT(均为 40000)起始(ipv4_nat.c);
  • ICMP:在 LWIP_NAT_DEFAULT_STATE_TABLES_ICMP(默认 4 条)的 ICMP 表中记录 echo 请求的 id 与 seqno,用于回应报文的逆转换匹配(ipv4_nat.c)。

转换动作本身分两步:将 IP 源地址改写为 out_if->ip_addr,将 TCP/UDP 源端口改写为新分配的 nport。改写的同时必须修正校验和,组件采用增量校验和算法 ip_nat_chksum_adjust()(ipv4_nat.c)——基于 RFC 1624 的增量更新方法,只针对发生变化的地址/端口字段做旧值减、新值加的回卷计算,避免对整个报文重新求和,显著降低 CPU 开销。

2. 入口逆转换

ip_nat_input()(ipv4_nat.c)按协议类型在状态表中查找匹配项:

  • TCP 按 (src IP == 会话 dest) && (src port == 会话 dport) && (dest port == 会话 nport) 匹配,命中后把目的端口恢复为原始 sport,目的 IP 恢复为原始内网地址,并刷新会话 TTL;
  • UDP 逻辑与 TCP 完全对称;
  • ICMP 则按回显应答的 id 与 seqno 精确匹配。

逆转换完成后,组件将报文通过 in_if->output() 直接送出到内网接口,并在头部空间不足时自动追加 pbuf 以容纳链路层头部。整个转换过程对上层协议完全透明,被消费的报文不会进入 UDP/TCP/RAW 上层分发(ip.c 第 576-581 行)。

3. 状态表规模与超时回收

状态表全部为编译期静态数组,各协议表规模由宏控制:

宏 默认值 说明
LWIP_NAT_DEFAULT_STATE_TABLES_ICMP 4 ICMP 会话表项数(ipv4_nat.c)
LWIP_NAT_DEFAULT_STATE_TABLES_TCP 32 TCP 会话表项数(ipv4_nat.c)
LWIP_NAT_DEFAULT_STATE_TABLES_UDP 32 UDP 会话表项数(ipv4_nat.c)
LWIP_NAT_DEFAULT_TTL_SECONDS 128 会话默认存活时间(秒),命中后刷新(ipv4_nat.c)
LWIP_NAT_TMR_INTERVAL_SEC 30 定时巡检间隔(秒),定义于 ipv4_nat.h

ip_nat_init()(ipv4_nat.c)在关调度器(rt_enter_critical)的保护下,通过 sys_timeout() 注册周期任务 nat_timer,每隔 LWIP_NAT_TMR_INTERVAL_SEC 秒调用一次 ip_nat_tmr()(ipv4_nat.c)。巡检时对每条会话执行 ip_nat_check_timeout():TTL 大于巡检间隔则直接减去间隔,否则清零,从而逐步淘汰过期会话,回收状态表槽位供新连接使用。

六、从源码注释看设计边界

ipv4_nat.c 的 TODO 注释揭示了当前实现的已知边界,配置与评估时需注意:

  • 当前实现不转发广播地址,ip_input() 中 NAT 挂钩均有 !ip_addr_isbroadcast() 前置判断;
  • 多个内网客户端同时发起 ICMP ping 时,ICMP 表项(仅 4 条)可能耗尽,源码注释也指出应分配 ICMP ping id 应对多客户端场景;
  • 状态表为线性查找的静态数组,表项打满后新连接会被静默丢弃(调试日志提示 "no more NAT entries available");
  • 表项移除时通过 ip_nat_reset_state() 复位关联会话,避免悬挂指针。

调试方面,可通过定义 LWIP_NAT_DEBUG(默认关闭,见 ipv4_nat.c)并开启 LWIP_DEBUG,启用 ip_nat_dbg_dump_* 系列函数输出每条 TCP/UDP/ICMP 会话的原始映射与转换后映射,便于验证规则是否生效。

七、实践建议

  1. 先确认协议栈版本:本组件实现位于 lwIP 1.4.1 路径(components/net/lwip/lwip-1.4.1)下,使用前请确认工程所选 lwIP 版本与其匹配;
  2. 核对接口指针:out_if/in_if 必须指向已初始化且 netif_is_up 的接口,否则转换后的报文无法正常发出;
  3. 典型配置方向:实际产品中通常配置为内网(如 192.168.1.0/24)→ 外网(0.0.0.0/0)方向,即 source_net 为内网段、dest_net 覆盖全部目的地址;
  4. 评估并发规模:默认 TCP/UDP 各 32 条会话表对多数嵌入式网关足够,但高并发场景需适当增大对应宏并评估静态内存开销;
  5. 观察调试输出:先在开发板上以 LWIP_NAT_DEBUG 打开日志,确认 ip_nat_add() 返回 ERR_OK 且会话建立后,再关闭调试进入正式运行。

通过本文的配置步骤与源码剖析,你可以直接在 RT-Thread 工程中启用 lwIP NAT,让单网口或双网口设备承担网关转发职能,实现内网设备对外网的共享访问。

登录后查看全文
rt-thread