NDIS协议驱动开发NAT路由程序

 

在上一篇文章中,描述了windows7以上平台下NDIS协议驱动的开发过程。
本文将描述NDIS协议驱动的其中一个用途,用于实现一个介于应用层和驱动层的混合的NAT程序。

NAT路由程序,这个名字可能比较陌生,但是一说到路由器
(这里主要说的是家用和公司用的具备NAT功能的路由器 ),
能把家里或者公司里多台电脑,手机,平板等各类设备连接起来上网。肯定100个人中有90个人都熟悉。
路由器依然是广义上的电脑,它运行着独特的操作系统。
操作系统上依然运行着各种程序,它跟我们生活中经常接触到的windows程序,手机(Andriod,iOS)App 看起来不大一样。
其实本质都一样。常见的路由器基本是各种嵌入式linux(比如openwrt,eCos等),vxworks,或者其他系统。
路由器中的核心就是NAT路由程序(否则它就不应该叫路由器),它负责网络数据包的转发,让各类设备都能正常连接到Internet网。

稍微具体来说,NAT路由程序分为内侧和外侧,我们的手机,电脑,平板就处于NAT路由内侧,外侧连接的是外网。
内网的设备要连接外网的时候,NAT路由程序在外网映射一个端口,跟内网设备请求的IP地址和端口关联起来,
然后把内网数据包地址修改成外网地址,通过外网网卡发送出去,当接收到外网发来的数据包,通过这个映射的端口,
找到内网的来源地址,然后转发到对应的内网设备上。
以上说得太抽象,还是举个具体的例子:
路由器至少有两个网卡,一个是外网一侧的网卡,假设IP地址是 12.34.56.12
(这个外网一侧地址一般是ADSL拨号生成的,当然也可以把另外一个局域网当成外网)
一个是内网一侧的网卡,假设IP地址是 192.168.1.1。
其他设备的地址都是内网一侧,均是192.168.1.XXX(XXX代表2-254范围),并且网关设置为 192.168.1.1 。
假设 其中一台设备A地址是 192.168.1.10 。
A设备需要跟外网地址 101.1.101.1 的 1234 端口 通讯,
于是它发送一个IP数据包,目标地址是 101.1.101.1,目标端口1234, 源地址是 192.168.1.10,源端口 4567,
这个数据包会被发送到路由器的NAT路由程序上
(因为目标地址不属于同一网段的数据包,都会集中朝网关IP发送,这是TCP/IP网络世界的特性),
NAT路由程序会在外网12.23.56.12映射一个端口,假设是 5678,并且把端口和内网地址关联起来:
5678 --> 192.168.1.10:4567, 这样通过这个映射的端口能迅速找到对应的内网设备地址。
然后把这个数据包的源IP和PORT分别改成 12.34.56.12和5678, 通过外网网卡发送出去。
发出去的数据包,会经过各种各样的路由和交换机,直到最终到达 101.1.101.1 目标地址。
同样的,101.1.101.1 会回答一个数据包,这个数据包的目标地址是 12.34.56.12,
目标端口是 5678, 假设数据包传输过程中不再经历NAT路由,否则地址和端口不会是12.34.56.12和5678 ,
不管其他各种路由器和交换机怎么折腾,
达到我们的路由器外网的数据包目标端口最终都会是5678, 然后NAT路由程序根据 5678这个外网端口,
找到是发给 192.168.1.10 的 4567端口的,于是修改这个数据包的目标IP是192.168.1.10,端口 4567, 然后通过内网网卡发出去。
这样内网一侧的 192.168.1.10这个设备就接收到这个数据包了。

通常的NAT路由程序基本都是在IP层对IP数据包进行修改,然后转发,
而且通常都是在操作系统内核完成这个修改和转发的。
前几篇介绍的NdisFilter 实现的NAT 或者更早前介绍的NAT程序,稍微特殊一点是把IP数据包转发到应用层来处理,
但是也只是把网际层数据转发到应用层方便处理而已,本质上还是对IP数据包的修改。
这里说的IP数据包,是包含IP头,TCP(UDP,ICMP)头,UserData 的数据包,是网络传输中处于IP层(网际层)的数据包。

而这篇文章介绍的 “另类的NAT程序”,则是内网一侧抓取IP数据包,
而在外网一侧直接通过应用层socket套接字跟目标连接。
在很早的一篇文章
Network Address Translate(NAT)Windows平台(一) - 信易达 - 博客园 (cnblogs.com)
开篇的时候,就介绍过这种办法,与vmware的NAT功能很像。
(只要你仔细观察,虚拟机每建立一个连接,
宿主机上的vmnat.exe程序都会建立一个对应的socket连接,看起来比较神奇。
如果是通过修改底层IP数据包的办法,是看不到socket连接的。)
当时只是提到,没去实现,到后来基本也就忘记了。
直到最近重新实现NDIS6以上的协议驱动,才想起这个,于是打算实现这个功能。

原理说起来也没有想象的复杂(当然实现细节也不会是想象的简单),主要包括两个部分:
一,IP层数据包的抓取。
这个在windows平台使用NDIS协议驱动是最好的选择,当然使用WFP框架也可以。
而在linux这样的平台,IP数据包的抓取就显得非常简单了,使用PF_PACKET的套接字,直接在应用层就能获取。
二,IP数据包转换到应用层数据。
这里强调的是转换,不是前几篇文章介绍NdisFilter驱动的时候开发NAT程序那样把IP数据包传递到应用层。
所谓的转换,就是分析IP数据包,剥去IP头,TCP(UDP,ICMP)头然后传递到应用层,
UDP,ICMP这两个还比较好处理(UDP可能牵涉到 IP分片 问题稍微麻烦些)。
TCP则比较难,需要跟踪每个TCP连接状态, 跟踪 Seq, Ack, window Size 等。
实现这部分等于是实现一个简化版的TCP传输机制,要解决数据连续性,解决丢包问题,解决超时重传等。
总之,就是需要熟悉TCP/IP协议。否则这部分的转换,你会寸步难行。

首先是数据包的抓取,
windows平台可以使用NDIS协议驱动。
至于linux平台的抓包就更加简单了,详细可以去查阅linux下的PF_PACKET套接字。
获取到的数据包是包含14字节的以太网卡头部。
针对这个特殊的NAT程序:
抓取得到的数据包再做进一步过滤,过滤掉组播包,过滤掉广播包,
同时过滤掉源IP地址和目标IP地址是属于本网卡的数据包。
而且我们只处理IPv4,所以不属于IPv4的包都过滤掉。
并且移除以太网卡头部,只保留 IP + TCP(UDP,ICMP) + UserData ,(就是IP数据包)
假设经过以上处理之后,生成一个简单而又实用的函数接口集合,如下所示:

void* iplayer_open( const char* name,
void (*recv_ip_packet_callback)(char* ip_packet_data, int data_len, void* param),
void* param);
void iplayer_close(void* handle);
int iplayer_write(void* handle, char* ip_packet_data, int datalen);

iplayer_open 函数打开某个具体网卡,第一个参数就是网卡名,第二个参数传递一个 回调函数 recv_ip_packet_callback,
当NDIS协议驱动(或者linux下的PF_PACKET套接字)采集到网卡数据包,
并且经过上面提到的各种过滤条件,移除以太网卡头部。
最终符合条件的数据包达到的时候, recv_ip_packet_callback 回调函数就会被调用。其中ip_packet_data就是IP数据包。
当我们需要朝网卡写入我们自己的IP数据包的时候,调用 iplayer_write 函数,
当然iplayer_write在实现的内部添加以太网卡头部,然后写入到协议驱动(或通过PF_PACKET套接字写入)

至此,抓包这一步就完成了,并且导出了一个非常简洁的 iplayer 接口函数。剩下的大量工作就是对IP数据包的处理。

先通过TCP建立链接,传输,再到关闭的整个流程来理解这种 另类NAT 的工作过程:
为了方便,假设这个运行”另类NAT“ 网卡地址是 192.168.8.8(A机器) ,iplayer_open 打开的就是 192.168.8.8这个网卡.
截获到的IP数据包也是发给这块网卡的。
和它同一网段的另一台机器 192.168.8.100 (B机器)设置网关为 192.168.8.8 。
当B想连接TCP到 某个外网比如 112.1.1.1 的 1234端口。
它会首先发起标志是 SYN 的TCP数据包,这个数据包会发送到网关,也就是A机器的 192.168.8.8这块网卡。
由于这个数据包的源IP和目标IP,都不是192.168.8.8,符合过滤条件,不会被过滤掉。
于是recv_ip_packet_callback回调函数被调用,在回调函数中获取到这个SYN数据包之后,分析出需要连接的目标IP和目标PORT。
然后调用 sock=socket(AF_INET, SOCK_STREAM,0); connect ( sock, 目标地址,...);等函数,
连接到目标地址,连接成功之后,
制造一个 SYN + ACK 标志的IP数据包,目标地址和源地址跟SYN包相反,
同时在内存创建一个数据结构比如 usock_t 跟这个五元组(源IP + 源端口 + 目标IP + 目标端口 + 协议类型)关联起来 ,
这个 usock_t 用于以后的通讯和状态跟踪。记录SeqNumber,AckNumber, WIndowSize等。
然后调用 iplayer_write 回复这个SYN+ACK数据包,这样, B机器就接收到连接成功的回复包,
B机器再回复一个ACK包,B机器就认为这个连接成功了。
而A机器实际是在应用层直接通过connect函数连接到目标地址,并不是修改B机器发来的SYN数据包然后进行转发。
这里还有一个特点:
一般的NAT程序,执行数据包转发的时候,都是把数据包从内网网卡转发到外网网卡,也就是说至少需要两个网卡。
而这个NAT程序,只要一个网卡都能工作,所有的数据包都通过同一块网卡传输。
当然得确保你的机器能上网,也就是必须连接到真正的网关。否则你的机器和以你的机器作为网关的别的机器都不能上网了。

接下来是发送和接收数据包的过程。
B机器要发送数据,比如发送 8000大小的数据,经过TCP之后,被拆分成不超过 MSS大小的多个段,最终都会被发送到
192.168.8.8上来,然后通过分析这个包的五元组,找到内存中对应的usock_t结构,根据usock_t先前记录的SeqNumber等
信息,判断是不是重复发来的数据包,如果不是,拆除包里边出去TCP和IP头部,剩下用户数据,
这个用户数据调用 send 函数发送到目标地址,成功之后,回复一个ACK包给B机器,同时增量SeqNumber 。

同样的,调用 recv 函数接收到目标地址发来的UserData用户数据,
然后根据 usock_t里边的信息,给这个UserData添加IP头,添加TCP头,
记录和调整相关的AckNumber等信息,然后通过iplayer_write发送给B机器,这样B机器就接收到数据了。

以上描述的是数据的接收和发送的过程,实际处理的时候,比上边的描述复杂得多。
为了TCP协议的连续性和可靠性,需要考虑Seq,Ack保证数据包不丢失,考虑窗口大小确保流量控制等问题。
需要非常熟悉TCP协议栈,否则的话一不留神就会发生莫名其妙的TCP连接和传输问题。
然后就是TCP连接关闭的情况,当抓取到FIN或者RST数据包的时候,表示这个链接被关闭了。
于是删除相关数据,调用closesocket函数关闭到目标地址的TCP连接。
对FIN数据包,还得回复 FIN+ACK 数据包。

对于UDP和ICMP包的处理就显得简单的多了。
比如ICMP的Echo回显数据包,当 192.168.8.8 接收到 Echo的ICMP数据包的时候,
比如在windows平台,直接调用IcmpCreateFile ,IcmpSendEcho这些WIN32API函数Ping目标地址,
Ping的结果,组合成回复的ICMP数据包回传给B机器就可以了。

UDP数据包因为不用考虑丢包和顺序等问题,处理也变得很简单,
当192.168.8.8 接收到UDP数据包,首先查看是否发生了IP分片,
因为UDP单个数据包最大可能达到 64*1024 - 20-8 ,而网卡都有MTU限制,这个MTU一般都是1500,
因此一个完整的UDP数据包可能被拆分成多个IP分片传输。严格来说TCP也可能发生IP分片,
但是只要把TCP的MSS控制在 MTU - 40 内,就不会发生分片,所以可以不用考虑TCP的分片问题。
到达192.168.8.8 的UDP如果发生分片,必须把所有分片全部收到之后,组合成一个完整的UDP包,
再拆除IP头和UDP头,剩下UserData,通过
sock=socket(AF_INET,SOCK_DGRAM,0);
sendto(sock, UserData, 目标地址);发送出去,

同样的,通过recvfrom接收到目标地址的数据,如果接收到的UDP数据包 长度超过MTU,
需要把这个数据包拆分成多个小于MTU的数据包,再通过 iplayer_write 发给对应的机器。

以上就是TCP,UDP,ICMP三种数据包在这种特殊的NAT程序的处理过程。

这种特殊NAT程序的特点其实就是在外网一侧完全是通过应用层的socket套接字函数来完成数据的通讯,
内网一侧是对IP数据包的分析处理过程,并且转换到跟外网的每个socket套接字联系起来,并且互相传输数据。

因为外网一侧完全是应用层的socket来传输数据,
你可以理解成这个NAT程序完全等于是自己创建许多socket套接字并且跟许多目标地址通讯。
就像浏览器程序那样,可以同时连接到非常多的目标服务器。

那么就可以在这些socket上做许多在应用层可以做的事情。
比如做SOCKS5 代理,或者其他私有类型应用层协议代理,
最终的结果等于是把整个局域网的所有机器的通讯都代理到代理服务器上了。
这个看起来比较神奇。

在应用层非常容易统计流量,可以访问哪些地址,哪些不该访问,就是防火墙功能非常容易实现。
可以非常容易重定向到其他地址等,
因为一切都是socket套接字控制的。

当然这种NAT程序的效率肯定不如直接在内核修改IP数据包的常规的NAT程序。

posted @ 2022-11-04 15:27  信易达  阅读(462)  评论(0编辑  收藏  举报