1. 程式人生 > >TDI Filter 過濾驅動

TDI Filter 過濾驅動

                                                                           By Fanxiushu  2013, 引用和轉載請註明原作者

為了讓大家有興趣閱讀下去,
舉個正在使用的可能大家都比較熟悉的例子: 360 的安全衛士裡,有個流量防火牆的功能,
它可以監視每個程序的流量情況,可以限制上傳下載速度,等等。
他的驅動部分的就是一個 TDI Filter 驅動。

TDI Filter ,這是個快被微軟淘汰的驅動模式,但是為了相容,又不得不使用的驅動。
是因為新的Windows系統中,有了更簡單的開發框架替代TDI框架。
到某天XP系統如win98,win2000那樣消失的時候,TDI功能會被微軟從新的系統中去除掉,到時就真正不能使用TDI做開發了。
如果你的應用要能相容XP,WIN7,WIN8,而且又不想寫兩套程式碼來分別應付兩套不同的系統,那使用TDI是最好的選擇。
如果你的應用只在WIN7 以上的系統裡執行,可以使用WFP代替TDI Filter。
至於說win7以上系統TDI效率比較差的問題,你又不是做伺服器,個人PC機以現在的硬體能力,這點損失基本忽略不計。

據說WFP是比TDI Filter簡單,實際上微軟的東西都是那麼臃腫,能簡單到哪去呢;
WIN7以上又出來一個WDF框架,類似應用層的MFC框架一樣,把底層程式碼封裝了一遍,
作為一個初學者或者想深入理解核心的工程師,我覺得還是應從WDM基礎和原理上去研究。

關於TDI Filter驅動,網上介紹比較多。還有開源工程tdifw,更詳細的從程式碼上述說了TDI Filter的開發細節。
很多時候,這個東西是用來做網路防火牆的,就是可以阻止你想要阻止的網路連線,
讓不想訪問的資料包不進入到你的電腦裡,同時可以實時監控網路流量,限制流量,修改資料包等等。

TDI Filter是標準的NT式裝置過濾驅動,所以按照標準的NT式過濾驅動模式來開發TDI就是最正確的了。

首先在 DriverEntry裡 替換掉所有的派遣函式為自己的函式,如下
for( int i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
       DriverObject->MajorFunction[i] = myDispatch;

接著呼叫IoCreateDevice建立裝置,然後呼叫 IoAttachDevice 掛載到 \Device\Tcp和Device\Udp等 TDI 標準裝置上,
若有必要再建立一個控制裝置,用來跟使用者程式通訊。
這樣一個TDI Filter 就初始化成功了,接著主要的任務就是 在 myDispatch 派遣函式裡處理具體的任務了。
我們最關心的其實就3個MAJOR命令:
IRP_MJ_CREATE                                          //建立地址物件和連線物件
IRP_MJ_CLEANUP                                       //銷燬地址物件和連線物件
IRP_MJ_INTERNAL_DEVICE_CONTROL     //傳送網路資料傳輸等命令

簡單來說 TDI Filter 就是對以上三個命令進行過濾,但是最後一個命令牽涉到的子命令很多,所以整個的過濾就比較臃腫了。
下文所說的 依照 TDI 開頭的命令,都是 IRP_MJ_INTERNAL_DEVICE_CONTROL 的子命令。

虛擬碼如下:
NTSTATUS DriverEntry(.....)
{
       for(int i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
             DriverObject->MajorFunction[i] = myDispatch;
     
       ////建立TCP過濾裝置並掛載到 標準 的TDI的TCP
       IoCeateDevice( .... &tcp_filter_dev,...);
       IoAttachDevice( tcp_filter_dev, "\\Device\\Tcp",... );
     
      ////建立UDP過濾裝置,並掛載到 標準TDI的UDP
      IoCreateDevice( ...., &udp_filter_dev, ... );
      IoAttachDevice( udp_filter_dev, "\\Device\\Udp", ... );
      
      ////若有必要,建立一個控制裝置
      IoCreateDevice( ... &control_dev, ... );
    .............
}
NTSTATUS myDispatch( PDEVICE_OBJECT dev, PIRP irp)
{
     irpStack = IoGetCurrentIrpStackLocation( irp);
     /////
     switch( irpStack->MajorFunction)
     {
     case IRP_MJ_CREATE:
           ....
           break;
    case IRP_MJ_CLEANUP:
          .....
          break;
    case IRP_MJ_INTERNAL_DEVICE_CONTROL:
         .....
         break;
     }
     ...............
   
}

應用層的網路資料包,絕大部分都是TCP和UDP協議的,所有要通訊的程式,進入到Windows核心,都會呼叫afd.sys驅動,
afd.sys驅動管理所有應用層的套接字,他是TDI的客戶端,
比如我們在程式中呼叫socket和connect等函式進行TCP通訊時,afd.sys驅動會開啟TDI的TCP裝置,
建立至少兩個物件:一個地址物件,一個連線物件;
地址物件繫結到本地某個未使用的地址(本地IP + 本地埠),
連線物件是用來跟遠端機器建立起一對一的連線,以後在資料通訊中,都使用這個連線物件進行資料的收發工作。
當兩個物件建立好之後,afd.sys接著就會把連線物件跟本地地址物件關聯起來(TDI_ASSOCIATE_ADDRESS ),
這樣才能知道這個連線物件是使用這個本地地址跟遠端機器通訊。
關聯好之後,接著就傳送連線命令(TDI_CONNECT),成功之後,就可以使用這個連線物件進行資料收發工作了。
以上說的是 TCP協議中,作為客戶端的機器的工作情況。
TCP協議中,作為服務端的工作情況,稍微有點差別,
應用程式呼叫socket,bind,listen之後,afd.sys驅動依然會首先建立地址物件, 
接著會建立 N 個連線物件,這個N一般是listen函式的引數。
然後依然需要把連線物件跟地址物件關聯起來。
接著就等待 TDI_EVENT_CONNECT 事件,當有客戶端連上來之後,這事件會被相應,於是連線成功後,
接著就跟客戶端那樣用連線物件收發資料了。

在建立連線物件時候,還必須把他的一個引數:連線上下文(其實就是一個指標值)儲存起來,
這樣我們在收資料包(實際是接收事件中)的時候,才能根據這個連線上下文找到正確的連線物件。

至於UDP通訊,那就簡單多了,他不存在連線的概念,所以不需要連線物件,
afd.sys驅動只建立一個地址物件,然後就用這個地址物件收發資料包。

接著看看收發資料包的命令,
傳送資料包比較簡單點:
在TCP中,使用 TDI_SEND 命令傳送,
在UDP中,使用 TDI_SEND_DATAGRAM命令傳送。
也沒什麼特別,只需按照一般的過濾驅動模式處理這些命令即可。

收資料包就比較複雜了,他除了提供TDI_RECEIVE和TDI_RECEIVE_DATAGRAM命令之外,
還提供事件回撥函式處理接收操作,
要能抓取所有接收的資料包,我們必須對這些事件進行處理。
所謂的事件回撥,是afd.sys驅動會發送一個 TDI_SET_EVENT_HANDLER命令下來,
並提供某些事件的回撥函式地址
告訴 tcpip.sys 協議驅動說,我打算利用這些地址來接收資料,當你有資料的時候,就呼叫這些回撥函式告訴給我。
如果回撥函式裡的資料還不能完全滿足我,我就接著傳送 TDI_RECEIVE 請求更多的資料。

TDI filter過濾驅動一般就在 TDI_SET_EVENT_HANDLER命令中,儲存上層的回撥函式地址,然後用我們自己的地址替換掉,
當tcpip.sys有資料就會呼叫我們過濾驅動裡的回撥函式,我們做些處理,然後接著呼叫我們儲存的上層的回撥函式地址。
TCP對應的事件我們一般感興趣的如下:
TDI_EVENT_CONNECT                //連線 事件,這是作為伺服器端的TCP相應的事件,當有客戶端連上來,此事件回撥函式會被tcpip.sys呼叫
TDI_EVENT_DISCONNECT          //同上,這是斷開連線的事件
TDI_EVENT_RECEIVE                 //接收事件,當有資料到來時候,tcpip.sys會呼叫此事件的回撥函式
TDI_EVENT_RECEIVE_EXPEDITED   //這個是 OOB,就是TCP概念中的緊急帶外資料,依然是接收事件
TDI_EVENT_CHAINED_RECEIVE       //這個是 只讀接收事件,就是 客戶端可以一次性讀取的資料。其實具體有何用處,我也不太清楚,反正也必須得處理
TDI_EVENT_CHAINED_RECEIVE_EXPEDITED  //這個是隻讀事件的帶外資料事件。

UDP的事件也比這簡單多,主要是兩個
TDI_EVENT_RECEIVE_DATAGRAM
TDI_EVENT_CHAINED_RECEIVE_DATAGRAM
意思同TCP的差不多,不過第2個事件,我們其實都可以懶得去處理。


總結一下以上的我們必須處理的子命令:
TCP
關聯連線物件和地址物件:TDI_ASSOCIATE_ADDRESS,TDI_DISASSOCIATE_ADDRESS
主動連線:        TDI_CONNECT, TDI_DISCONNECT
被動連線事件: TDI_EVENT_CONNECT, TDI_EVENT_DISCONNECT
傳送 :              TDI_SEND
接收:               TDI_RECEIVE
接收事件:        TDI_EVENT_CONNECT,TDI_EVENT_DISCONNECT, TDI_EVENT_RECEIVE,TDI_EVENT_RECEIVE_EXPEDITED,
TDI_EVENT_CHAINED_RECEIVE, TDI_EVENT_CHAINED_RECEIVE_EXPEDITED

UDP
傳送:        TDI_SEND_DATAGRAM
接收:        TDI_RECEIVE_DATAGRAM
接收事件: TDI_EVENT_RECEIVE_DATAGRAM,TDI_EVENT_CHAINED_RECEIVE_DATAGRAM

TCP和UDP都必須處理的命令:
設定事件的命令: TDI_SET_EVENT_HANDLER
還有主命令: IRP_MJ_CREATE, IRP_MJ_CLEANUP

其實算算也不算太多,複雜的是TCP。

接著說說如何獲得哪些程序佔用和釋放了哪些TCP和UDP本地埠,(這是我比較感興趣的)。
主要是對 IRP_MJ_CREATE命令的處理,
上面已經說了,afd.sys驅動會首先建立地址物件,就是為了繫結到本地的(IP+PORT)地址,
就是在這個命令中獲得每個套接字繫結到的本地埠。
在此命令中 直接呼叫 PsGetCurrentProcessId  即可獲得這個套接字所在的使用者程序。
上層建立 地址物件和連線物件時候,會有一個FILE_FULL_EA_INFORMATION 結構的引數儲存到
IRP->AssociatedIrp.SystemBuffer 裡,我們取出這個引數,進行判斷,
FILE_FULL_EA_INFORMATION* ea = (FILE_FULL_EA_INFORMATION*)IRP->AssociatedIrp.SystemBuffer;
如上,如果ea為空,說明建立的是其他物件,而不是地址物件和連線物件,(afd.sys有可能還會建立控制物件)
如果不為空,可對 ea進行判斷,從而知道上層建立的究竟是地址物件,還是連線物件。
如果判斷出是地址物件,必須要等到這個IRP完成之後,才能正確獲得本地埠。
於是設定完成函式,在完成函式裡,呼叫TDI_QUERY_INFORMATION查詢這個地址物件繫結到的本地埠。
原理不復雜,但是處理細節挺討厭的。
有興趣可仔細看看我在工程的處理細節。

最後簡單說說,如何監控每個程序的流量已經每個程序每個連線的流量,以及簡單阻止程序訪問網路。
要做到如上幾點,我們必須用一個結構來儲存 地址物件和連線物件,同時要能快速的查詢和刪除。
同時也要儲存 程序ID和程序相關的結構。
我使用的是 rbtree.c和rbtree.h (linux核心中的紅黑樹原始碼)。
每個物件都可以在IRP_MJ_CTRAETE命令處理中獲得程序ID,
我們其實就是在IRP_MJ_CREATE命令中構造一個以建立的物件為key的結構,
然後把相關的所以資訊填寫進去(包括程序ID,流量初始化,等等)。
在 IRP_MJ_CLEANUP中,再刪除這個物件對應的結構。
在 TCP的連線命令和事件中,通過判斷程序是否需要禁止訪問網路,從而決定是否建立此連線。
在接收命令和事件中,通過連線物件和連線上下文(或者UDP中只有地址物件)來找到我們的結構,
通過此結構找到程序相關的結構,然後把流量累加上去。
傳送也是如此。

http://download.csdn.net/detail/fanxiushu/5198130

上面的連線是對應的TDI工程,是借用 tdifw的思想,重新搭建的TDI Filter框架
如果您對這程式碼不感興趣,可直接查閱 tdifw開源工程。

附:(直到2013-04-09,大致發現以上連線的工程的以下BUG,以此作為記錄,方便查詢)

1,在 xioctl.cpp原始檔中,xi_done_laddr_irps的函式,不管成功與失敗,都應該返回 STATUS_SUCCESS,否則應用程式不能獲得全部的埠變化通知。

2,還是在 xi_done_laddrirps函式裡,在呼叫IoSetCancelRoutine之前應該 InitializeListHead( &irp->Tail.Overlay.ListEntry ); 否則可能會 BSOD。

3,xfilter.cpp程式碼裡呼叫xf_set_event_handler設定事件地址:

上層取消此次事件,直接設定evt->old_handler為空,但是還沒把命令下傳給底層驅動,

很可能在這空檔時間內,底層驅動繼續呼叫回撥函式,所以在所有事件中,必須對old_handler是否為空進行判斷,否則可能會BSOD。

4,應該在 IRP_MJ_CLEANUP的完成函式裡刪除 obj_t物件結構,否則如上那樣,容易在底層還麼關閉期間,而obj_t結構刪除了,驅動訪問無效地址而BSOD。

5, xf_recv.cpp原始檔的xf_event_chain_receive函式不能簡單返回 STATUS_DATA_NOT_ACCEPTED了事,要呼叫原來的回撥函式,否則可能會使上層接收不到資料.

6,xf_creat.cpp的完成函式裡,在建立地址物件失敗的時候,應該呼叫 tdi->xf.laddr.free(ctx) 釋放先前建立的記憶體,否則時間長了,會記憶體洩漏從而造成BSOD。

7,xf_connect.cpp處理TCP連線,應該是成功後之後才查詢本地地址,程式碼未在完成函式裡做出錯判斷。

(到2013-04-12為止,大致發現這些BUG,工程就懶得更新了(主要是 CSDN沒提供一個功能能夠刪除替換原來上傳的程式碼),有興趣的朋友可以幫忙找BUG,非常感謝)

附:(簡單介紹如何限流)

TDI 流量控制分為TCP和UDP部分,UDP的處理則比較簡單,在對應的收發包函式裡邊,比如__xf_receive_datagram_complete 裡邊,

根據某種流量控制演算法,決定對這個包丟棄還是接收,

TCP稍微麻煩些,在TDI層的TCP已經處於連線狀態,不能簡單丟包處理。

而是使用延時傳輸的辦法,比如xf_receive裡邊, 也是根據某種流量演算法,在一定時間內,如果超過規定的最大值,

則在此函式中,把IRP掛載到延時處理佇列裡,返回STATUS_PENDING延時完成這個IRP,

至於何時完成這個IRP,則根據演算法計算出一個時間值,在另外一個執行緒裡,達到這個時間值在完成。 這樣就能對TCP進行限流處理了。

每個UDP和TCP收發的IRP包,都能獲取他所在的程序ID,因此可以細化到單個程序限流。

也能獲取到埠資訊,所以可以細化到埠限流,反正只要想得到,就能做得到。