VxWorks下NAT实现:nat_ip hook挂载与地址转换实战解析

发布时间:2026/9/14 1:50:15
VxWorks下NAT实现:nat_ip hook挂载与地址转换实战解析 简介面向VxWorks嵌入式网络开发的一份NAT功能实现源码包适合需要在内核IP层定制地址转换的驱动或协议栈工程师。资源围绕vxworks_nat、ip_hook等关键机制展示了在IP数据包收发路径上注册钩子、修改地址端口并完成转发的完整流程可解决有限公网IP下嵌入式设备上网与安全隔离问题。包内共45个文件以27个C源码、11个头文件为主另有4个makefile、2个mib和1个txt说明覆盖NAT初始化、过滤钩子、TCP/UDP/ICMP事务处理、FTP/PPTP/H323等ALG应用层网关及snmp管理接口结构完整。RAR压缩包仅145KB轻量易用。目前已有104人学习下载适合希望研读Wind River VxWorks网络协议栈源代码、快速接入NAT模块的开发者。通过阅读nat_ip.c、nat_filter_hook.c等核心实现可掌握钩子注册与数据包改写逻辑理解natAlgApi与natmgmt等配置管理方式为自主裁剪或移植NAT功能提供直接参考。1. 老平台上要自己写 NATnat.rar 就是那个半成品交换机通了不等于网关通了。VxWorks 板卡做两个网段间转发时你会发现 ping 能过去业务流量却回不来——因为两端都只认自己网段的地址。老版本 VxWorks 的风河协议栈不提供现成的 NAT 模块镜像里 netmask、route 配得再漂亮也解决不了地址转换。这个场景里nat.rar_VXWORKS nat_ip hook vxworks_nat_vxworks_windriver这类资源就是最常被翻出来的东西一套从老 BSP 里剥离出来的 IP 层钩子实现核心思路是把网络地址转换挂到 Wind River 协议栈的 IP 输入输出路径上在包进 IP 层之前改掉源地址、目的地址和端口。适合两类人一类是把旧板卡改造成双出口 NAT 网关的维护工程师另一类是准备在 VxWorks 上做协议栈改造、还不清楚钩子系统怎么用的新人。下面按 hook 的挂载原理、NAT 表设计、编译链接、验证排错的顺序把整套方案讲透。2. nat_ip hook 挂在哪从 IP 输入到输出的三条路径2.1 hook 技术原理协议栈留出来的处理窗口VxWorks 的网络协议栈从底层驱动收包后包会经过 mux 层、链路层最终进到ipintr或ipOutput这类内部函数。NAT 必须发生在路由决策之后、校验和计算之前否则改完地址还要自己重算一大堆东西。老版本风河协议栈留了几组钩子标识nat_ip的名字就是从这里来的。钩子函数挂在 IP 层能同时看到输入、输出和转发三种方向的包。这份资源在标题里带nat_ip hook按常见的移植经验它做的是协议层钩子而不是 mux 层钩子。mux 层拿到的还是以太网帧要自己解析 IP 头而 IP 层钩子直接拿到struct ip指针源地址目的地址就在偏移 12 字节处改起来不需要跨层解析。绝大多数工程师选择 IP 层钩子也正因为这层动手成本最低。下面以一个实际的回调函数骨架说明挂载写法。2.2 能直接挂在镜像里的钩子原型与注册方式下面的 C 代码把钩子函数挂到 VxWorks 的 IP 输入路径上。不同版本风河开发包里的函数名可能带前缀但原型基本落在这个意思上#include vxWorks.h #include netinet/ip.h #include netinet/ip_net_api.h /* nat_ip hook 回调每个进入 IP 层的包都会过这里 */ int natIpHook( IP_HOOK_OP op, /* 操作方向见下表 */ struct ip * pIpHdr, /* IP 头指针 */ struct netif * pNetif, /* 收/发包的网络接口 */ void * pCookie /* 注册时传的用户参数 */ ) { if (!pIpHdr) return IP_HOOK_CONTINUE; /* 判断是否是我们要处理的转发/出入栈包 */ return natPktRewrite(op, pIpHdr, pNetif); } void natHookInit(void) { /* 同时挂输入、输出、转发三个方向 */ netHookAdd(IP_HOOK_IN, natIpHook, NULL); netHookAdd(IP_HOOK_OUT, natIpHook, NULL); netHookAdd(IP_HOOK_FWD, natIpHook, NULL); }注册函数natHookInit里的三个调用是最小挂载方案。IP_HOOK_IN处理进入本机的包IP_HOOK_OUT处理本机发出的包IP_HOOK_FWD处理纯转发的包。回调返回值IP_HOOK_CONTINUE表示包继续走正常协议栈路径如果 NAT 改写失败或判断为非法包可以返回丢弃值。pCookie一般用来传 NAT 表指针或统计计数器。op参数决定了你能对包做什么下一节列一张表说明每个方向的执行时机。2.3 钩子方向与执行时机对照钩子操作触发时机能改什么执行上下文IP_HOOK_IN包从网口进入 IP 层后、递交上层协议前目的地址、目的端口任务上下文或中断上下文IP_HOOK_OUT本机应用发包、路由选路后源地址、源端口调用进程上下文IP_HOOK_FWD包从一网口进、从另一网口出源地址或目的地址及端口协议栈任务上下文执行上下文是这里最容易被忽视的。板卡数据面跑的是转发路径上的包IP_HOOK_FWD很可能就在网络任务里被调用。如果你在回调里调用了阻塞函数、semTake或大块malloc轻则丢包重则把整个协议栈卡死。我一般会在钩子函数里只做查表、改头和重算校验和这三件事任何日志、统计、表项老化逻辑都用标志位丢到后台任务去处理。3. NAT 表设计与改写逻辑唯一不能省的数据结构3.1 从“地址对照表”到五元组会话项很多人把 NAT 表想成old_ip - new_ip的线性映射实际做双出口 NAT 时完全不够。两个网段并发访问同一个目的地址时端口必须区分开否则连接会互相踩。所以工作在钩子函数里的 NAT 表记录的基本单元至少是五元组级别的会话结构。下面这个结构体是这份资源里常见的表项骨架typedef struct nat_session { UINT32 sAddr; /* 原始源地址 */ UINT16 sPort; /* 原始源端口 */ UINT32 dAddr; /* 原始目的地址 */ UINT16 dPort; /* 原始目的端口 */ UINT32 xAddr; /* 转换后源地址外网口地址 */ UINT16 xPort; /* 转换后源端口 */ UINT32 lastUsed; /* 最近一次命中的 tick 数 */ UINT8 proto; /* IPPROTO_TCP / IPPROTO_UDP / IPPROTO_ICMP */ UINT8 state; /* 会话状态 */ struct nat_session *next; /* 哈希桶冲突链 */ } NAT_SESSION;xAddr和xPort是地址转换后的结果。lastUsed用来做老化判断配合风河系统的tickGet()读取系统时钟。proto字段不能省因为 TCP、UDP 的端口字段位置不同ICMP 甚至没有端口它的“端口”要用 ID 字段代替。3.2 查表与改写的核心函数理解了表项结构真正的查表逻辑就好写了。下面的函数在钩子里被调用入参是包里的原地址、原端口、目的地址、目的端口出参是 NAT 改写后的地址端口static NAT_SESSION *natTblLookup( UINT32 sAddr, UINT16 sPort, UINT32 dAddr, UINT16 dPort, UINT8 proto) { UINT32 hash natHash(sAddr, sPort, dAddr, dPort, proto); NAT_SESSION *p natTbl[hash NAT_HASH_MASK]; while (p ! NULL) { if (p-sAddr sAddr p-sPort sPort p-dAddr dAddr p-dPort dPort p-proto proto (tickGet() - p-lastUsed) NAT_SESSION_TTL) { p-lastUsed tickGet(); /* 刷新老化时间 */ return p; } p p-next; } return NULL; }natHash用源地址、端口、目的端口异或后与掩码取模保证冲突链长度控制在个位数。命中后刷新lastUsed这样长连接不会被后台老化任务误杀。如果查表没命中转发方向要为新连接创建会话创建时先看你板卡的外网口地址是多少再从可分配端口区间里挑一个空闲端口。3.3 端口分配与冲突处理端口分配是最容易出问题的部分。简单的做法是维护一个全局自增变量每次分配natNextPort但必须检查该端口是否已在某个会话中使用。下面的分配函数在会话表里查一圈直到找到空闲端口为止static UINT16 natAllocPort( UINT32 dAddr, UINT16 dPort, UINT8 proto) { UINT16 port natNextPort; while (natPortInUse(port, dAddr, dPort, proto)) { port natNextPort; if (port NAT_PORT_BASE) port NAT_PORT_BASE; } return port; }NAT_PORT_BASE一般设在 1024 以上避免和系统保留端口混在一起。同一目的地址同一目的端口的多条连接分配时要保证源端口不重复否则回包到了外网口之后无法区分归属连接会被重置。为更稳的会话冲突处理可以把(dAddr, dPort, proto)放到小哈希表里做占用检查。注意natPortInUse内部如果每次都遍历全表板卡并发连接一多开销会陡增实际做成最近 100 个已分配端口的小数组检查即可。3.4 校验和不能只改 IP 头NAT 改写地址后校验和设备会一路失败这是最常见的现象。IP 头校验和必须全量重算TCP 和 UDP 的校验和是伪头校验源地址改了就必须增量更新伪头部分。下面这段函数就是标准的增量校验和算法比整包重算省一半以上的 CPU 开销static void natChecksumAdjust( UINT16 *pHdrSum, UINT32 oldVal, UINT32 newVal) { UINT32 sum ~(*pHdrSum) 0xFFFF; sum (oldVal 16) (oldVal 0xFFFF); sum (newVal 16) (newVal 0xFFFF); while (sum 0xFFFF0000) sum (sum 0xFFFF) (sum 16); *pHdrSum (~sum) 0xFFFF; }入口参数里oldVal是转换前地址newVal是转换后地址。改动源地址时调用一次改动源端口时再调用一次。UDP 校验和为 0 的包表示发送端没做校验NAT 之后可以保持 0 不下手省一笔开销TCP 校验和是强制的不能跳过。4. 把 nat.rar 编译进 VxWorks 镜像最小路径与初始化时机4.1 先核对解包后的源码组成解压标题里那份 nat.rar 之后先对一下文件清单再动手。一份完整的钩子实现至少包含这几个文件NAT 表实现文件、钩子回调文件、初始化文件以及一份头文件。如果少了初始化函数工程里根本找不到注册入口如果少了校验和更新函数跑通第一个连接就会看到全部 TCP 握手包有去无回。照下面的清单核对缺哪个文件补哪份逻辑。文件典型职责缺了会怎样natTbl.c会话表增删查、老化任务无法建立连接natHook.c钩子回调、包改写包进了栈但没人处理natInit.c注册钩子、初始化表、启动后台任务钩子没有任何效果natLib.h结构体定义和函数声明编译时报类型错误4.2 手动编译与符号检查VxWorks 5.5/6.x 时代的做法是直接编成一个.o或.a再链进内核镜像。下面的命令用风河的ccpentium交叉编译器把natLib.c编译成目标文件ccpentium -c -fno-builtin -fno-strict-aliasing \ -I. -I$(WIND_BASE)/target/h \ -DCPUPENTIUM -DTOOL_FAMILYgnu -DTOOLgnu \ natLib.c编译完成先不要急着链镜像先查符号。nm natLib.o | grep natHook能看到钩子回调函数是否真的被编进了对象文件。如果natHookInit没有出现在符号表里多半是某处#ifdef宏没打开。ar rcs libnat.a natLib.o把对象文件归档成库之后在工程配置里增加这个库即可。有些工程在 VxWorks 7 下用vxprj构建常见做法是把源码文件追加到VIP组件的src目录再跑vxprj build让构建系统自动编排依赖不需要手写上面的 gnu 编译命令。4.3 初始化调用放在网络启动之后钩子函数注册的时机非常关键。netHookAdd必须在协议栈初始化完成之后调用否则注册表还没分配调用会直接失败。但也不能太晚否则网络接口起来之后已有部分数据包绕过转换。看板卡 BSP 里的启动顺序ipInit或netInit执行之后、网口eth0管理口配好地址之前是一个稳妥的窗口。老版本镜像里一般追加在usrNetworkInit之后VxWorks 7 则可以在应用工程的启动脚本里调用。5. 验证与排错钩子是否生效的三个关键动作5.1 串口日志与 logMsg 定位中断上下文问题钩子挂上后别急着接业务先用串口观察。printf在部分网络中断路径上会导致系统死锁验证阶段最好全部换成logMsg。logMsg本身也不是完全安全高频场景下日志会拖慢收包速率。验证时只对首包打日志即可新建会话时打印“新会话 源地址-转换地址”命中既有会话时不打印。5.2 双向抓包确认校验和与端口正确一个网口接 PC 跑tcpdump另一个网口模拟业务源分别在两个网口抓包。业务源 ping NAPT 后的目的地址时PC 上应看到源地址已变成外网口地址且 ICMP 的 ID 字段已做映射。用 TCP 连接测试时重点看握手的第一包源端口是否落在配置的端口区间内。回程包在业务源侧抓包看目的地址是否恢复成原始地址。两侧对照能快速区分是查表没命中还是改写没生效。5.3 分片、校验和、老化时间最常翻车的三处现象原因处理方式大包通、小包不通IP 分片后非首片不带端口查表失败按 IP 头的 ID 和源/目的地址记录分片归属复用同一转换结果连接能建传数据断增量校验和算法里 old/new 参数传反对照oldVal是原始值、newVal是转换后的新值重查一遍空闲连接 30 秒被切断老化时间小于下游防火墙会话超时TCP 老化调到 120 秒UDP 调到 20 秒ICMP 保持 10 秒最后调参时把三个老化阈值做成全局宏放在natLib.h顶部板卡进实验室后用不同协议的长连接各跑一轮把lastUsed的刷新打印打开确认命中会话的包确实刷新了老化字段。这样一套验证下来钩子挂得稳不稳、表结构覆盖了哪些流量特征就全在掌握里了。本文还有配套的精品资源点击获取