简介:面向 VxWorks 嵌入式网络开发者,这份资源是一套基于风河系统的网络地址转换功能实现源码,核心围绕 IP 协议栈钩子机制展开,用于解决嵌入式设备在连接公网时的地址转换与访问管控问题。压缩包共 45 个文件,以 C 语言源码为主,辅以对应头文件、构建脚本、管理信息库文件及一份说明文档,整体体积仅 145 KB,结构紧凑便于研读。目前已有 104 位开发者学习下载。源码完整覆盖网络地址转换任务初始化、过滤钩子注册、传输层协议处理、分片重组、连接状态维护与清理回收等环节,同时提供多种应用层网关算法示例,适合希望深入理解 VxWorks 网络协议栈扩展机制,或需要在真实项目中快速移植网络地址转换功能的研发人员参考。这些模块按功能分层组织,既可用于单独调试,也可作为整体参考实现,对研究嵌入式协议栈的开发者很有帮助。
1. 从 ip_hook 说起:VxWorks 的 NAT 为什么和 Linux 不一样
拿到nat.rar这份源码时,第一反应是看它的发布时间和代码风格——nat_ip.c里大量使用K&R风格函数声明,nat_structures.h里手工维护的LIST结构而不是内核链表,这基本可以断定是 Wind River 早期为 VxWorks 5.x / Tornado 2.x 时代写的协议栈级 NAT 实现。和我们熟悉的 Linux 内核netfilter+conntrack不同,VxWorks 的 NAT 实现机制是纯粹的hook 回调:在 IP 协议栈的收发路径上注册钩子函数,数据包每经过一个挂载点,NAT 模块就获得一次检查和修改的机会。
这套代码里没有iptables这样的用户态配置工具链,所有规则都通过nat_configuration.h里的静态配置和nat_api.c提供的natAddMap()、natDeleteMap()等接口完成。对做嵌入式网络设备的人来说,理解这份代码的意义在于:它展示了在资源受限的 RTOS 环境下,如何不依赖内核模块机制,仅靠协议栈预留的 hook 点完成地址转换、状态维护和 ALG 支持。源码包里natFtpAlg.c、natPPTPPTAlg.c、natH323Alg.c三个 ALG 文件,正好覆盖了 FTP 主动/被动模式、PPTP GRE 隧道、H.323 信令这三类最容易让 NAT 翻车的协议。适合需要在 VxWorks 上做 NAT 功能移植、或者想理解嵌入式 NAT 状态机设计的开发者阅读。
2. VxWorks NAT 的核心数据结构与初始化链路
2.1nat_structures.h里的映射表模型:全局表、静态映射和动态映射
打开nat_structures.h,最值得先看的是它定义的 NAT 映射结构体。这套代码把映射分成三类:全局地址映射(全局 NAT 地址池)、静态映射(手工指定内部 IP 到外部 IP 的一对一关系)、动态映射(外出连接临时分配的映射)。代码片段如下:
/* 每个 NAT 映射条目的核心结构 */ typedef struct nat_mapping { struct nat_mapping *next; /* 哈希链下一节点 */ struct nat_mapping *prev; /* 哈希链前一节点 */ LIST_ENTRY lEntry; /* 双向链表节点,用于定时器扫描 */ IP_ADDR LocalIPAddr; /* 内部 IP */ IP_ADDR GlobalIPAddr; /* 外部 IP */ IP_ADDR SrcIPAddr; /* 实际发送方 IP */ unsigned short LocalPort; /* 内部端口 */ unsigned short GlobalPort; /* 外部端口 */ unsigned short SrcPort; /* 实际源端口 */ unsigned short Protocol; /* TCP / UDP / ICMP */ unsigned long Age; /* 条目的时间戳,用于超时扫描 */ unsigned char TcpState; /* TCP 连接状态跟踪 */ unsigned char MapType; /* 静态 or 动态 */ } NAT_MAPPING;这段代码揭示了 VxWorks NAT 的核心设计思路:用一个NAT_MAPPING结构同时承载五元组信息和 TCP 状态。next和prev指针表明映射表使用哈希链组织,而lEntry是独立的双向链表节点,用于定时器模块做超时扫描。注意到TcpState字段——帧代码在nat_tcp_state_structures.h中单独定义了一套 TCP 状态枚举,说明它对 TCP 连接不只做地址转换,还会跟踪连接状态以决定何时回收映射条目。
2.2 初始化入口:natInit.c与nat_task_init.c的分工
natInit.c是 NAT 模块的初始化入口,它做的事情按顺序可以拆成三步:第一,调用natGlobalInit()初始化全局变量和映射表;第二,调用natTaskInit()创建 NAT 定时器任务;第三,调用natFilterInstallHook()注册 IP 层钩子。初始化代码的关键路径如下:
/* natInit.c 中的初始化流程 */ STATUS natInit(void) { /* 初始化 NAT 全局状态、配置参数结构体 */ if (natGlobalInit() != OK) { return ERROR; } /* 创建 NAT 定时器任务:负责映射条目超时回收 */ if (natTaskInit() != OK) { return ERROR; } /* 注册 IP 层钩子函数:natFilterHook() */ if (natFilterInstallHook() != OK) { return ERROR; } return OK; }natFilterInstallHook()实际上调用的是 VxWorks 协议栈的ipHookAdd()接口。这个接口允许外部模块在 IP 输入路径和输出路径上各挂一个回调函数。挂载完成后,所有进出协议栈的 IP 数据包都会先经过natFilterHook(),由它决定是直通、转换还是丢弃。注意这里有个容易被忽略的点:nat_task_init.c创建的定时器任务使用了taskSpawn(),优先级通常设为 150 左右,低于网络任务优先级,避免抢占协议栈处理。
2.3 配置结构体与nat_configuration.h的编译期约束
nat_configuration.h是一种编译期配置,它决定 NAT 模块支持哪些功能。常见的宏定义及其作用如下表:
| 宏名称 | 作用 | 置 0 的后果 |
|---|---|---|
NAT_CFG_UDP_TIMEOUT | UDP 映射条目超时时间(秒) | UDP 映射永不超时,表项会越积越多 |
NAT_CFG_TCP_TIMEOUT | TCP 映射条目超时时间(秒) | TCP 连接断开后映射不回收 |
NAT_CFG_ALG_FTP | 启用 FTP ALG 支持 | FTP 数据连接无法穿越 NAT |
NAT_CFG_ALG_PPTP | 启用 PPTP ALG 支持 | GRE 隧道无法建立 |
NAT_CFG_MAX_MAPPING | 映射表最大条目数 | 超过后新的映射分配失败 |
NAT_CFG_ENABLE_ICMP | 启用 ICMP 转换 | 内网 ping 外网失败 |
在移植时,NAT_CFG_MAX_MAPPING的值要结合设备内存估算。一个NAT_MAPPING结构体在 32 位 VxWorks 下约占用 60 字节左右,若配置 1024 条映射,大约消耗 60KB 内存外加哈希表本身的开销,这在传统 VxWorks 设备上需要提前规划。定时器任务每个 tick(通常 1 秒)扫描一次映射链表,NAT_CFG_MAX_MAPPING越大,单次扫描耗时越长。
3. 数据面主路径:filter hook 里的转发决策与地址转换
3.1nat_filter_hook.c的挂载逻辑与 mBlk 操作
NAT 模块最核心的代码在nat_filter_hook.c,它提供了natFilterHook()作为 IP 层回调函数。VxWorks 网络协议栈使用 mBlk 链来管理数据包缓冲区,所以钩子函数的第一步是从M_BLK结构中提取 IP 头部信息。这里的代码比较有代表性:
/* nat_filter_hook.c — 钩子主函数,截获 IP 包并交给 NAT 处理 */ int natFilterHook(M_BLK_ID pMblk, int dir, int *pResult) { IP_ADDR srcAddr, dstAddr; unsigned short srcPort, dstPort; NAT_MAPPING *pMapping = NULL; /* 从 mBlk 中取出 IP 头 */ struct ip *pIpHdr = (struct ip *)pMblk->mBlkHdr.mHdr.mData; srcAddr = ntohl(pIpHdr->ip_src.s_addr); dstAddr = ntohl(pIpHdr->ip_dst.s_addr); /* 根据方向选择处理逻辑 */ if (dir == IP_HOOK_IN) { /* 入方向:目的地址是 NAT 全局地址,查映射并改写为目的内网地址 */ pMapping = natFindMappingByGlobal(dstAddr, dstPort, protocol); if (pMapping != NULL) { natApplyInboundTranslation(pMblk, pMapping); } } else if (dir == IP_HOOK_OUT) { /* 出方向:源地址是内网地址,分配映射并改写源地址 */ pMapping = natFindMappingByLocal(srcAddr, srcPort, protocol); if (pMapping == NULL) { pMapping = natCreateNewMapping(pMblk); } if (pMapping != NULL) { natApplyOutboundTranslation(pMblk, pMapping); } } *pResult = IP_HOOK_OK; /* 允许数据包继续走协议栈 */ return OK; }这里dir参数的取值由协议栈的ipHookAdd()调用方式决定:入方向是IP_HOOK_IN,出方向是IP_HOOK_OUT。代码中取 IP 头时直接用了pMblk->mBlkHdr.mHdr.mData的偏移,这是 VxWorks 6.x 之前版本的典型写法;在 6.x 以上的END_OBJ驱动模型下,应该改用netMBlkGet()等接口安全检查 mBlk 的有效性。
3.2nat_ip.c中的校验和更新:硬伤重灾区
NAT 修改 IP 头后必须同步更新校验和,这是嵌入式 NAT 最常见的 bug 来源。VxWorks NAT 的nat_ip.c提供了一套增量校验和更新函数,它没有调用协议栈的ipCksum()重算整个头,而是采用 RFC 1624 的增量更新算法,只调整被修改的字段。看代码:
/* nat_ip.c — 增量更新 IP 头校验和 */ void natUpdateIpChecksum(M_BLK_ID pMblk, unsigned short *checksum, unsigned short oldVal, unsigned short newVal) { unsigned long sum; /* 将 oldVal 从校验和中减去,再加上 newVal */ sum = *checksum + (~oldVal & 0xffff); sum += newVal; /* 处理进位回卷 */ while (sum >> 16) { sum = (sum & 0xffff) + (sum >> 16); } *checksum = (unsigned short)sum; }这段代码的意义在于:IP 头中的地址字段被修改后,只需要把旧的 IP 地址和新的 IP 地址代入这个函数,就能原地更新校验和,不需要重新遍历整个 IP 头做全量计算。在高速网络环境下,这能省下不少 CPU 周期。注意这里有一个坑:~oldVal的取反操作是对 16 位值执行的,但如果调用时传入的oldVal或newVal是 32 位整型,需要先截断成unsigned short,否则回卷计算会出错。在nat_tcp.c里对 TCP 校验和的更新使用了同样的函数,但 TCP 校验和覆盖的是伪头部加 TCP 头部加负载,更新时除了端口号和 IP 地址变化外,还必须处理 TCP 载荷中可能被 ALG 改写过的部分——这时的处理方式不一样。
3.3 TCP 与 UDP:nat_tcp.c和nat_udp.c的差异化处理
nat_tcp.c的复杂度远高于nat_udp.c,核心原因在于 TCP 是有状态协议。nat_tcp.c在处理数据包时,除了改地址和端口,还需要在建连阶段创建映射条目(natCreateNewMapping()内部调用natTcpStateMachine()),在收到 FIN/RST 时更新状态并启动删除计时器。nat_tcp_state.c维护了类似 Linux conntrack 的 TCP 状态机:NAT_TCP_STATE_SYN_SENT、NAT_TCP_STATE_ESTABLISHED、NAT_TCP_STATE_FIN_WAIT等。
UDP 的处理则简单直接:nat_udp.c在收到第一个出方向 UDP 包时创建映射,后续匹配到五元组就直接转发,超时时间由NAT_CFG_UDP_TIMEOUT决定。ICMP 在nat_icmp_datagram.c中处理,它针对的是 ICMP Echo 请求和响应,需要注意 ICMP 的 Identifier 字段充当了端口号的角色,在地址转换时必须一并修改,否则 ping 无法匹配应答。
4. 控制面:配置管理、API 接口与 ALG 扩展机制
4.1nat_configure.c的静态配置与nat_api.c的动态管理
nat_configure.c的作用是在系统启动时把配置文件里的映射规则加载到 NAT 映射表。常见做法是定义一个静态配置数组,在natConfigure()里逐条调用natAddMap()。与之对应的是nat_api.c,它面向运行时的动态管理,提供了以下几类核心接口:
natAddMap():添加静态映射,参数包括本地 IP、全局 IP、协议类型、端口号。natDeleteMap():删除映射条目。natGetStats():获取 NAT 转发统计信息,包括映射总数、命中次数、超时删除次数。natFlushMappings():清空所有动态映射,常用于网络环境切换时。
/* nat_api.c — 添加静态 NAT 映射 */ STATUS natAddMap(IP_ADDR localIP, IP_ADDR globalIP, unsigned short protocol, unsigned short port) { NAT_MAPPING *pMap; /* 预检查:映射数量是否达到上限 */ if (natGetMappingCount() >= NAT_CFG_MAX_MAPPING) { return ERROR; } pMap = (NAT_MAPPING *)malloc(sizeof(NAT_MAPPING)); if (pMap == NULL) { return ERROR; } memset(pMap, 0, sizeof(NAT_MAPPING)); pMap->LocalIPAddr = localIP; pMap->GlobalIPAddr = globalIP; pMap->Protocol = protocol; pMap->LocalPort = port; pMap->GlobalPort = port; pMap->MapType = NAT_MAP_STATIC; /* 插入到哈希表及定时器链表 */ natMapInsert(pMap); return OK; }这里的natMapInsert()会根据协议和端口的哈希值插入到对应哈希桶中。natGetMappingCount()在实现上通过一个全局计数器维护,每次插入和删除时同步更新。在实际产品中,natAddMap()通常会配合一个 shell 命令或者管理端口暴露给上层网管使用,比如在 VxWorks shell 里敲natAddMap "192.168.1.10" "202.106.1.10" 6 80,表示将内网192.168.1.10:80映射到公网202.106.1.10:80的 TCP 服务。
4.2NAT_ALG的实现范式:以natFtpAlg.c为模板
FTP 是 NAT 最难处理的协议之一,因为 FTP 控制连接中携带了数据连接的 IP 地址和端口(PORT命令和227响应)。natFtpAlg.c的处理逻辑是解析 FTP 控制流中的 IP 和端口信息,并把它们替换成 NAT 后的公网地址。关键代码:
/* natFtpAlg.c — 解析 FTP PORT 命令并改写其中的 IP 与端口 */ STATUS natFtpPortParse(M_BLK_ID pMblk, char *pData, int len) { int a1, a2, a3, a4, p1, p2; IP_ADDR newIP; unsigned short newPort; NAT_MAPPING *pMap; /* PORT 命令格式:PORT a1,a2,a3,a4,p1,p2 */ if (sscanf(pData, "PORT %d,%d,%d,%d,%d,%d", &a1, &a2, &a3, &a4, &p1, &p2) != 6) { return ERROR; } /* 找到控制连接对应的 NAT 映射,取得外部地址 */ pMap = natFindMappingByConn(pMblk); if (pMap == NULL) { return ERROR; } /* 将 PORT 命令中的内网地址替换为外部地址 */ newIP = pMap->GlobalIPAddr; newPort = natGetExternalPort(pMap, (unsigned short)(p1 * 256 + p2)); /* 重新格式化 PORT 命令 */ sprintf(pData, "PORT %d,%d,%d,%d,%d,%d", (newIP >> 24) & 0xff, (newIP >> 16) & 0xff, (newIP >> 8) & 0xff, newIP & 0xff, (newPort >> 8) & 0xff, newPort & 0xff); return OK; }这个函数的执行时机是在 NAT 模块把 FTP 控制包转发出去之前。它修改的是 TCP 载荷,所以除了需要在 IP 层更新校验和外,还必须重算 TCP 校验和——这就是为什么nat_tcp.c中专门提供了一个针对载荷修改的校验和更新路径。在natFtpAlg.c的代码中可以看到,它定义了ALG_FTP_CMD_PORT和ALG_FTP_CMD_PASV两个宏,分别在主动模式和被动模式下触发不同的改写分支。主动模式改写PORT命令,被动模式改写227响应中的 IP 端口。
4.3 PPTP 与 H.323:非 TCP 协议的 ALG 处理
natPPTPPTAlg.c针对的是 PPTP 协议中 GRE 隧道载荷的处理。PPTP 控制连接使用 TCP 1723 端口建立会话,但数据面走的是 GRE 协议。NAT 需要做的有两件事:一是改写控制连接中协商的 Call ID;二是对 GRE 包进行地址转换并维护 GRE 会话的状态映射。natPPTPPTAlg.c中natPptpCallIdParse()专门解析 GRE 头中的 Call ID 字段,确保内外网 Call ID 一一对应。
natH323Alg.c是所有 ALG 里最复杂的,因为 H.323 协议栈包含 H.225 和 H.245 两套信令通道,IP 地址和端口信息分散在多层 TPKT 封装中。在实际使用场景中,如果嵌入式设备的 NAT 需要支持 H.323 语音通话,建议先用抓包工具确认呼叫流程中的地址信息位于哪一层,再确定如何调用natH323Alg.c中的解析函数。源码里该文件头部注释也标注了#define NAT_H323_TPKT_LEN 4这样的常量,用于跳过 TPKT 头部。
5. TCP 状态机驱动的映射生命周期管理
5.1nat_tcp_state.c的状态定义与迁移条件
nat_tcp_state_structures.h定义了 NAT 模块自己的一套 TCP 状态枚举,它比 TCP 协议本身的 Eleven States 精简得多。看定义:
/* nat_tcp_state_structures.h — NAT 模块的 TCP 状态机 */ typedef enum { NAT_TCP_STATE_NONE, /* 无连接状态,等待 SYN */ NAT_TCP_STATE_SYN_SENT, /* 已发出 SYN,等待 SYN+ACK */ NAT_TCP_STATE_ESTABLISHED, /* 连接建立,正常转发 */ NAT_TCP_STATE_FIN_WAIT, /* 收到 FIN,等待对端关闭 */ NAT_TCP_STATE_CLOSE_WAIT, /* 对端已发出 FIN */ NAT_TCP_STATE_TIME_WAIT, /* 等待超时回收 */ NAT_TCP_STATE_MAX } NAT_TCP_STATE;状态迁移函数natTcpStateMachine()接收当前的 TCP 标志位(SYN、ACK、FIN、RST),根据当前状态和标志位决定新状态。这个状态机的核心目的是确定映射条目的回收时机:ESTABLISHED状态下的映射即使超过NAT_CFG_TCP_TIMEOUT也不回收,而FIN_WAIT或TIME_WAIT状态的映射会启动一个短超时(一般是 30 秒)用于快速回收。
/* nat_tcp_state.c — TCP 状态迁移 */ NAT_TCP_STATE natTcpStateMachine(NAT_TCP_STATE curState, unsigned char tcpFlags) { switch (curState) { case NAT_TCP_STATE_NONE: /* 收到 SYN 则进入 SYN_SENT */ if (tcpFlags & TCP_SYN) { return NAT_TCP_STATE_SYN_SENT; } break; case NAT_TCP_STATE_SYN_SENT: /* 收到 SYN+ACK 说明连接建立 */ if ((tcpFlags & TCP_SYN) && (tcpFlags & TCP_ACK)) { return NAT_TCP_STATE_ESTABLISHED; } break; case NAT_TCP_STATE_ESTABLISHED: /* 收到 FIN 进入 FIN_WAIT */ if (tcpFlags & TCP_FIN) { return NAT_TCP_STATE_FIN_WAIT; } /* 收到 RST 直接关闭 */ if (tcpFlags & TCP_RST) { return NAT_TCP_STATE_NONE; } break; case NAT_TCP_STATE_FIN_WAIT: /* 对端也发来 FIN,进入 TIME_WAIT */ if (tcpFlags & TCP_FIN) { return NAT_TCP_STATE_TIME_WAIT; } break; default: break; } return curState; }这个状态机的设计思路在资源受限的嵌入式环境是合理的。它砍掉了 TCP 的SYN_RCVD、LAST_ACK等中间状态,因为它们对 NAT 映射回收决策的影响不大。但注意一个实际工作中的坑:如果连接的一端异常断电,没有发出 FIN 或 RST,映射会长期停留在ESTABLISHED状态,直到NAT_CFG_TCP_TIMEOUT超时才被回收。nat_timer.c中会周期性检查每个映射条目的年龄,同时对比状态和超时配置,决定是否删除条目。
5.2nat_timer.c的超时扫描机制
nat_timer.c创建的定时器任务默认每 1 秒执行一次natTimerTick()。该函数遍历所有映射条目,检查当前时间与Age字段的差值。判断规则如下:
| 映射类型 | 状态 | 超时时间 | 到期动作 |
|---|---|---|---|
| TCP ESTABLISHED | 有数据传输 | 刷新 Age | 不回收 |
| TCP ESTABLISHED | 无数据传输 | NAT_CFG_TCP_TIMEOUT(默认 3600 秒) | 回收映射 |
| TCP FIN_WAIT / TIME_WAIT | 等待关闭 | 30 秒 | 回收映射 |
| UDP | 无新流量 | NAT_CFG_UDP_TIMEOUT(默认 120 秒) | 回收映射 |
| ICMP | 应答已返回 | 10 秒 | 回收映射 |
natTimerTick()的遍历实现是 O(n) 复杂度,当映射数量达到数千条时,遍历开销不可忽视。优化的话可以在natTaskInit()中把定时器周期从 1 秒改成 5 秒,但对于 UDP 这种短连接场景,超时检查精度降低会占用更多内存资源。另一个优化手段是维护一个最近最少使用链表,每次扫描只检查链表尾部的一小批条目。这是产品化过程中值得改造的地方。
5.3 快速验证:用 VxWorks shell 观察映射表状态
源码包中虽然没有提供 shell 命令注册代码,但nat_api.c的natGetStats()接口可以用来编写调试命令。在 VxWorks shell(或者宿主机的串口控制台)中绑定一条命令即可观察映射表:
/* 调试用 — 打印 NAT 映射表 */ void natDebugPrintMappings(void) { NAT_MAPPING *pMap; int count = 0; printf("=== NAT Mapping Table ===\n"); pMap = natGetFirstMapping(); while (pMap != NULL) { printf("proto=%d %08x:%d -> %08x:%d type=%d age=%ld state=%d\n", pMap->Protocol, pMap->LocalIPAddr, pMap->LocalPort, pMap->GlobalIPAddr, pMap->GlobalPort, pMap->MapType, pMap->Age, pMap->TcpState); count++; pMap = natGetNextMapping(pMap); } printf("Total: %d mappings\n", count); }natGetFirstMapping()和natGetNextMapping()是对哈希表遍历接口的二次封装,在nat_api.c中实现。实际调试时,如果在内网机器上 ping 外部地址,可以在natDebugPrintMappings()中看到一条 IP 协议为 1、端口为 ICMP Identifier 的映射条目;用 FTP 客户端连接外部服务器,则能看到 TCP 协议状态从SYN_SENT迁移到ESTABLISHED的过程。这套检查手段相比直接抓包要快得多,也更容易定位问题是出在规则配置还是状态机逻辑。
本文还有配套的精品资源,点击获取