如何理解Netfilter中的連線跟蹤機制?

    本篇我打算以一個問句開頭,因為在知識探索的道路上只有多問然後充分調動起思考的機器才能讓自己走得更遠。連線跟蹤定義很簡單:用來記錄和跟蹤連線的狀態。

問:為什麼又需要連線跟蹤功能呢?

答:因為它是狀態防火牆NAT的實現基礎。

OK,算是明白了。Neftiler為了實現基於資料連線狀態偵測的狀態防火牆功能和NAT地址轉換功能才開發出了連線跟蹤這套機制。那就意思是說:如果編譯核心時開啟了連線跟蹤選項,那麼Linux系統就會為它收到的每個資料包維持一個連線狀態用於記錄這條資料連線的狀態。接下來我們就來研究一下Netfilter的連線跟蹤的設計思想和實現方式。

    之前有一副圖,我們可以很明確的看到:用於實現連線跟蹤入口的hook函式以較高的優先順序分別被註冊到了netfitler的NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT兩個hook點上;用於實現連線跟蹤出口的hook函式以非常低的優先順序分別被註冊到了netfilter的NF_IP_LOCAL_IN和NF_IP_POST_ROUTING兩個hook點上。

其實PRE_ROUTING和LOCAL_OUT點可以看作是整個netfilter的入口,而POST_ROUTING和LOCAL_IN可以看作是其出口。在只考慮連線跟蹤的情況下,一個數據包無外乎有以下三種流程可以走:

一、傳送給本機的資料包

流程:PRE_ROUTING----LOCAL_IN---本地程序

二、需要本機轉發的資料包

流程:PRE_ROUTING---FORWARD---POST_ROUTING---外出

三、從本機發出的資料包

流程:LOCAL_OUT----POST_ROUTING---外出

我們都知道在INET層用於表示資料包的結構是大名鼎鼎的sk_buff{}(後面簡稱skb),如果你不幸的沒聽說過這個東東,那麼我強烈的建議你先補一下網路協議棧的基礎知識再繼續閱讀這篇文章。在skb中有個成員指標nfct,型別是struct nf_conntrack{},該結構定義在include/linux/skbuff.h檔案中。該結構記錄了連線記錄被公開應用的計數,也方便其他地方對連線跟蹤的引用。連線跟蹤在實際應用中一般都通過強制型別轉換將nfct轉換成指向ip_conntrack{}型別(定義在include/linux/netfilter_ipv4/ip_conntrack.h裡)來獲取一個數據包所屬連線跟蹤的狀態資訊的。即:Neftilter框架用ip_conntrack{}來記錄一個數據包與其連線的狀態關係

同時在include/linux/netfilter_ipv4/ip_conntrack.h檔案中還提供了一個非常有用的介面:struct ip_conntrack *ip_conntrack_get(skb, ctinfo)用於獲取一個skb的nfct指標,從而得知該資料包的連線狀態和該連線狀態的相關資訊ctinfo。從連線跟蹤的角度來看,這個ctinfo表示了每個資料包的幾種連線狀態:

l  IP_CT_ESTABLISHED

Packet是一個已建連線的一部分,在其初始方向。

l  IP_CT_RELATED

Packet屬於一個已建連線的相關連線,在其初始方向。

l  IP_CT_NEW

Packet試圖建立新的連線

l  IP_CT_ESTABLISHED+IP_CT_IS_REPLY

Packet是一個已建連線的一部分,在其響應方向。

l  IP_CT_RELATED+IP_CT_IS_REPLY

Packet屬於一個已建連線的相關連線,在其響應方向。

    在連線跟蹤內部,收到的每個skb首先被轉換成一個ip_conntrack_tuple{}結構,也就是說ip_conntrack_tuple{}結構才是連線跟蹤系統所“認識”的資料包。那麼skb和ip_conntrack_tuple{}結構之間是如何轉換的呢?這個問題沒有一個統一的答案,與具體的協議息息相關。例如,對於TCP/UDP協議,根據“源、目的IP+源、目的埠”再加序列號就可以唯一的標識一個數據包了;對於ICMP協議,根據“源、目的IP+型別+代號”再加序列號才可以唯一確定一個ICMP報文等等。對於諸如像FTP這種應用層的“活動”協議來說情況就更復雜了。本文不試圖去分析某種具體協議的連線跟蹤實現,而是探究連線跟蹤的設計原理和其工作流程,使大家掌握連線跟蹤的精髓。因為現在Linux核心更新的太快的都到3.4.x,變化之大啊。就算是2.6.22和2.6.21在連線跟蹤這塊還是有些區別呢。一旦大家理解了連線跟蹤的設計思想,掌握了其神韻,它再怎麼也萬變不離其宗,再看具體的程式碼實現時就不會犯迷糊了。俗話說“授人一魚,不如授人一漁”,我們教給大家的是方法。有了方法再加上自己的勤學苦練,那就成了技能,最後可以使得大家在為自己的協議開發連線跟蹤功能時心裡有數。這也是我寫這個系列博文的初衷和目的。與君共勉。

在開始分析連線跟蹤之前,我們還是站在統帥的角度來俯視一下整個連線跟蹤的佈局。這裡我先用比較粗略的精簡流程圖為大家做個展示,目的是方便大家理解,好入門。當然,我的理解可能還有不太準確的地方,還請大牛們幫小弟指正。

    我還是重申一下:連線跟蹤分入口和出口兩個點。謹記:入口時建立連線跟蹤記錄,出口時將該記錄加入到連線跟蹤表中。我們分別來看看。

入口:

整個入口的流程簡述如下:對於每個到來的skb,連線跟蹤都將其轉換成一個tuple結構,然後用該tuple去查連線跟蹤表。如果該型別的資料包沒有被跟蹤過,將為其在連線跟蹤的hash表裡建立一個連線記錄項,對於已經跟蹤過了的資料包則不用此操作。緊接著,呼叫該報文所屬協議的連線跟蹤模組的所提供的packet()回撥函式,最後根據狀態改變連線跟蹤記錄的狀態。

出口:

整個出口的流程簡述如下:對於每個即將離開Netfilter框架的資料包,如果用於處理該協議型別報文的連線跟蹤模組提供了helper函式,那麼該資料包首先會被helper函式處理,然後才去判斷,如果該報文已經被跟蹤過了,那麼其所屬連線的狀態,決定該包是該被丟棄、或是返回協議棧繼續傳輸,又或者將其加入到連線跟蹤表中。

連線跟蹤的協議管理:

    我們前面曾說過,不同協議其連線跟蹤的實現是不相同的。每種協議如果要開發自己的連線跟蹤模組,那麼它首先必須例項化一個ip_conntrack_protocol{}結構體型別的變數,對其進行必要的填充,然後呼叫ip_conntrack_protocol_register()函式將該結構進行註冊,其實就是根據協議型別將其設定到全域性陣列ip_ct_protos[]中的相應位置上。

     ip_ct_protos變數裡儲存連線跟蹤系統當前可以處理的所有協議,協議號作為陣列唯一的下標,如下圖所示。

    結構體ip_conntrack_protocol{}中的每個成員,核心原始碼已經做了很詳細的註釋了,這裡我就不一一解釋了,在實際開發過程中我們用到了哪些函式再具體分析。

  連線跟蹤的輔助模組:

    Netfilter的連線跟蹤為我們提供了一個非常有用的功能模組:helper。該模組可以使我們以很小的代價來完成對連線跟蹤功能的擴充套件。這種應用場景需求一般是,當一個數據包即將離開Netfilter框架之前,我們可以對資料包再做一些最後的處理。從前面的圖我們也可以看出來,helper模組以較低優先順序被註冊到了Netfilter的LOCAL_OUT和POST_ROUTING兩個hook點上。

每一個輔助模組都是一個ip_conntrack_helper{}結構體型別的物件。也就是說,如果你所開發的協議需要連線跟蹤輔助模組來完成一些工作的話,那麼你必須也去例項化一個ip_conntrack_helper{}物件,對其進行填充,最後呼叫ip_conntrack_helper_register{}函式將你的輔助模組註冊到全域性變數helpers裡,該結構是個雙向連結串列,裡面儲存了當前已經註冊到連線跟蹤系統裡的所有協議的輔助模組。

全域性helpers變數的定義和初始化在net/netfilter/nf_conntrack_helper.c檔案中完成的。

最後,我們的helpers變數所表示的雙向連結串列一般都是像下圖所示的這樣子:

由此我們基本上就可以知道,註冊在Netfilter框架裡LOCAL_OUT和POST_ROUTING兩個hook點上ip_conntrack_help()回撥函式所做的事情基本也就很清晰了:那就是通過依次遍歷helpers連結串列,然後呼叫每個ip_conntrack_helper{}物件的help()函式。

期望連線:

    Netfilter的連線跟蹤為支援諸如FTP這樣的“活動”連線提供了一個叫做“期望連線”的機制。我們都知道FTP協議服務端用21埠做命令傳輸通道,主動模式下伺服器用20埠做資料傳輸通道;被動模式下伺服器隨機開一個高於1024的埠,然後客戶端來連線這個埠開始資料傳輸。也就是說無論主、被動,都需要兩條連線:命令通道的連線和資料通道的連線。連線跟蹤在處理這種應用場景時提出了一個“期望連線”的概念,即一條資料連線和另外一條資料連線是相關的,然後對於這種有“相關性”的連線給出自己的解決方案。我們說過,本文不打算分析某種具體協議連線跟蹤的實現。接下來我們就來談談期望連線。

    每條期望連線都用一個ip_conntrack_expect{}結構體型別的物件來表示,所有的期望連線儲存在由全域性變數ip_conntrack_expect_list所指向的雙向連結串列中,該連結串列的結構一般如下:

         結構體ip_conntrack_expect{}中的成員及其意義在核心原始碼中也做了充分的註釋,這裡我就不逐一介紹了,等到需要的時候再詳細探討。

連線跟蹤表:

    說了半天終於到我們連線跟蹤表拋頭露面的時候了。連線跟蹤表是一個用於記錄所有資料包連線資訊的hash散列表,其實連線跟蹤表就是一個以資料包的hash值組成的一個雙向迴圈連結串列陣列,每條連結串列中的每個節點都是ip_conntrack_tuple_hash{}型別的一個物件。連線跟蹤表是由一個全域性的雙向連結串列指標變數ip_conntrack_hash[]來表示。為了使我們更容易理解ip_conntrack_hash[]這個雙向迴圈連結串列的陣列,我們將前面提到的幾個重要的目前還未介紹的結構ip_conntrack_tuple{}ip_conntrack{}ip_conntrack_tuple_hash{}分別介紹一下。

    我們可以看到ip_conntrack_tuple_hash{}僅僅是對ip_conntrack_tuple{}的封裝而已,將其組織成了一個雙向連結串列結構。因此,在理解層面上我們可以認為它們是同一個東西。

在分析ip_conntrack{}結構時,我們將前面所有和其相關的資料結構都列出來,方便大家對其理解和記憶。

該圖可是說是連線跟蹤部分的資料核心,接下來我們來詳細說說ip_conntrack{}結構中相關成員的意義。

l  ct_general:該結構記錄了連線記錄被公開應用的計數,也方便其他地方對連線跟蹤的引用。

l  status:資料包連線的狀態,是一個位元點陣圖。

l  timeout:不同協議的每條連線都有預設超時時間,如果在超過了該時間且沒有屬於某條連線的資料包來重新整理該連線跟蹤記錄,那麼會呼叫這種協議型別提供的超時函式。

l  counters:該成員只有在編譯核心時打開了CONFIG_IP_NF_CT_ACCT開完才會存在,代表某條連線所記錄的位元組數和包數。

l  master:該成員指向另外一個ip_conntrack{}。一般用於期望連線場景。即如果當前連線是另外某條連線的期望連線的話,那麼該成員就指向那條我們所屬的主連線。

l  helper:如果某種協議提供了擴充套件模組,就通過該成員來呼叫擴充套件模組的功能函式。

l  proto:該結構是ip_conntrack_proto{}型別,和我們前面曾介紹過的用於儲存不同協議連線跟蹤的ip_conntrack_protocol{}結構不要混淆了。前者是個列舉型別,後者是個結構體型別。這裡的proto表示不同協議為了實現其自身的連線跟蹤功能而需要的一些額外引數資訊。目前這個列舉型別如下:

   如果將來你的協議在實現連線跟蹤時也需要一些額外資料,那麼可以對該結構進行擴充。

l  help:該成員代表不同的應用為了實現其自身的連線跟蹤功能而需要的一些額外引數資訊,也是個列舉型別的ip_conntrack_help{}結構,和我們前面剛介紹過的結構體型別ip_conntrack_helpers{}容易混淆。ip_conntrack_proto{}是為協議層需要而存在的,而ip_conntrack_help{}是為應用層需要而存在

l  tuplehash:該結構是個ip_conntrack_tuple_hash{}型別的陣列,大小為2。tuplehash[0]表示一條資料流“初始”方向上的連線情況,tuplehash[1]表示該資料流“應答”方向的響應情況,見上圖所示。

    到目前為止,我們已經瞭解了連線跟蹤設計思想和其工作機制:連線跟蹤是Netfilter提供的一套基礎框架,不同的協議可以根據其自身協議的特殊性在連線跟蹤機制的指導和約束下來開發本協議的連線跟蹤功能,最後將其交給連線跟蹤機制來統一管理。