(六)洞悉linux下的Netfilter&iptables:如何理解連接跟蹤機制?【中】

Netfilter連接跟蹤的詳細流程

    上一篇我們瞭解了連接跟蹤的基本框架和大概流程,本篇我們着重分析一下,數據包在連接跟蹤系統裏的旅程,以達到對連接跟蹤運行原理深入理解的目的。

    連接跟蹤機制在Netfilter框架裏所註冊的hook函數一共就五個:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local()、ip_conntrack_help()

和ip_confirm()。前幾篇博文中我們知道ip_conntrack_local()最終還是調用了ip_conntrack_in()。這五個hook函數及其掛載點,想必現在大家應該也已經爛熟於心了,如果記不起來請看【上】篇博文。

    在連接跟蹤的入口處主要有三個函數在工作:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local();在出口處就兩個:ip_conntrack_help()和ip_confirm()。

接下來的事情就變得非常奇妙,請大家將自己當作一個需要轉發的數據包,且是一條新的連接。然後跟隨我去連接跟蹤裏耍一圈吧。在進入連接跟蹤之前,我需要警告大家:連接跟蹤雖然不會改變數據包本身,但是它可能會將數據包丟棄。

 

我們的旅行的線路圖已經有了:

 

ip_conntrack_defrag()

    當我們初到連接跟蹤門口的時候,是這位小生來招待我們。這個函數主要是完成IP報文分片的重新組裝,將屬於一個IP報文的多個分片重組成一個真正的報文。關於IP分片,大家可以去閱讀《TCP/IP詳解卷1》瞭解一點基礎,至於IP分片是如何被重新組裝一個完整的IP報文也不是我們的重心,這裏不展開講。該函數也向我們透露了一個祕密,那就是連接跟蹤只跟蹤完整的IP報文,不對IP分片進行跟蹤,所有的IP分片都必須被還原成原始報文,才能進入連接跟蹤系統。

 

ip_conntrack_in()

    該函數的核心是resolve_normal_ct()函數所做的事情,其執行流程如下所示:

在接下來的分析中,需要大家對上一篇文章提到的幾個數據結構:

ip_conntrack{}、ip_conntrack_tuple{}、ip_conntrack_tuple_hash{}和ip_conntrack_protocol{}以及它們的關係必須弄得很清楚,你才能徹底地讀懂resolve_normal_ct()函數是幹什麼。最好手頭再有一份2.6.21的內核源碼,然後打開source insight來對照着閱讀效果會更棒!

第一步:ip_conntrack_in()函數首先根據數據包skb的協議號,在全局數組ip_ct_protos[]中查找某種協議(如TCP,UDP或ICMP等)所註冊的連接跟蹤處理模塊ip_conntrack_protocol{},如下所示。

在結構中,具體的協議必須提供將屬於它自己的數據包skb轉換成ip_conntrack_tuple{}結構的回調函數pkt_to_tuple()和invert_tuple(),用於處理新連接的new()函數等等。

第二步:找到對應的協議的處理單元proto後,便調用該協議提供的錯誤校驗函數(如果該協議提供的話)error來對skb進行合法性校驗。

    第三步:調用resolve_normal_ct()函數。該函數的重要性不言而喻,它承擔連接跟蹤入口處剩下的所有工作。該函數根據skb中相關信息,調用協議提供的pkt_to_tuple()函數生成一個ip_conntrack_tuple{}結構體對象tuple。然後用該tuple去查找連接跟蹤表,看它是否屬於某個tuple_hash{}鏈。請注意,一條連接跟蹤由兩條ip_conntrack_tuple_hash{}鏈構成,一“去”一“回”,參見上一篇博文末尾部分的講解。爲了使大家更直觀地理解連接跟蹤表,我將畫出來,如下圖,就是個雙向鏈表的數組而已。

如果找到了該tuple所屬於的tuple_hash鏈表,則返回該鏈表的地址;如果沒找到,表明該類型的數據包沒有被跟蹤,那麼我們首先必須建立一個ip_conntrack{}結構的實例,即創建一個連接記錄項。

然後,計算tuple的應答repl_tuple,對這個ip_conntrack{}對象做一番必要的初始化後,其中還包括,將我們計算出來的tuple和其反向tuple的地址賦給連接跟蹤ip_conntrack裏的tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]。

最後,把ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]的地址返回。這恰恰是一條連接跟蹤記錄初始方向鏈表的地址。Netfilter中有一條鏈表unconfirmed,裏面保存了所有目前還沒有收到過確認報文的連接跟蹤記錄,然後我們的ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]就會被添加到unconfirmed鏈表中。

第四步:調用協議所提供的packet()函數,該函數承擔着最後向Netfilter框架返回值的使命,如果數據包不是連接中有效的部分,返回-1,否則返回NF_ACCEPT。也就是說,如果你要爲自己的協議開發連接跟蹤功能,那麼在實例化一個ip_conntrack_protocol{}對象時必須對該結構中的packet()函數做仔細設計。

雖然我不逐行解釋代碼,只分析原理,但有一句代碼還是要提一下。

resolve_normal_ct()函數中有一行ct = tuplehash_to_ctrack(h)的代碼,參見源代碼。其中h是已存在的或新建立的ip_conntrack_tuple_hash{}對象,ct是ip_conntrack{}類型的指針。不要誤以爲這一句代碼的是在創建ct對象,因爲創建的工作在init_conntrack()函數中已經完成。本行代碼的意思是根據ip_conntrack{}結構體中tuplehash[IP_CT_DIR_ORIGINAL]成員的地址,反過來計算其所在的結構體ip_conntrack{}對象的首地址,請大家注意。

大家也看到ip_conntrack_in()函數只是創建了用於保存連接跟蹤記錄的ip_conntrack{}對象而已,並完成了對其相關屬性的填充和狀態的設置等工作。簡單來說,我們這個數據包目前已經拿到連接跟蹤系統辦法的“綠卡”ip_conntrack{}了,但是還沒有蓋章生效。

 

ip_conntrack_help()

大家只要把我前面關於鉤子函數在五個HOOK點所掛載情況的那張圖記住,就明白ip_conntrack_help()函數在其所註冊的hook點的位置了。當我們這個數據包所屬的協議在其提供的連接跟蹤模塊時已經提供了ip_conntrack_helper{}模塊,或是別人針對我們這種協議類型的數據包提供了擴展的功能模塊,那麼接下來的事兒就很簡單了:

首先,判斷數據包是否拿到“綠卡”,即連接跟蹤是否爲該類型協議的包生成了連接跟蹤記錄項ip_conntrack{};

其次,該數據包所屬的連接狀態不屬於一個已建連接的相關連接,在其響應方向。

兩個條件都成立,就用該helper模塊提供的help()函數去處理我們這個數據包skb。最後,這個help()函數也必須向Netfilter框架返回NF_ACCEPT或NF_DROP等值。任意一個條件不成立則ip_conntrack_help()函數直接返回NF_ACCEPT,我們這個數據包繼續傳輸。

 

ip_confirm()

    該函數是我們離開Netfilter時遇到的最後一個傢伙了,如果我們這個數據包已經拿到了“綠卡”ip_conntrack{},並且我們這個數據包所屬的連接還沒收到過確認報文,並且該連接還未失效。然後,我們這個ip_confirm()函數要做的事就是:

    拿到連接跟蹤爲該數據包生成ip_conntrack{}對象,根據連接“來”、“去”方向tuple計算其hash值,然後在連接跟蹤表ip_conntrack_hash[]見上圖中查找是否已存在該tuple。如果已存在,該函數最後返回NF_DROP;如果不存在,則將該連接“來”、“去”方向tuple插入到連接跟蹤表ip_conntrack_hash[]裏,並向Netfilter框架返回NF_ACCEPT。之所以要再最後纔將連接跟蹤記錄加入連接跟蹤表是考慮到數據包可能被過濾掉。

    至此,我們本次旅行就圓滿結束了。這裏我們只分析了轉發報文的情況。發送給本機的報文流程與此一致,而對於所有從本機發送出去的報文,其流程上唯一的區別就是在調用ip_conntrack_in()的地方換成了ip_conntrack_local()函數。前面說過,ip_conntrack_local()裏面其實調用的還是ip_conntrack_in()。ip_conntrack_local()裏只是增加了一個特性:那就是對於從本機發出的小數據包不進行連接跟蹤。

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