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發送請求到網卡驅動上 。

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