RT-Thread lwIP NAT 组件实战:在 RTOS 中为多网口设备实现 IPv4 网络地址转换
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 会话的原始映射与转换后映射,便于验证规则是否生效。
七、实践建议
- 先确认协议栈版本:本组件实现位于 lwIP 1.4.1 路径(components/net/lwip/lwip-1.4.1)下,使用前请确认工程所选 lwIP 版本与其匹配;
- 核对接口指针:
out_if/in_if必须指向已初始化且netif_is_up的接口,否则转换后的报文无法正常发出; - 典型配置方向:实际产品中通常配置为内网(如 192.168.1.0/24)→ 外网(0.0.0.0/0)方向,即
source_net为内网段、dest_net覆盖全部目的地址; - 评估并发规模:默认 TCP/UDP 各 32 条会话表对多数嵌入式网关足够,但高并发场景需适当增大对应宏并评估静态内存开销;
- 观察调试输出:先在开发板上以
LWIP_NAT_DEBUG打开日志,确认ip_nat_add()返回ERR_OK且会话建立后,再关闭调试进入正式运行。
通过本文的配置步骤与源码剖析,你可以直接在 RT-Thread 工程中启用 lwIP NAT,让单网口或双网口设备承担网关转发职能,实现内网设备对外网的共享访问。