引言 截包的需求一般來自於過濾、轉換協議、截取報文分析等。 過濾型的應用比較多,典型為包過濾型防火牆。 轉換協議的應用局限於一些特定環境。比如第三方開發網絡協議軟件,不能夠與原有操作系統軟件融合,只好采取“嵌入協議棧的塊”(BITS)方式實施。比如IPSEC在Windows上的第三方實現,無法和操作系統廠商提供的IP軟件融合,只好實現在IP層與鏈路層之間,作為協議棧的一層來實現。第三方PPPOE軟件也是通過這種方式實現。 截取包用於分析的目的,用“抓包”描述更恰當一些,“截包”一般表示有截斷的能力,“抓包”只需要能夠獲取即可。實現上一般作為協議層實現。 本文所說的“應用層截包”特指在驅動程序中截包,然後送到應用層處理的工作模式。
截包模式 用戶態下的網絡數據包攔截方式有 1.Winsock Layered Service Provider; 2.windows 2000 包過濾接口; 3.替換系統自帶的WINSOCK動態連接庫; 利用驅動程序攔截網絡數據包的方式有 1.TDI過濾驅動程序(TDI Filter Driver) 2.NDIS中間層驅動程序(NDIS Intermediate Driver) 3.Win2k Filter-Hook Driver 4.NDIS Hook Driver 用戶態下攔截數據包有一些局限性,“很顯然,在用戶態下進行數據包攔截最致命的缺點就是只能在Winsock層次上進行,而對於網絡協議棧中底層協議的數據包無法進行處理。對於一些木馬和病毒來說很容易避開這個層次的防火牆。” 我們所說的“應用層截包”不是指上面描述的在用戶態攔截數據包。而是在驅動程序中攔截,在應用層中處理。要獲得一個通用的方式,應該在IP層之下進行攔截。綜合比較,本文選用中間層模式。
為什麼要在應用層處理截取的報文 一般來說,網絡應用如防火牆,協議類軟件都是工作在內核,我們為什麼要反過來,提出要在應用層處理報文呢?理由也可以找出幾點(哪怕是比較牽強): 眾所周知,驅動程序開發有一定的難度,對於一個經驗豐富的程序員來說,或許開發過程中不存在技術問題,但是對初學者,尤其是第一次接觸的程序員簡直是痛苦的經歷。 另外,開發周期也是一個不得不考慮的問題。程序工作在內核,穩定性/兼容性都需要大量測試,而且可供使用的函數庫相對於應用層來說相當少。在應用層開發,調試修改相對要容易地多。 不利的因素也有: 性能影響,在應用層工作,改變了工作模式,每當驅動程序截到數據,送到應用層處理後再次送回內核,再向上傳遞到IP協議。因此,性能影響非常大,效率非常低,在100Mbps網絡上,只有80%的性能表現。 綜合來看,在特定的場合應用還是比較適合的: 台式機上使用,台式機的網絡負載相當小,不到100Mbps足以滿足要求,尤其是主要用於上網等環境,網絡連接的流量不到512Kbps,根本不用考慮性能因素。作為單機防火牆或其他一些協議實現,分析等很容易基於這種方式實現。
方案 模型 上圖描述了應用層截包的模型,主要的流程如下: 接收報文過程: 1.網絡接口收到報文,中間層截取,通過2送到應用層處理; 2.應用層處理後,送回中間層處理結果; 3.中間層根據處理結果,丟棄該報文,或者將處理後的報文通過1送到IP協議; 4.IP協議及上層應用接收到報文; 發送報文過程: 1.上層應用發送數據,從而IP協議發送報文; 2.報文被中間層截取,通過2送到應用層處理; 3.應用層處理後,送回中間層處理結果; 4.中間層根據處理結果,丟棄該報文,或者將處理後的報文發送到網絡上;
實現細節探討 IO與通訊 有一個很容易的方式,在驅動程序和應用程序之間用一個事件。 在應用程序CreateFile的時候,驅動程序IoCreateSynchronizationEvent一個有名的事件,然後應用程序CreateEvent/OpenEvent此有名事件即可。 注意點: 1,不要在驅動初始化的時候創建事件,此時大多不能成功創建; 2,讓驅動先創建,那麼此後應用程序打開時,只能讀(Waitxxxx),不能寫(SetEvent/ResetEvent)。反之,如果應用程序先創建,則應用程序和驅動程序都有讀寫權限; 3,用名字比較理想,注意驅動中名字在\BaseNamedObjects\下,例如應用程序用“xxxEvent”,那麼驅動中就是“\BaseNamedObjects\xxxEvent”; 4,用HANDLE的方式也可以,但是在WIN98下是否可行,未知。 5,此後,驅動對讀請求應立即返回,否則就返回失敗。不然將失去用事件通知的意義(不再等待讀完成,而是有需要(通知事件)時才會讀); 6,應用程序發現有事件,應該在一個循環中讀取,直到讀取失敗,表明沒有數據可讀;否則會漏掉後續數據,而沒有及時讀取;
處理線程優先級 應用層處理線程應該提高優先級,因為該線程為其他上層應用程序服務,如果優先級比其他線程優先級低的話,將會發生類似死鎖的等待狀態。 另外,提高優先級的時候必須注意,線程盡量縮短運行時間,不要長期占用CPU,否則其他線程無法得到服務。優先級不必提高到REALTIME_PRIORITY_CLASS級,此時線程不能做一些磁盤IO之類的操作,而且也影響到鼠標、鍵盤等工作。 驅動程序也可以動態地提高線程的優先級。
緩存 在驅動程序接收到報文後,至少應該有一個緩沖以便臨時存儲,等待應用層處理。緩沖不必很大,只要能在應用層得到時間片之前緩沖區不溢出就可以了,實踐中大約能存儲幾十個報文就夠了。 緩沖的使用方式,是一個先進先出的隊列。考慮方便實現為靜態存儲的環形隊列,也就是說,不必每次分配內存,而是一次性分配好一大塊內存,環形的使用。 初始,head==tail==0; tail和head都是無限增長的。 Tail – head head表明有數據; tail + input packet length - head >size表明滿; 取數據時: ppacket GetPacket() { ASSERT(tail>=head); if(tail==head) return NULL; //else ppacket = &start[head % SIZE]; if(head % size + ppacket->length > size ) //數據不連續(一部分在尾部,一部分在頭部); else //數據是連續的 return ppacket; } 放入數據: bool InputPacket(ppacket) { if(tail + input packet length - head >size) //滿 return false; //copy packet to &start[tail % SIZE] //if(tail % SIZE + packet length > SIZE) //數據不連續(一部分在尾部,一部分在頭部); //else //數據是連續的 tail = tail + packet length; return true; } 上面這種方式采用數組的方式組織,為每個報文提供一個最大報文長度的空間。因為緩沖區數目有限,因此這種方式可以滿足需要。如果要考慮到減少空間的浪費,那麼可以按每個報文的實際長度存儲,上面的算法不能夠適應這種方式。
應用層和驅動程序的通信 在網卡接收/IP發送過程中,驅動程序緩存報文,用事件通知應用層有報文需要處理。那麼應用層可以通過IO方式或者共享內存方式取得此報文。 實踐說明,在100Mbps速率下,以上兩種方式都可以滿足需要,最為簡便的方式就是使用有緩沖的IO方式。 應用層處理完畢,也可以使用以上兩種方式之一來向驅動程序遞交結果。不過,IO方式因為一次只能發送一個報文,100Mbps網絡速度下降為70%~80%網絡速度,10Mbps不會有影響。也就是說,主機發出的最大速度只有70%的網絡速度,這和應用程序發送不超過MTU的UDP數據報的速度是一樣的。對TCP來說,由於是雙向通信,損失更加大一些,大約40%~60%速度。 這時候,使用共享內存方式,因為減少了系統調用的開銷,可以避免速度下降。
報文發送的速度控制 當IP協議發送報文的時候,一般來說,我們的中間層驅動必須把這些報文緩存起來,告訴IP軟件發送成功,然後讓應用層處理完畢之後再做決定。顯然,存儲報文的速度遠遠超過網卡能夠發送的速度,然而IP軟件(特別是UDP)將以我們存儲的速度發送報文。造成緩存迅速耗盡。後續的報文只好丟棄。這樣一來,UDP發送將不能正常工作。TCP由於可以自行適應網絡狀況,依然可以在這種情況下工作,速度在70%左右。在Passthru裡,可以轉發至低層驅動,然後用異步或同步方式返回,從而達到網卡的發送速度一致。 因此,必須有一個辦法避免這種狀況。中間層驅動把這些報文緩存起來,告訴IP軟件發送狀態未決(Pending)。等到最後處理完畢,告訴IP軟件發送完成。從而協調了發送速度。這種方式帶來一個問題,就是驅動程序必須在發送超時的情況下放棄對這些緩沖報文的所有權。具體來說,就是MiniportReset被調用的時候,就有可能是NDIS察覺到發送超時,從而放棄所有未完成的發送操作,如果沒有正確處理這種情況,將會導致嚴重問題。如果中間層在Miniport初始化的時候通過調用NdisMSetAttributesEx函數設置了NDIS_ATTRIBUTE_IGNORE_PACKET_TIMEOUT標志,那麼中間層驅動程序將不會得到報文超時通知,中間層必須自行處理緩存的報文。
與Passthru協同工作 當上層應用不再需要截包時,驅動程序應該完全是Passthru行為。這就要