(八)洞悉linux下的Netfilter&iptables:狀態防火牆

基於連接跟蹤機制的狀態防火牆的設計與實現

連接跟蹤本身並沒有實現什麼具體功能,它爲狀態防火牆和NAT提供了基礎框架。前面幾章節我們也看到:從連接跟蹤的職責來看,它只是完成了數據包從“個性”到“共性”抽象的約定,即它的核心工作是如何針對不同協議報文而定義一個通用的“連接”的概念出來,具體的實現由不同協議自身根據其報文特殊性的實際情況來提供。那麼連接跟蹤的主要工作其實可以總結爲:入口處,收到一個數據包後,計算其hash值,然後根據hash值查找連接跟蹤表,如果沒找到連接跟蹤記錄,就爲其創建一個連接跟蹤項;如果找到了,則返回該連接跟蹤項。出口處,根據實際情況決定該數據包是被還給協議棧繼續傳遞還是直接被丟棄。

    我們先看一下iptables指南中關於用戶空間中數據包的四種狀態及其解釋:

狀態

解釋

NEW

NEW說明這個包是我們看到的第一個包。意思就是,這是conntrack模塊看到的某個連接第一個包,它即將被匹配了。比如,我們看到一個SYN包,是我們所留意的連接的第一個包,就要匹配它。第一個包也可能不是SYN包,但它仍會被認爲是NEW狀態。這樣做有時會導致一些問題,但對某些情況是有非常大的幫助的。例如,在我們想恢復某條從其他的防火牆丟失的連接時,或者某個連接已經超時,但實際上並未關閉時。

ESTABLISHED

ESTABLISHED已經注意到兩個方向上的數據傳輸,而且會繼續匹配這個連接的包。處於ESTABLISHED狀態的連接是非常容易理解的。只要發送並接到應答,連接就是ESTABLISHED的了。一個連接要從NEW變爲ESTABLISHED,只需要接到應答包即可,不管這個包是發往防火牆的,還是要由防火牆轉發的。ICMP的錯誤和重定向等信息包也被看作是ESTABLISHED,只要它們是我們所發出的信息的應答。

RELATED

RELATED是個比較麻煩的狀態。當一個連接和某個已處於ESTABLISHED狀態的連接有關係時,就被認爲是RELATE的了。換句話說,一個連接要想是RELATED的,首先要有一個ESTABLISHED的連接。這個ESTABLISHED連接再產生一個主連接之外的連接,這個新的連接就是RELATED的了,當然前提是conntrack模塊要能理解RELATED。ftp是個很好的例子,FTP-data 連接就是和FTP-control有RELATED的。還有其他的例子,比如,通過IRC的DCC連接。有了這個狀態,ICMP應答、FTP傳輸、DCC等才能穿過防火牆正常工作。注意,大部分還有一些UDP協議都依賴這個機制。這些協議是很複雜的,它們把連接信息放在數據包裏,並且要求這些信息能被正確理解。

INVALID

INVALID說明數據包不能被識別屬於哪個連接或沒有任何狀態。有幾個原因可以產生這種情況,比如,內存溢出,收到不知屬於哪個連接的ICMP 錯誤信息。一般地,我們DROP這個狀態的任何東西。

    認真體會這個表格所表達意思對我們理解狀態防火牆的機制和實現有很大的幫助。我們以最常見的TCP、UDP和ICMP協議爲例來分析,因爲他們最常見。對於TCP/UDP來說,我們可以用“源/目的IP+源/目的端口”唯一的標識一條連接;因爲ICMP沒有端口的概念,因此對ICMP而言,其“連接”的表示方法爲“源/目的IP+類型+代碼+ID”。因此,你就可以明白,如果你有一種不同於目前所有協議的新協議要爲其開發連接跟蹤功能,那麼你必須定以一個可以唯一標識該報文的規格,這是必須的。

    接下來我就拋磚引玉,分析一下NEW、ESTABLISHED、RELATED和INVALID幾種狀態內核中的變遷過程。

 

    針對於NEW狀態的理解:

依舊在ip_conntrack_in()函數中,只不過我們這次的側重點不同。由於該報文是某條連接的第一個數據包,ip_conntrack_find_get()函數中根據該數據包的tuple在連接跟蹤表ip_conntrack_hash中肯定找不到對應的連接跟蹤記錄,然後重任就交給了init_conntrack()函數:

如果連接跟蹤數已滿,或沒有足夠的內存時,均會返回錯誤。否則,將新連接跟蹤記錄的引用計數置爲1,設置連接跟蹤記錄“初始”和“應答”方向的tuple鏈,同時還設置了連接跟蹤記錄被銷燬和超時的回調處理函數destroy_conntrack()和death_by_timeout()等。

    至此,我們新的連接記錄ip_conntrack{}就華麗麗滴誕生了。每種協議必須對其“新連接記錄項”提供一個名爲new()的回調函數。該函數的主要作用就是針對不同協議,什麼樣的報文才被稱爲“new”狀態必須由每種協議自身去考慮和實現。具體我就不深入分析了,大家只要知道這裏有這麼一齣戲就可以了,感興趣的朋友可以去研究研究。當然,這需要對協議字段和意義有比較透徹清晰的瞭解才能完全弄明白別人爲什麼要那麼設計。畢竟我們不是去爲TCP、UDP或ICMP開發連接跟蹤,開源界的信條就是“永遠不要重複發明車輪”。如果你想深入研究現有的東西,目的只有一個:那就是學習別人的優點和長處,要有重點,有主次的去學習,不然會讓自己很累不說,還會打擊求知的積極性和動力。

閒話不都說,我們繼續往下分析。如果該數據包所屬的協議集提供了helper接口,那麼將其掛到conntrack->helper的回調接口上。最後,將該連接跟蹤項“初始”方向的tuple鏈添加到一條名爲unconfirmed的全局鏈表中,該鏈表裏存儲的都是截止到目前爲止還未曾收到“應答”方向數據包的連接跟蹤記錄。

費了老半天勁兒,狀態防火牆終於出來和大家見面了:

在ip_ct_get_tuple()函數裏初始化時tuple.dst.dir就被設置爲了IP_CT_DIR_ORIGINAL,因爲我們討論的就是NEW狀態的連接,tuple.dst.dir字段到目前爲止還未被改變過,與此同時,ip_conntrack.status位圖自從被創建之日起經過memset()操作後就一直爲全0狀態,纔有最後的skb->nfctinfo=*ctinfo=IP_CT_NEW和skb->nfct = &ip_conntrack->ct_general。

繼續回到ip_conntrack_in()函數裏,此時調用協議所提供的回調packet()函數。在博文六中我們曾提及過,該函數承擔着數據包生死存亡的使命。這裏我們有必要注意一下packet()函數最後給Netfilter框架返回值的一些細節:

-1,其實就是-NF_ACCEPT,意思是:連接跟蹤出錯了,該數據包不是有效連接的一部分,Netfilter不要再對這類報文做跟蹤了,調用前面的回調函數destroy()清除已經爲其設置的連接跟蹤項記錄項,釋放資源。最後向Netfilter框架返回ACCEPT,讓該數據包繼續傳輸。

0,就是NF_DROP,返回給Netfilter框架的也是該值,那麼這數據包就掛在這裏了。

1,就是NF_ACCEPT,同樣,該數據包已經被正確跟蹤了,通知Netfilter框架繼續傳輸該數據包。

對於像TCP這樣非常複雜的協議纔用到了NF_DROP操作,像UDP、ICMP、GRE、SCTP等協議都沒有到,但不排除你的項目中使用NF_DROP的情形。

在連接跟蹤的出口處的ip_conntrack_confirm()函數中,如果已經爲該數據包skb創建了連接跟蹤記錄ip_conntrack{}(即skb->nfct有值),則做如下處理:

如果該連接還沒有收到回覆報文----明顯如此;

如果該連接沒有掛掉----毫無疑問。

因爲是新連接,因此在全局鏈表數組ip_conntrack_hash[]就沒有記錄該連接“初始”和“應答”方向的tuplehash鏈。然後,緊接着我把該連接初始方向的tuplehash鏈ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list從unconfirmed鏈表上卸下來。並且,將該連接初始和應答方向的tuplehash鏈表根據其各自的hash之加入到ip_conntrack_hash[]裏,最後啓動連接跟蹤老化時間定時器,修改引用計數,將ip_conntrack.status狀態位圖更新爲IPS_CONFIRMED_BIT,並向Netfilter框架返回NF_ACCEPT。數據包離開Netfilter繼續在協議棧中傳遞。
 

針對於ESTABLISHED狀態的理解:

    每個ip_conntrack{}結構末尾有兩條tuplehash鏈,分別代表“初始”和“應答”方向的數據流向,如下圖所示:

    如果一條連接進入ESTABLISHED,那麼的前一狀態一定是NEW。因此,我們繼續前面的分析分析過程,當我們的連接跟蹤記錄收到了其對應的響應報文後的處理流程。注意前面剛分析過的:對於新連接的狀態位圖status已經被設置成IPS_CONFIRMED_BIT了。

    繼續在ip_conntrack_in()函數,所以數據包從skb到tuple的生成過程中,初始化時都有tuple->dst.dir = IP_CT_DIR_ORIGINAL;那麼tuple->dst.dir是何時被改變狀態的呢?這就牽扯到一種很重要的通信機制netlink。連接跟蹤框架還爲連接記錄的躍遷改變定義了一些事件處理和通知機制,而這目前不是本文的重點。在入口處,雖然從連接跟蹤表中找到了該tuple所屬的連接跟蹤記錄項,但在過濾表中該報文有可能會被丟棄,因此不應該急於改變回應報文所屬的連接跟蹤記錄的狀態,迴應報文也有skb->nfctinfo = *ctinfo=IP_CT_NEW,該數據包所屬的連接跟蹤記錄保存在skb->nfct裏。在出口處,函數ip_conntrack_confirm()中,由於在NEW狀態時,連接跟蹤記錄項中status=IPS_CONFIRMED_BIT了,因此這裏對於響應報文,不會重複執行函數__ip_conntrack_confirm()。

緊接着,當“迴應”報文被連接跟蹤框架看到後,它會調用ip_ct_deliver_cached_events()函數,以某種具體的事件通過netlink機制來通知ip_conntrack_netlink.c文件中的ctnetlink_parse_tuple()函數將初始方向的tuple->dst.dir = IP_CT_DIR_REPLY。這裏理解起來稍微有點抽象,不過還是提醒大家抓重點思路,我們後面這幾章內容相對來說比前面幾章要稍微複雜些,這也是能力提升必須要經歷的過程。

    至此,該連接跟蹤記錄ip_conntrack{}的數據我們來欣賞一下:

    如果該連接後續一個初始方向的數據包又到達了,那麼在resolve_normal_ct()函數中,便會執行設置skb->nfctinfo=*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY和set_reply=1,然後退出到ip_conntrack_in()裏,將上圖中ip_conntrack{}結構體裏的status成員屬性由原來的IPS_CONFIRMED_BIT設置爲IPS_SEEN_REPLY_BIT,並通過函數ip_conntrack_event_cache()觸發一個netlink狀態改變事件。最後,在ip_conntrack_confirm()裏也只出發netlink事件而已。緊接着,第二個應答方向的報文也到達了,和上面的處理動作一樣。

 

    針對於RELATED狀態的理解:

    很多文章都從FTP協議的角度來剖析這個狀態,確實FTP也是最能體現RELATED特性的協議。假如有個報文屬於某條已經處於ESTABLISHED狀態的連接,我們來看看狀態防火牆是如何來識別這種情況。

    依然在resolve_normal_ct()函數中,執行到init_conntrack()裏面時,通過數據包相對應的tuple即可在全局鏈表ip_conntrack_expect_list裏找到該連接所屬的主連接。然後將我們這條RELATED連接記錄的status= IPS_EXPECTED_BIT,並建立我們RELATED連接和它所屬的主連接之間的對應關係conntrack->master = exp->master,同樣將其掛載到unconfirmed鏈裏。返回到resolve_normal_ct()裏,爲數據包設置狀態值skb->nfctinfo = *ctinfoIP_CT_RELATED。當數據包將要離開時,在ip_conntrack_confirm()函數中也會將其加入連接跟蹤表,並設置status爲IPS_CONFIRMED_BIT。剩下的流程就和前面我們討論的是一樣了,唯一卻別的地方在於屬於RELATED的連接跟蹤,其master指向了它所屬的主連接跟蹤記錄項。

    INVALID狀態壓根兒就沒找着,汗。跟俺玩躲貓貓,哥還不鳥你捏。。。

    本篇的知識點相對來說總體來說比較抽象,內容比較多,實現上也較爲複雜,我在省略了其狀態躍遷流程情況下都還寫了這麼多東西,很多地方研究的其實都不是很深入,只能感慨Netfilter的博大。

    未完,待續…

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