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()裡只是增加了一個特性:那就是對於從本機發出的小資料包不進行連線跟蹤。

連線跟蹤系統的初始化流程分析

    有了前面的知識,我們再分析連線跟蹤系統的初始化ip_conntrack_standalone_init()函式就太容易不過了。還是先上ip_conntrack_standalone_init()函式的流程圖:

該函式的核心上圖已經標出來了“初始化連線跟蹤系統”和“註冊連線跟蹤的hook函式”。其他兩塊這裡簡單做個普及,不展開講。至少讓大家明白連線跟蹤為什麼需要兩中檔案系統。

1、  procfs(/proc檔案系統)

這是一個虛擬的檔案系統,通常掛載在/proc,允許核心以檔案的形式向用戶空間輸出內部資訊。該目錄下的所有檔案並沒有實際存在在磁盤裡,但可以通過cat、more或>shell重定向予以寫入,這些檔案甚至可以像普通檔案那樣指定其讀寫許可權。建立這些檔案的核心元件可以說明任何一個檔案可以由誰讀取或寫入。但是:使用者不能在/proc目錄下新增,移除檔案或目錄

2、  sysctl(/proc/sys目錄)

此介面允許使用者空間讀取或修改核心變數的值。不能用此介面對每個核心變數進行操作:核心應該明確指出哪些變數從此介面對使用者空間是可見的。從使用者空間,你可以用兩種方式訪問sysctl輸出的變數:sysctl系統呼叫介面;procfs。當核心支援procfs檔案系統時,會在/proc中增加一個特殊目錄(/proc/sys),為每個由sysctl所輸出的核心變數引入一個檔案,我們通過對這些檔案的讀寫操作就可以影響到核心裡該變數的值了。

    除此之外還有一種sysfs檔案系統,這裡就不介紹了,如果你感興趣可以去研讀《Linux裝置驅動程式》一書的詳細講解。

    那麼回到我們連線跟蹤系統裡來,由此我們可以知道:連線跟蹤系統向用戶空間輸出一些核心變數,方便使用者對連線跟蹤的某些特性加以靈活控制,如改變最大連線跟蹤數、修改TCP、UDP或ICMP協議的連線跟蹤超時是時限等等。

    注意一點:/proc/sys目錄下的任何一個檔名,對應著核心中有一個一模一樣同名的核心變數。例如,我的系統中該目錄是如下這個樣子:

ip_conntrack_init()函式

    該函式承擔了連線跟蹤系統初始化的絕大部分工作,其流程我們也畫出來了,大家可以對照原始碼來一步一步分析。

    第一步:連線跟蹤的表大小跟系統記憶體相關,而最大連線跟蹤數和連線跟蹤表容量的關係是:最大連線跟蹤數=8×連線跟蹤表容量。程式碼中是這樣的:

ip_conntrack_max = 8 × ip_conntrack_htable_size;那麼從上面的圖我們可以看出來,我們可以通過手工修改/proc/sys/net/ipv4/netfilter目錄下同名的ip_conntrack_max檔案即可動態修改連線系統的最大連線跟蹤數了。

    第二步:註冊Netfilter所用的sockopt,先不講,以後再說。只要知道是這裡註冊的就行了。

    第三步:為連線跟蹤hash表ip_conntrack_hash分配記憶體並進行初始化。並建立連線跟蹤和期望連線跟蹤的快取記憶體。

    第四步:將TCP、UDP和ICMP協議的連線跟蹤協議體,根據不同協議的協議號,註冊到全域性陣列ip_ct_protos[]中,如下所示:

    最後再做一些善後工作,例如註冊DROP這個target所需的功能函式,為其他諸如NAT這樣的模組所需的引數ip_conntrack_untracked做初始化,關於這個引數我們在NAT模組中再詳細討論它。

    這樣,我們連線跟蹤系統的初始化工作就算徹底完成了。有了前幾篇關於連線跟蹤的基礎知識,再看程式碼是不是有種神清氣爽,豁然開朗的感覺。

    至於連線跟蹤系統所提供的那五個hook函式的註冊,我想現在的你應該連都不用看就知道它所做的事情了吧。