(二)洞悉linux下的Netfilter&iptables:核心中的ip_tables小覷
Netfilter框架為核心模組參與IP層資料包處理提供了很大的方便,核心的防火牆模組(ip_tables)正是通過把自己所編寫的一些鉤子函式註冊到Netfilter所監控的五個關鍵點(NF_IP_PRE_ROUTING,
NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING)這種方式介入到對資料包的處理。這些鉤子函式功能非常強大,按功能可分為四大類:連線跟蹤、資料包的過濾、網路地址轉換(NAT)和資料包的修改。它們之間的關係,以及和Netfilter、ip_tables難捨難分的纏綿可以用下圖來表示:
從上圖我們可以看出,ip_tables模組它是防火牆的核心模組,負責維護防火牆的規則表,通過這些規則,實現防火牆的核心功能。歸納起來,主要有三種功能:包過濾(filter)、NAT以及包處理(mangle)。同進該模組留有與使用者空間通訊的介面
在核心中我們習慣將上述的filter,nat和mangle等稱之為模組。連線跟蹤conntrack有些特殊,它是NAT模組和狀態防火牆的功能基礎,其實現機制我們也會在後面詳細分析的。
OK,回到開篇的問題,我們來看一下基於Netfilter的防火牆系統到底定義了哪些鉤子函式?而這些鉤子函式都是分別掛載在哪些hook點的?按照其功能結構劃分,我將這些hook函式總結如下:
包過濾子功能:包過濾一共定義了四個hook函式,這四個hook函式本質最後都呼叫了ipt_do_table()函式。
網路地址轉換子功能:該模組也定義了四個hook函式,其中有三個最終也都呼叫了ip_nat_fn()函式,ip_nat_adjust()有自己另外的功能。
連線跟蹤子功能:這裡連線跟蹤應該稱其為一個子系統更合適些。它也定義四個hook函式,其中ip_conntrack_local()最後其實也呼叫了ip_conntrack_in()函式。
以上便是Linux的防火牆---iptables在核心中定義的所有hook函式。接下來我們再梳理一下這些hook函式分別是被掛載在哪些hook點上的。還是先貼個三維框圖,因為我覺得這個圖是理解Netfilter核心機制最有效,最直觀的方式了,所以屢用不爽!
然後,我們拿一把大刀,從協議棧的IPv4點上順著hook點延伸的方向一刀切下去,就會得到一個平面,如上圖所示。前面這些hook函式在這個平面上的分佈情況如下所示:
這幅圖徹底暴露了ip_tables核心模組中那些hook函式在各個hook點分佈情況。與此同時,這個圖還告訴了我們很多資訊:所有由網絡卡收上來的資料包率先被ip_conntrack_defrag處理;連結跟蹤系統的入口函式以-200的優先順序被註冊到了PRE_ROUTING和LOCAL_OUT兩個hook點上,且其優先順序高於mangle操作,NAT和包過濾等其他模組;DNAT可以在PRE_ROUTING和LOCAL_OUT兩個hook點來做,SNAT可以在LOCAL_IN和POST_ROUTING兩個hook點上。如果你認真研究會發現這個圖確實很有用。因為當初為了畫這個圖我可是兩個晚上沒睡好覺啊,畫出來後還要驗證自己的想法,就得一步一步給那些關鍵的hook點和hook函式分別加上除錯列印資訊,重新編譯核心然後確認這些hook函式確實是按照我所分析的那樣被呼叫的。因為對學術嚴謹就是對自己負責,一直以來我也都這麼堅信的。“沒有調查就沒發言權”;在我們IT行業,“沒有親自動手做過就更沒有發言權”。又扯遠了,趕緊收回來。
框架的東西多看些之上從巨集觀上可以使我們對整個系統的架構和設計有個比較全面的把握,接下來在分析每個細節的時候才會做到心中有數,不至於“盲人摸象”的境地。在本章即將結束之際,我們來看點程式碼級的東西。我保證只是個簡單的入門瞭解,因為重頭戲我打算放到後面,大家也知道分析程式碼其實是最頭疼的,關鍵還是看自己的心態。
Netfilter的實現方式:
第一篇我們講了Netfilter的原理,這裡我們談談其實現機制的問題。
我們回頭分析一下那個用於儲存不同協議簇在每個hook點上所註冊的hook函式鏈的二維陣列 nf_hooks[][],其型別為list_head:
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
list_head{}結構體定義在include/linux/list.h標頭檔案中
struct list_head {
struct list_head *next, *prev;
};
這是Linux核心中處理雙向連結串列的標準方式。當某種型別的資料結構需要被組織成雙向連結串列時,會在該資料結構的第一個欄位放置一個list_head{}型別的成員。在後面的使用過程中可以通過強制型別轉換來實現雙向連結串列的遍歷操作。
在Netfilter中一個非常重要的資料結構是nf_hook_ops{} :
struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; struct module *owner; int pf; int hooknum; /* Hooks are ordered in ascending priority. */ int priority; }; |
對該結構體中的成員引數做一下解釋:
- list:因為在一個HOOK點有可能註冊多個鉤子函式,因此這個變數用來將某個HOOK點所註冊的所有鉤子函式組織成一個雙向連結串列;
- hook:該引數是一個指向nf_hookfn型別的函式的指標,由該函式指標所指向的回撥函式在該hook被啟用時呼叫【nf_hookfn在後面做解釋】;
- owner:表示這個hook是屬於哪個模組的
- pf:該hook函式所處理的協議。目前我們主要處理IPv4,所以該引數總是PF_INET;
- hooknum:鉤子函式的掛載點,即HOOK點;
- priority:優先順序。前面也說過,一個HOOK點可能掛載了多個鉤子函式,當Netfilter在這些HOOK點上遍歷查詢所註冊的鉤子函式時,這些鉤子函式的先後執行順序便由該引數來制定。
nf_hookfn所定義的回撥函式的原型在include/linux/netfilter.h檔案中:
typedef unsigned int nf_hookfn(unsigned int hooknum, //HOOK點 struct sk_buff **skb, //不解釋 const struct net_device *in, //資料包的網路如介面 const struct net_device *out, //資料包的網路出介面 int (*okfn)(struct sk_buff *)); //後續的處理函式 |
我們可以到,上面這五個引數最後將由NF_HOOK或NF_HOOK_COND巨集傳遞到Netfilter框架中去。
如果要增加新的鉤子函式到Netfilter中相應的過濾點,我們要做的工作其實很簡單:
1)、編寫自己的鉤子函式;
2)、例項化一個struct nf_hook_ops{}結構,並對其進行適當的填充,第一個引數list並不是使用者所關心的,初始化時必須設定成{NULL,NULL};
3)、用nf_register_hook()函式將我們剛剛填充的nf_hook_ops結構體註冊到相應的HOOK點上,即nf_hooks[prot][hooknum]。
這也是最原生的擴充套件方式。有了上面這個對nf_hook_ops{}及其用法的分析,後面我們再分析其他模組,如filter模組、nat模組時就會不那麼難懂了。
核心在網路協議棧的關鍵點引入NF_HOOK巨集,從而搭建起了整個Netfilter框架。但是NF_HOOK巨集僅僅只是一個跳轉而已,更重要的內容是“核心是如何註冊鉤子函式的呢?這些鉤子函式又是如何被呼叫的呢?誰來維護和管理這些鉤子函式?”