windows7以上平台 NDISFilter 网卡过滤驱动开发

                                                                                         by fanxiushu 2019-01-16 转载或引用请注明原始作者

这里讨论的都是基于WIN7以上平台,NDIS 6.0以上版本的网络驱动。

做个驱动的目的,是因为很早之前,我使用 TDI 和 NDIS5.1 框架的passthru中间层驱动,实现的基于应用层的NAT程序,
之所以说是基于应用层,是把passthru所有网络通讯数据包转发到应用层来处理,
在应用层NAT程序实现了大量的代码来处理NAT功能,
以及针对程序限流(免得一些恶心的程序疯狂的占用上传带宽),监控程序流量等多种功能。
并且长期运行在我的电脑上,在很早的CSDN博客中,曾经断断续续的提到过NAT的开发过程,有兴趣可以去查看这些文章。
当时主要运行在WINXP和WIN7中,这没有什么问题。到了WIN10平台,虽然 TDI 依然可以使用,
但是基于NDIS5.1框架的中间层驱动已经无法安装和运行了。因此想着修改驱动,本来是打算整个用WFP框架替换的,
WFP既能解决以前使用TDI才能处理的应用进程和端口关联问题,又能获取到IP数据包进行NAT转发和拦截。
但是我大量应用层代码,都是基于以太网数据包进行的分析处理,改起来实在蛋疼,因此想到了NDISFilter,跟passthru非常接近。
只需修改驱动代码,甚至连应用层接口都可以不修改,直接能用。开发完成之后,证明这确实是个好的选择。

windows平台内核中关于网络通讯这部分,分层是清晰的,同时也是非常复杂的。
如下链接中
https://blog.csdn.net/fanxiushu/article/details/78221340 (Windows7以上使用WFP驱动框架实现IP数据包截取(一))
在讲述WIN7以上的平台中WFP驱动框架的时候,就已经说明了windows平台的这种分层流程。

大家对应用层网络套接字一定不陌生,任何程序的通讯,都离不开socket。简单使用 BSD Socket(伯克利套接字)
socket,bind,listen,accept,connect,recv,send等这些函数就能实现客户端和服务端网络通讯。
这些函数被设计得非常精简,也是很经典的接口函数,虽然经历了几十年,这些接口函数依然是经典中的典范。
然而各种操作系统(不管是windows,linux,还是各类UNIX内核等)对这些函数的内核实现,都不是一件简单的事。
经历了几十年的风风雨雨,各种操作系统对通讯协议栈的实现已经非常成熟,臻于完美。

以windows7以上平台send函数为例,当在应用层调用send发送数据的时候,进入到windows内核。
首先跟afd.sys(WinSock辅助功能驱动)交互,比如应用层的SOCKET句柄跟内核对应对象映射,数据映射等等预备工作。
然后递交给TLNPI接口,转换之后,进入到tcpip.sys核心处理,在这里边分析拆包,添加 TCP(UDP)头,IP头等。
处理的数据包再递交给NDIS层, 通过网卡最终传递到物理网线上去。
NDIS又细分成三层:
1,NDIS协议层,如果从NDIS角度来说,tcpip.sys 是属于NDIS的协议层。
2,NDIS中间层,NDIS6.0以上的框架,
      专门在中间层提供了一个轻量级的过滤驱动NDISFilter(LightWeight Filter)。也就是本文讨论的重点。
3,NDIS miniport驱动(NDIS网卡驱动),就是对应的各种网卡硬件的驱动。
      只要做网卡硬件的厂商,开发的网卡驱动符合NDIS规范,都能在windows正常运行起来。

(以下的都是6.0 以上的NDIS版本, 这里没有NDIS5.1以及以前版本的什么事。)

我们先简单的来看看 NDIS协议驱动的初始化过程。
首先初始化NDIS_PROTOCOL_DRIVER_CHARACTERISTICS 数据结构。填写里边的各种回调函数。
填写完成之后
在 DriverEntry 入口函数中调用 NdisRegisterProtocolDriver 注册我们自己的NDIS协议驱动。
NDIS_PROTOCOL_DRIVER_CHARACTERISTICS 数据结构中的回调函数不算多。
比如绑定到某块网卡的回调
BindAdapterHandlerEx函数,
当NDIS发现底层的网卡可以绑定到我们注册的NDIS协议驱动的时候,这个函数就会被调用。
在这个回调函数中可以调用NdisOpenAdapterEx 来打开这个网卡,
如果NdisOpenAdapterEx 返回NDIS_STATUS_PENDING,则在真正打开完成的时候,
会调用 
OpenAdapterCompleteHandlerEx 回调函数完成打开过程。
取消绑定回调函数,也是同样的一个原理。
OID请求相关的回调函数包括
OidRequestCompleteHandler和DirectOidRequestCompleteHandler,
还有状态指示回调函数,PNP事件函数,协议驱动卸载函数等。
还有两个很重要的回调函数,就是跟数据包传输相关的。

1. SendNetBufferListsCompleteHandler,
   这个回调函数是在当我们的NDIS协议驱动调用
NdisSendNetBufferLists 函数来发送数据包到底层网卡,
         当发送完成之后,NDIS调用这个回调函数。
2. ReceiveNetBufferListsHandler,
   这个回调函数是在底层网卡接收到数据包,上传到NDIS中间层驱动,
        中间层驱动再传递给我们的NDIS协议驱动,从而被调用的函数。

然后我们再看NDIS网卡驱动的框架:
 首先初始化
NDIS_MINIPORT_DRIVER_CHARACTERISTICS数据结构,
然后在
DriverEntry 入口函数调用NdisMRegisterMiniportDriver注册我们的网卡驱动。
这个结构里边的回调函数可就比NDIS协议驱动的多。这里只关注跟数据包传输相关的回调函数。

1. SendNetBufferListsHandler
      这个回调函数是当协议驱动调用
NdisSendNetBufferLists  发送数据包到底层网卡的时候,被调用的函数,
      表示上层有数据包需要通过网卡发送出去。
2.  ReturnNetBufferListsHandler,
     当底层网卡从网线接收到数据包,需要投递给上层,这时候调用 NdisMIndicateReceiveNetBufferLists投递数据包,
     当上层处理完成之后,NDIS就会调用这个回调函数。

再来看看NDIS中间层驱动框架,
 中间层驱动实际上对上面两个(NDIS协议驱动和NDIS网卡驱动)的合并,
对NDIS协议驱动来说,中间驱动相当于是网卡驱动。
而对底层网卡驱动来说,中间层驱动则相当于是协议驱动。
我们在DriverEntry入口函数中,分别初始化

NDIS_MINIPORT_DRIVER_CHARACTERISTICS 和 NDIS_PROTOCOL_DRIVER_CHARACTERISTICS 数据结构。
然后分别调用
NdisMRegisterMiniportDriver 和 NdisRegisterProtocolDriver  来注册协议驱动和miniport驱动。
都成功之后,再调用 NdisIMAssociateMiniport 函数告诉NDIS,把协议驱动句柄和miniport驱动句柄关联起来,
告诉NDIS这是个中间层驱动,这样初始化就完成了。
然后在 NDIS_PROTOCOL_DRIVER_CHARACTERISTICS 的 BindAdapterHandlerEx回调函数中
调用NdisOpenAdapterEx打开对应的底层网卡。

接着再调用 NdisIMInitializeDeviceInstanceEx函数来初始化一个虚拟网卡,因为中间驱动本身就是承上启下的,
新创建的这个虚拟网卡,使得上层的所有的NDIS协议驱动全都绑定到这个虚拟网卡上,而不再绑定到这个中间驱动所绑定的底层真实网卡上。
至于这个新建的虚拟网卡要不要在系统中显示出来,取决于inf配置文件的配置参数。
NdisIMInitializeDeviceInstanceEx 调用使得
NDIS_MINIPORT_DRIVER_CHARACTERISTICS  里边的 InitializeHandlerEx网卡初始化回调函数被调用,
我们可以像初始化虚拟网卡那样的一般流程进行初始化就可以。

同时,可以在BindAdapterHandlerEx回调函数中多次调用 NdisIMInitializeDeviceInstanceEx,
这就相当于一块真实底层网卡虚拟出多个虚拟网卡,相当于是一块真实网卡拆分成了多块网卡(N : 1)。
最典型的应用就是 802.1Q VLAN 协议的虚拟局域网。
也可以绑定几个真实网卡,虚拟出一个虚拟网卡出来,相当于几块真实网卡合并成了一块网卡( 1 :M)
这种应用一般出现在高性能的服务器上,需要处理相当庞大的进出口带宽或做数据传输均衡等。
 我们可以再想复杂一点,M块真实网卡,模拟出N块网卡出来 (N :M)。
而这里唯独没有考虑 1 :1 的情况。这种情况显然就是对模拟的网卡数目不感兴趣,而只是关注和过滤网卡里边传输的数据包。
针对这种情况NDIS5.1以及更早之前的NDIS框架并没有做特别处理,而只能使用NDIS中间驱动模型。
NDIS6.0以上则专门提供了 NDISFilter 框架。虽然不知道NDISFilter具体实现过程,
但是很显然微软工程师是把NDIS中间层驱动框架针对 1 :1 的情况作了封装和简化。

同样的,初始化 NdisFilte r驱动,需要在DriverEntry入口函数中初始化
NDIS_FILTER_DRIVER_CHARACTERISTICS 数据结构。然后调用 NdisFRegisterFilterDriver 注册NdisFilter驱动。
这个结构里边的回调函数有点多,因为毕竟是把NDIS miniport驱动和NDIS协议驱动两者的回调函数合并之后的结果。
不过好在除了几个必须要实现的函数外,其他很多都是可选的,只要不感兴趣,简单设置 NULL 就可以了。
当某个回调函数设置为NULL,NDIS就会跳过我们的NDISFilter,直接寻找处理链中的下一个对应的回调函数。

这里只关注其中一些比较重要的回调函数,其他函数的说明请查阅相关的MSDN文档。
1。FilterAttach,
      当我们的NDISFilter驱动绑定到某块网卡的时候,会被调用。在这个回调函数里,创建和初始化一些资源,
      创建一个我们自己的数据结构,然后调用 NdisFSetAttributes 函数,把这个数据传递进去,作为以后各种回调函数的入口参数。
       NDISFilter本身就是中间层驱动,因为要承上启下,它的绑定,使得原先直接绑定到底层网卡的各种协议驱动都得解绑,
       然后再重新绑定到NDISFilter驱动上。而NDISFilter又得绑定到这个底层网卡上。
      
这显然会造成通讯短暂的中断,也会造成网卡重新启动。
2,FilterDetach,
      与上面回调函数正好相反,解除绑定。在此函数中清除在FilterAttch创建的资源和数据结构。
      同样的道理,这个函数的调用,也会造成通讯短暂中断,网卡重启。
3,FilterRestart, FilterPause,
      这两个函数是对应网卡暂停和重启回调函数, 典型的:
      当FilterAttach被调用之后,这时处于Paused 状态,之后网卡重新启动 ,FilterRestart被调用,之后处于Running状态
      当FilterDetach即将被调用前,FilterPause被调用,这时处于Paused状态,之后FilterDetach被调用,最终处于Detached状态。
4,FilterSetOptionsHandler,FilterSetFilterModuleOptionsHandler,
     用于处理一些额外信息,基本就实现一个占位函数就可以了。
5,Oid相关函数。
      FilterOidRequestHandler,FilterOidRequestCompleteHandler,FilterCancelOidRequestHandler,
      还有NDIS6.1以上版本对应的DirectXOid相关函数,如果对Oid请求不感兴趣,完全可以把这些函数设置为NULL。
      只是这里我需要屏蔽 网卡的offload功能(offload下面会介绍),
      因此需要
设置 FilterOidRequestHandler,FilterOidRequestCompleteHandler回调函数。
      需要注意的是在FilterOidRequestHandler 回调函数中,不能直接调用 NdisFOidRequest直接传递Oid请求,而是需要先调用
      NdisAllocateCloneOidRequest复制出一个Clone的Oid,然后把原始Oid指针保存到CloneOid中,
      然后再对这个Clone的Oid发起 NdisFOidRequest请求,如果NdisFOidRequest返回NDIS_STATUS_PENDING,
      Filter
OidRequestCompleteHandler 回调函数就会被调用。具体实现请阅读微软的ndisfilter相关实例代码。
6,其他一些函数,比如FilterStatus(指示网卡的各种状态),FilterNetPnPEvent,FilterDevicePnPEventNotify等,
      这些函数要么设置NULL,要么直接在回调函数里边调用NdisFXXX进行传递就可以。

7,数据包传递函数,这个是核心的部分,包括如下四个:
     FilterSendNetBufferListsHandler,
       当NDIS协议层驱动调用NdisSendNetBufferLists函数发送数据包,经过NdisFilter,这个回调函数被调用,
       可以在这个函数中调用 NdisFSendNetBufferLists继续向下投递,或者调用 NdisFSendNetBufferListsComplete直接完成这个数据包,
       不再继续向下传递,因此可以使用这种办法来拦截阻断数据包的向下传递。
     FilterSendNetBufferListsCompleteHandler,
        当底层网卡完成了从上层发来的数据包的处理,调用NdisMSendNetBufferListsComplete完成这个数据包,向上经过Ndisfilter,
        这个回调函数就会被调用,在这里判断是不是自己数据包,如果是则自己释放资源。
        不是则继续调用 NdisFSendNetBufferListsComplete向上传递,
     FilterReceiveNetBufferListsHandler,
        当底层网卡接收到物理网线上的数据包,调用NdisMIndicateReceiveNetBufferLists函数向上投递,经过NDISFilter,
        就会调用这个回调函数,在这里可以使用NdisFIndicateReceiveNetBufferLists 继续投递;或者 调用NdisFReturnNetBufferLists
        直接返回给底层网卡,这样就不会继续向上传递,因此可以使用这个办法来阻止从网卡来的数据包上传。
     FilterReturnNetBufferListsHandler,
        当从底层网卡传递的数据包最终被协议层驱动接收处理,协议驱动调用NdisReturnNetBufferLists完成这个数据包,经过NDISFilter,
       这个函数被调用。

如果NDISFilter驱动需要跟应用层程序通讯,就还需要创建一个用户设备。
可以在DriverEntry调用 NdisRegisterDeviceEx 注册一个用户接口设备,
因为我的驱动是直接把所有数据包传递到应用层再来处理。
所以必须创建这样一个用户接口设备,用来收发数据。

我们再来理清数据包如何传递的,并且经过NDISFilter的时候,
把数据包都传递到我们自己的应用程序的应用层,然后再从应用层传递回来。

从应用层send 开始。
应用层通讯程序调用send函数发送数据的时候,进入内核afd.sys做预处理,进入TLNPI转换,
进入到tcpip.sys进行分析拆分,添加TCP(UDP),IP头( 对NDIS来说,tcpip.sys就是个NDIS协议驱动)。
然后tcpip.sys 调用NdisSendBufferLists来发送经过分析处理的数据包,
NdisSendBufferLists 函数查找整个NDIS处理链,一个一个的调用相关的回调函数,因为NDIS处理链中可能不止我们的NDISFilter驱动,
还可能有别的NDISFilter2,NDISFilter3... 或者中间驱动2,中间驱动3... 等等。
然后进入到我们的NDISFilter驱动,这个时候 我们的FilterSendNetBufferListsHandler 回调函数被 NdisSendBufferLists 调用。
在FilterSendNetBufferListsHandler 中,接收到 NET_BUFFER_LIST数据结构的数据包,这是个单链表,而且每个NET_BUFFER_LIST包含
一个或者多个NET_BUFFER,而每个NET_BUFFER有包含一个或多个 MDL。而MDL链里边存储的就是具体的数据。
对每个NET_BUFFER 可以直接调用NdisGetDataBuffer获取数据,
或者遍历每个MDL,调用MmGetSystemAddressForMdlSafe 获取数据,然后再合并起来。两者的效果都是一样的。
通过遍历NET_BUFFER_LIST把数据包获取到,并且传递到我们自己程序的应用层来,
至于如何传递,可以利用NdisRegisterDeviceEx创建的用户接口设备,通过定义一些IOCTL来传递。比如
IOCTL_READ_FROM_UP,表示用户接口接收从上层的NDIS协议驱动传递来的数据包,

IOCTL_WRITE_TO_DOWN,表示用户层接收到IOCTL_READ_FROM_UP传递的数据包,
                     然后分析处理,再调用这个IOCTL传递给驱动,并且被NDISFilter驱动继续传递给下层。

IOCTL_READ_FROM_DOWN,表示用户层接口获取到从底层网卡传递上来的数据包,

IOCTL_WRITE_TO_UP,表示用户层接收到IOCTL_READ_FROM_DOWN传递的数据包,
                    经过分析处理,再调用这个IOCTL,传递给驱动,并且被NdisFilter继续传递给NDIS协议驱动上层。
看起来比较绕口,其实理清楚了头绪,还是挺好理解。
在我们的FilterSendNetBufferListsHandler  函数中,获取到从上层传递的 NET_BUFFER_LIST 数据包,然后通过
IOCTL_READ_FROM_UP 传递到我们的应用层程序。同时调用 NdisFSendNetBufferListsComplete 完成这个数据包。
NdisFSendNetBufferListsComplete  函数查找并调用NDIS处理链中的回调函数,
这个时候协议驱动的SendNetBufferListsCompleteHandler 回调函数会被调用,协议驱动在这个回调函数中完成释放相关资源,
通知上层数据包传递完成等,或者再到更上层比如应用层的send函数。很显然,这个时候数据包还没真正到达网卡。
接着,我们的应用层程序接收到 IOCTL_READ_FROM_UP传递的数据包,经过分析处理,调用IOCTL_WRITE_TO_DOWN写到驱动,
然后驱动接收到这个数据,创建新的 NET_BUFFER_LIST链表,调用 NdisFSendBufferLists函数,继续朝下层传递数据包。
最终这个数据包到达真正的网卡。网卡驱动处理完成之后,调用 NdisMSendNetBufferListsComplete 函数,
这个函数最终会调用到我们的Ndisfilter驱动的FilterSendNetBufferListsCompleteHandler 回调函数,在这个回调函数中,
判断出是我们自己创建的NET_BUFFER_LIST数据包,因此释放到内存池,等待下次继续使用。
上面就是 从应用层send函数开始到我们的NDISFilter再到底层网卡的数据包传递过程,

同样的对应recv,
当底层网卡接收到数据包,调用 NdisMIndicateReceiveNetBufferLists 通知上层,这个函数同样会查找NDIS处理链,
查找相关的回调函数来调用,经过NDISFilter,我们的FilterReceiveNetBufferListsHandler 回调函数会被调用,
在这个回调函数中,同样的,获取到NET_BUFFER_LIST数据包,通过IOCTL_READ_FROM_DOWN传递到应用层,
判断接收的包是不是包含 NDIS_RECEIVE_FLAGS_RESOURCES 标志,如果是则直接忽略这个数据包。
否则就调用 NdisFReturnNetBufferLists 完成这个数据包,NdisFReturnNetBufferLists最终会调用网卡驱动的
ReturnNetBufferListsHandler 回调函数,于是网卡驱动认为数据包已经传递成功,同时释放相关资源。
接着,我们的应用层程序接收到IOCTL_READ_FROM_DOWN的数据包,分析处理,然后调用IOCTL_WRITE_TO_UP写到驱动。
然后驱动接收到这个数据,创建新的NET_BUFFER_LIST数据包,调用 NdisFIndicateReceiveNetBufferLists 继续通知上层驱动。
最终这个数据包到达各种协议驱动,当然包括tcpip.sys,tcpip.sys接收到这个数据包,通过分析合并,拆除 TCP(UDP),IP头等处理,
接着朝上层传递,最终到达应用层,然后应用层通讯程序调用recv就接收到了数据。
同时tcpip.sys会调用NdisReturnNetBufferLists来完成这个数据包, 于是我们的 FilterReturnNetBufferListsHandler 回调函数被调用。
在这个回调函数中判断出是我们自己的NET_BUFFER_LIST,释放到内存池,等待下次继续使用。

这个就是整个的通讯流程,
看起来很罗嗦,真正罗嗦是在调用IOCTL转发数据包到应用层这部分,会造成网络通讯性能下降。
虽然我花了许多力气,做了许多优化,还是不能达到理想的效果。这个在百兆网卡上几乎能有很好的表现,
可现在基本上都是千兆网卡,而且将来会是万兆网卡,这种速度,传输的数据量更加庞大。
比如我家的500M宽带,使用电信测速。
在不使用这个驱动传输到应用层的时候,基本能达到500-550M的速度,
而在传递到应用层之后,速度基本只能达到 360-400M的速度,100-200M就这样被吞噬了。
因此闲的没事都会想法来做优化,希望有天能达到比较理想的效果。

这里还有一个问题,就是TCP Offload。
所谓TCP Offload,就是现在的网卡速度越来越快,传输的数据越来越大,TCPIP协议栈的某些耗时运算本来原先是在电脑内部处理的。
现在可以移到网卡硬件来处理,这样可以节省一些CPU消耗。
具体来说如下这些:
校验码计算:包括IPV4校验,TCPV4校验,UDPv4校验,TCPv6校验,UDPv6校验。
TCP大数据包传输:因为以太网硬件限制,传输的每个数据包不能超过 MTU (1514)字节大小,
这样TCPIP协议栈都会把大数据包拆分成小于MTU的数据包,比如TCP协议 send 一个4MBytes的数据,
经过TCPIP协议会被拆分成几千个小包,然后再发给网卡,这种拆包会消耗CPU,因此只要网卡硬件支持,
TCPIP协议栈可以一次传递很大的数据包,由网卡硬件自己来拆分。
NDIS6.3以上的驱动,还支持RSC功能,也就是网卡硬件接收到许多的小数据包合并成一个大的数据包,然后再传递给系统。
当然还包括其他一些Offload的功能,目的都是为了优化系统,提高网络传输性能的。

而在我们的NdisFilter驱动中,这些功能会造成很大的麻烦,因为传递到应用层做NAT转发,需要分析IP数据包,会做相关计算,
而且如果接收到超过MTU的很大的数据包,还得我们自己拆分,一样是个麻烦。因此得屏蔽这些TCP Offload 。
在NDISFilter中,使用OID是 OID_TCP_OFFLOAD_PARAMETERS ,并且填写 NDIS_OFFLOAD_PARAMETERS 数据结构,
把里边我们需要屏蔽的Offload功能全部屏蔽,然后调用NdisFOidRequest发送请求到网卡驱动上 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章