Netfilter框架爲內核模塊參與IP層數據包處理提供了很大的方便,內核的防火牆模塊(ip_tables)正是通過把自己所編寫的一些鉤子函數註冊到Netfilter所監控的五個關鍵點(NF_IP_PRE_ROUTING,
從上圖我們可以看出,ip_tables模塊它是防火牆的核心模塊,負責維護防火牆的規則表,通過這些規則,實現防火牆的核心功能。歸納起來,主要有三種功能:包過濾(filter)、NAT以及包處理(mangle)。同進該模塊留有與用戶空間通訊的接口。如第一篇博文中Netfilter處於內核中位置那副圖所描述的情形。
在內核中我們習慣將上述的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; }; |
對該結構體中的成員參數做一下解釋:
n list:因爲在一個HOOK點有可能註冊多個鉤子函數,因此這個變量用來將某個HOOK點所註冊的所有鉤子函數組織成一個雙向鏈表;
n hook:該參數是一個指向nf_hookfn類型的函數的指針,由該函數指針所指向的回調函數在該hook被激活時調用【nf_hookfn在後面做解釋】;
n owner:表示這個hook是屬於哪個模塊的
n pf:該hook函數所處理的協議。目前我們主要處理IPv4,所以該參數總是PF_INET;
n hooknum:鉤子函數的掛載點,即HOOK點;
n 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宏僅僅只是一個跳轉而已,更重要的內容是“內核是如何註冊鉤子函數的呢?這些鉤子函數又是如何被調用的呢?誰來維護和管理這些鉤子函數?”
未完,待續…