嵌入式VxWorks NAT协议栈实现:hook机制与TCP状态机映射管理

嵌入式VxWorks NAT协议栈实现:hook机制与TCP状态机映射管理 简介面向 VxWorks 嵌入式网络开发者这份资源是一套基于风河系统的网络地址转换功能实现源码核心围绕 IP 协议栈钩子机制展开用于解决嵌入式设备在连接公网时的地址转换与访问管控问题。压缩包共 45 个文件以 C 语言源码为主辅以对应头文件、构建脚本、管理信息库文件及一份说明文档整体体积仅 145 KB结构紧凑便于研读。目前已有 104 位开发者学习下载。源码完整覆盖网络地址转换任务初始化、过滤钩子注册、传输层协议处理、分片重组、连接状态维护与清理回收等环节同时提供多种应用层网关算法示例适合希望深入理解 VxWorks 网络协议栈扩展机制或需要在真实项目中快速移植网络地址转换功能的研发人员参考。这些模块按功能分层组织既可用于单独调试也可作为整体参考实现对研究嵌入式协议栈的开发者很有帮助。1. 从 ip_hook 说起VxWorks 的 NAT 为什么和 Linux 不一样拿到nat.rar这份源码时第一反应是看它的发布时间和代码风格——nat_ip.c里大量使用KR风格函数声明nat_structures.h里手工维护的LIST结构而不是内核链表这基本可以断定是 Wind River 早期为 VxWorks 5.x / Tornado 2.x 时代写的协议栈级 NAT 实现。和我们熟悉的 Linux 内核netfilterconntrack不同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_TIMEOUTUDP 映射条目超时时间秒UDP 映射永不超时表项会越积越多NAT_CFG_TCP_TIMEOUTTCP 映射条目超时时间秒TCP 连接断开后映射不回收NAT_CFG_ALG_FTP启用 FTP ALG 支持FTP 数据连接无法穿越 NATNAT_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 与 UDPnat_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等待 SYNACK */ 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: /* 收到 SYNACK 说明连接建立 */ 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的过程。这套检查手段相比直接抓包要快得多也更容易定位问题是出在规则配置还是状态机逻辑。本文还有配套的精品资源点击获取