今天我們討論一下防火牆的數據包過濾模塊iptable_filter 的設計原理及其實現方式。
內核中將filter 模塊被組織變成一個獨立的模塊,每個這樣獨立的模塊中都有個類似的的init()初始化函數編寫完該函數後,用。宏module_init()宏調用初始化函數;同樣當模塊被卸載時調用宏module_exit()宏將該模塊卸載掉,該那個主要調用模塊的“析構”函數。這當中就牽扯到內核ko 模塊的一些知識,而不是阻礙我們理解。
整個過濾器模塊就一百多行代碼,但要使其其理解清楚還是需要一些功夫。我們首先來看一下過濾器模塊是如何將自己的鉤子函數註冊到netfilter 所分配的幾個鉤點的。
靜態int __init iptable_filter_init(void) { 詮釋 if(forward <0 || forward > NF_MAX_VERDICT){ printk(“ iptables forward必須爲0或1 \ n”); 返回-EINVAL; } / *條目1是FORWARD鉤* / initial_table .entries [1] .target.verdict =-轉發 -1;
/ *註冊表* / ret = ipt_register_table(&packet_filter,&initial_table .repl); 如果(ret <0) 返回ret
/ *註冊鉤* / ret = nf_register_hooks(ipt_ops,ARRAY_SIZE(ipt_ops)); 如果(ret <0) 轉到cleanup_table; 返回ret
cleanup_table: ipt_unregister_table(&packet_filter); 返回ret } |
這裏我只看關鍵部分,根據上面的代碼我們已經知道。濾波器模塊初始化時先調用ipt_register_table 向Netfilter的完成過濾器過濾表的註冊,然後調用ipt_register_hooks 完成自己鉤子函數的註冊,就這麼簡單。至於這兩個註冊的動作分別都做了一件東西,我們接下來詳細探究一下。
註冊過濾表:ipt_register_table(&packet_filter,&initial_table .repl);
Netfilter 在內核中爲防火牆系統維護了一個結構體,該結構體中存儲的是內核中當前可用的所有匹配,target 和table ,它們都是以雙向鏈表的形式被組織起來的。變量static struct xt_af * xt定義在net / netfilter / x_tables.c 其中,其結構爲:
struct xt_af { 結構互斥鎖 struct list_head 匹配;// 每個比賽模塊都會被註冊到這裏 struct list_head 目標;// 每個目標模塊都會被註冊到這裏 struct list_head 表;// 每張表都被註冊到這裏 結構互斥體compat_mutex; }; |
其中xt 變量是在net / netfilter / x_tables.c 文件中的xt_init()函數中被分配存儲空間並完成初始化的,xt 分配的大小以當前內核所能支持的協議簇的數量有關,其代碼如下:
每個註冊一張表,就會根據該表所屬的協議簇,找到其對應的xt []成員,然後在其表的雙向鏈表中掛上該表結構即完成了表的註冊。接下來我們再看一下Netfilter 是如何定義內核中所認識的“表”結構的。
關於表結構,內核中有兩個結構體xt_table {} 和xt_table_info {} 來表示“表”的信息。
struct ipt_table {}的結構體類型定義在中,它主要定義表本身的一些通用的基本信息,如表名稱,所屬的協議簇,所影響的鉤點等等。
struct xt_table // 其中#define ipt_table xt_table { struct list_head list; 字符名稱[XT_TABLE_MAXNAMELEN];// 表的名字 unsigned int valid_hooks; // 該表所檢測的HOOK 點 rwlock_t鎖定;// 讀鎖 void *的私有 ; //描述表的具體屬性,如表的大小,表中的規則數等 struct module * me; // 如果要設計成模塊,則爲THIS_MODULE ;否則爲NULL int af; // 協議簇 ,如PF_INET(或PF_INET) }; |
而每張表中真正和規則相關的信息,則由該結構的的私人屬性來指向。從2.6.18 版內核開始,該變量被改成了void *的類型,目的是方便日後對其進行擴充需要。通常情況下,private 都指向一個xt_table_info {} 類型的結構體變量。
struct xt_table_info {}的結構體類型定義在<include / linux / netfilter / x_tables.h>中。
結構xt_table_info { 無符號整數大小;// 表的大小,即佔用的內存空間 無符號整數 // 表中的規則數 unsigned int initial_entries; // 初始的規則數,用於模塊計數
/ * 記錄所影響的HOOK 的規則入口相對於下面的條目變量的偏移量* / unsigned int hook_entry [NF_IP_NUMHOOKS]; / * 與hook_entry 相對應的規則表上限對齊量,當無規則錄入時,相應的hook_entry 和underflow 發生0 * / 無符號int 下溢 [NF_IP_NUMHOOKS]; char * entries [NR_CPUS]; }; |
我們發現ipt_register_table()函數還有一個輸入參數:initial_table 。根據其名稱不難推斷出它裏面存儲的就是我們用於初始化表的一些原始數據,該變量的結構雖然不復雜,但又引入了幾個其他的數據結構,如下:
靜態 結構 { struct ipt_replace repl; struct ipt_standard 條目[3]; struct ipt_error 術語; } initial_table; |
在註冊過濾表時我們只用到了該結構中的struct ipt_replace repl成員,其他成員我們暫時先不介紹,主要來看一下這個repl 是個神馬東東。
ipt_replace {} 結構體的定義在include / linux / netfilter_ipv4 / ip_tables.h 文件中。其內容如下:
struct ipt_replace { 字符名稱[IPT_TABLE_MAXNAMELEN];// 表的名字 unsigned int valid_hooks; // 所影響的HOOK 點 unsigned int num_entries; // 表中的規則數量 無符號整數大小;// 新規則所佔用的存儲空間的大小
unsigned int hook_entry [NF_IP_NUMHOOKS]; // 進入HOOK 的入口點 無符號整數下溢[NF_IP_NUMHOOKS];/ *下溢點。* /
/ * 這個結構變量ipt_table_info相互在於它還要保存舊的規則信息* / / *計數器數(必須等於當前的條目數)。* / unsigned int num_counters; / *舊條目的計數器。* / struct xt_counters __user * counters;
/ *條目(最末端:實際上不是數組)。* / struct ipt_entry 條目[0]; }; |
之所以要設計ipt_replace {} 這個結構體,是因爲在1.4.0 版的iptables 中有規則替換這個功能,它可以用一個新的規則替換掉指定位置上的已存在的現有規則(關於iptables 命令行工具的詳細用法請參見man 手冊或iptables 指南)。最後我們來看一下initial_table .repl 的長相:
initial_table .repl = {“ filter”,FILTER_VALID_HOOKS,4, sizeof(struct ipt_standard)* 3 + sizeof(struct ipt_error), {[NF_IP_LOCAL_IN] = 0, [NF_IP_FORWARD] = sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard)* 2 }, {[NF_IP_LOCAL_IN] = 0, [NF_IP_FORWARD] = sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard)* 2 }, 0,NULL,{} } ; |
根據上面的初始化代碼,我們就可以弄明白initial_table .repl成員的意思了:
“過濾器” 表從“ FILTER_VALID_HOOKS ” 這些鉤點介入Netfilter的框架,並且過濾器表初始化時有“4”的條規則鏈,每個HOOK 點(對應用戶空間的“規則鏈” )初始化成一條鏈,最後以一條“錯誤的規則”表示結束,filter 表佔(sizeof(struct ipt_standard)* 3 + sizeof(struct ipt_error))字節的存儲空間,每個鉤點的入口規則如代碼所示。因爲初始化模塊時不存在舊的表,因此後面兩個個參數依次爲0 ,NULL 都表示“空”的意思。最後一個柔性堆疊結構ipt_entry 項[0] 中保存了替代的那四條規則。
由此我們可以知道,filter 表初始化時其規則的分佈如下圖所示:
我們繼續往下走。什麼?你說還有個ipt_error?記性真好,不過請享用地無視吧,目前講了也沒用。那你還記得我們現在正在討論的是什麼主題嗎?忘了吧,我再重新一下:我們目前正在討論iptables 內核中的過濾器數據包過濾模塊是如何被註冊到Netfilter 中去的!!
有了上面這些基礎知識我們再分析ipt_register_table(&packet_filter,&initial_table .repl)函數就容易多了,該函數定義在net / ipv4 / netfilter / ip_tables.c 中:
int ipt_register_table(struct xt_table * table,const struct ipt_replace * repl) { 詮釋 struct xt_table_info * newinfo; 靜態結構xt_table_info bootstrap = { 0、0、0,{0},{0},{} }; 無效* loc_cpu_entry; newinfo = xt_alloc_table_info(repl-> size); // 爲filter 表申請存儲空間 如果(!newinfo) 返回-ENOMEM;
// 將過濾器表中的規則入口地址賦值給loc_cpu_entry loc_cpu_entry = newinfo-> entries [raw_smp_processor_id()]; // 將repl 中的所有規則,全部複製到newinfo->條目[] 中 memcpy(loc_cpu_entry,repl-> entries,repl-> size); / * translate_table 函數將由newinfo 所表示的表的各個規則進行邊界檢查,然後針對newinfo所指的xt_talbe_info 結構中的hook_entries 和下溢確定正確的值,最後將表項向其他cpu 拷貝* / ret = translation_table(表->名稱,表-> valid_hooks, newinfo,loc_cpu_entry,repl-> size, repl-> num_entries, repl-> hook_entry, repl->下溢); 如果(ret!= 0){ xt_free_table_info(newinfo); 返回ret } // 這纔是真正註冊我們filter 表的地方 ret = xt_register_table(table,&bootstrap,newinfo); 如果(ret!= 0){ xt_free_table_info(newinfo); 返回ret } 返回0; } |
在該函數中我們發現點有意思的東西:還記得前面我們在定義packet_filter?時是什麼情況不 packet_filter 中沒對其私有成員進行初始化,那麼這個工作自然而然的就留給了xt_register_table()函數來完成,它也定義在x_tables.c 文件中,它主要完成兩件事:
1 ),將由newinfo參數所存儲的表裏面關於規則的基本信息結構體xt_table_info {} 變量賦給由table參數所表示的packet_filter {} 的私有成員變量;
2 ),根據packet_filter 的協議號AF ,將過濾器表掛到變量XT 中表成員變量所表示的雙向鏈表裏。
最後我們回顧一下ipt_register_table(&packet_filter,&initial_table .repl)的初始化流程:
簡而言之ipt_register_table()處理的事情就是從模板initial_table 變量的repl成員裏恢復初始化數據,然後申請一塊內存並用repl 裏的值來初始化它,然後將這塊內存的首地址賦給packet_filter 表的private成員,最後將packet_filter 掛載到xt [2] .tables的雙向鏈表中。
註冊鉤子函數:nf_register_hooks(ipt_ops,ARRAY_SIZE(ipt_ops));
在第二篇博文中我們已經簡單瞭解nf_hook_ops {} 結構了,而且我們也知道該結構在整個Netfilter 框架中的具有相當重要的作用。當我們要向Netfilter 註冊我們自己的鉤子函數時,一般的思路都是去實例化一個nf_hook_ops {} 對象,然後通過nf_register_hook()接口其將其註冊到Netfilter 中即可。當然filter 模塊無外乎也是用這種方式來實現自己的吧,然後接下來我們來研究一下filter 模塊註冊鉤子函數的流程。
首先,我們看到它也實例化了一個nf_hook_ops {} 對象-ipt_ops ,代碼如下所示:
靜態 結構nf_hook_ops ipt_ops [] = { { .hook = ipt_hook, .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_IP_LOCAL_IN, .priority = NF_IP_PRI_FILTER, }, { .hook = ipt_hook, .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_IP_FORWARD, .priority = NF_IP_PRI_FILTER, }, { .hook = ipt_local_out_hook, .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_IP_LOCAL_OUT, .priority = NF_IP_PRI_FILTER, }, }; |
對上面這種定義的代碼我們現在應該已經很清楚其意義了:iptables的的過濾器包過濾模塊在Netfilter的框架的NF_IP_LOCAL_IN 和NF_IP_FORWARD 兩個鉤點以NF_IP_PRI_FILTER(0)優先級註冊了鉤子函數ipt_hook() ,同時在NF_IP_LOCAL_OUT 過濾點也以同樣的優先級註冊了鉤子函數ipt_local_out_hook()。
然後,在nf_register_hooks()函數內部通過循環調用nf_register_hook()接口來完成所有nf_hook_ops {} 對象的註冊任務。在nf_register_hook()函數裏所執行的操作就是一個雙向鏈表的查找和插入,沒啥區別。大家一個問題,測試一下你看博客的認真和專心程度:filter 模塊所定義的這些hook 函數是被註冊到哪裏去了呢?
================================= 華麗麗的分割線============= ====================
想不起的話可以去複習一下第一篇博文結尾部分的內容,不過我知道大多數人都懶的翻回去了。好吧,我再強調一遍:所有的hook 函數最終都被註冊到一個局部的二維的鏈表結構體結構體list_head nf_hooks [NPROTO] [NF_MAX_HOOKS] 裏了。一維表示協議號,二維表示鉤點。
還記得我們給過濾模塊所有hook 函數所劃的分類圖麼:
目前只出現了ipt_hook 和ipt_local_out_hook ,不過這四個函數本質上最後都調用了ipt_do_table()函數,而該函數也是包過濾的核心了。
數據包過濾的原理:
根據前面我們的分析可知,ipt_do_table()函數是最終完成包過濾功能的這一點現在已經非常肯定了,該函數定義在net / ipv4 / netfilter / ip_tables.c 文件中。實際上,90%的包過濾在分析該函數之前,我們把前幾章中所有的相關數據結構再梳理一遍,目的是爲了在分析該函數時達到的函數最終都調用了該接口,它可以說是iptables 包過濾功能的核心部分。心中有數。
我們前面提到過的核心數據結構有initial_table ,ipt_replace ,ipt_table ,ipt_table_info ,ipt_entry ,ipt_standard ,ipt_match ,ipt_entry_match ,ipt_target ,ipt_entry_target ,此處暫時沒有涉及到有關用戶空間的相應數據結構的討論。以上這些數據結構之間的關係如下:
我們還是先看一下ipt_do_table()函數的整體流程圖:
我們分析一下整個ipt_do_table()函數執行的過程:
對某個hook 點註冊的所有鉤子函數,當數據包到達該hook 點後,該鉤子函數便會被激活,從而開始對數據包進行處理。我們說過:規則就是“一組匹配+ 一個動作” ,而一組規則又組成了所謂的“表”,因此,每條規則都屬於唯一的一張表。前面我們知道,每張表都對不同的幾個HOOK 點進行了監聽,而且這些表的優先級是不相同的,我們在用戶空間裏去配置iptables 規則的時候恰恰也是必須指定鏈名和表名,在用戶空間HOOK 點就被抽象爲“鏈”的概念,例如:
iptables –A輸入–p tcp –s!192.168.10.0/24 –j DROP
這就表示我們在過濾器表的NF_IP_LOCAL_IN 這個HOOK 點上增加了一個過濾規則。當數據包到達LOCAL_IN 這個HOOK 點時,那麼它就有機會被註冊在這個點的所有鉤子函數處理,按照註冊時的優先級來。因爲表在註冊時都已確定了優先級,而一個表中可能有數條規則,因此,當數據包到達某個HOOK 點後。優先級最高的表(優先級的值越小表示其優先級達到)中的所有規則被匹配完之後才能輪到下一個次高優先級的表中的所有規則開始匹配(如果數據包還在的話)。
所以,我們在ipt_do_table()中看到,首先就是要獲取表名,因爲表名和優先級在某種意義上是一致的。獲取表之後,緊接着就要獲取表中的規則的起始地址。然後用依次按順序去比較當前正在處理的這個數據包是否和某條規則中的所有過濾項相匹配。如果匹配,就用那條規則裏的動作目標來處理包,完了之後返回;如果不匹配,當該表中所有的規則都被檢查完了之後,該數據包就轉入下一個次高優先級的過濾表中去繼續執行此操作。依次類推,直到最後包被處理或被返回到協議棧中繼續傳輸。
未完,待續...