今天我們讨論一下防火牆的資料包過濾子產品iptable_filter的設計原理及其實作方式。
核心中将filter子產品被組織成了一個獨立的子產品<net/ipv4/netfilter/iptable_filter.c>,每個這樣獨立的子產品中都有個類似的init()初始化函數。編寫完該函數後,用module_init()宏調用初始化函數;同樣當子產品被解除安裝時調用module_exit()宏将該子產品解除安裝掉,該宏主要調用子產品的“析構”函數。這當中就牽扯到核心ko子產品的一些知識,但這并不妨礙我們了解。
整個filter子產品就一百多行代碼,但要将其了解清楚還是需要一些功夫。我們首先來看一下filter子產品是如何将自己的鈎子函數注冊到netfilter所管轄的幾個hook點的。
static int __init iptable_filter_init(void) { int ret; if (forward < 0 || forward > NF_MAX_VERDICT) { printk("iptables forward must be 0 or 1n"); return -EINVAL; } initial_table.entries[1].target.verdict = -forward - 1; ret = ipt_register_table(&packet_filter, &initial_table.repl); if (ret < 0) return ret; ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops)); if (ret < 0) goto cleanup_table; return ret; cleanup_table: ipt_unregister_table(&packet_filter); return ret; } |
這裡我隻看關鍵部分,根據上面的代碼我們已經知道。filter子產品初始化時先調用ipt_register_table向Netfilter完成filter過濾表的注冊,然後調用ipt_register_hooks完成自己鈎子函數的注冊,就這麼簡單。至于這兩個注冊的動作分别都做了哪些東西,我們接下來詳細探究一下。
注冊過濾表:ipt_register_table(&packet_filter, &initial_table.repl);
Netfilter在核心中為防火牆系統維護了一個結構體,該結構體中存儲的是核心中目前可用的所有match,target和table,它們都是以雙向連結清單的形式被組織起來的。這個全局的結構體變量static struct xt_af *xt定義在net/netfilter/x_tables.c當中,其結構為:
struct xt_af { struct mutex mutex; struct list_head match; //每個match子產品都會被注冊到這裡 struct list_head target; //每個target子產品都會被注冊到這裡 struct list_head tables; //每張表都被被注冊到這裡 struct mutex compat_mutex; }; |
其中xt變量是在net/netfilter/x_tables.c檔案中的xt_init()函數中被配置設定存儲空間并完成初始化的,xt配置設定的大小以目前核心所能支援的協定簇的數量有關,其代碼如下:
初始化完成後 xt 的結構圖如下所示,這裡我們隻以 IPv4 協定為例加以說明:
每注冊一張表,就會根據該表所屬的協定簇,找到其對應的xt[]成員,然後在其tables雙向連結清單中挂上該表結構即完成了表的注冊。接下來我們再看一下Netfilter是如何定義核心中所認識的“表”結構的。
關于表結構,核心中有兩個結構體xt_table{}和xt_table_info{}來表示“表”的資訊。
struct ipt_table{}的結構體類型定義在<include/linux/netfilter/x_tables.h>中,它主要定義表自身的一些通用的基本資訊,如表名稱,所屬的協定簇,所影響的hook點等等。
struct xt_table //其中#define ipt_table xt_table { struct list_head list; char name[XT_TABLE_MAXNAMELEN]; //表的名字 unsigned int valid_hooks; //該表所檢測的HOOK點 rwlock_t lock; //讀寫鎖 void *private; //描述表的具體屬性,如表的size,表中的規則數等 struct module *me; //如果要設計成子產品,則為THIS_MODULE;否則為NULL int af; //協定簇 ,如PF_INET(或PF_INET) }; |
而每張表中真正和規則相關的資訊,則由該結構的的private屬性來指向。從2.6.18版核心開始,該變量被改成了void*類型,目的是友善日後對其進行擴充需要。通常情況下,private都指向一個xt_table_info{}類型的結構體變量。
struct xt_table_info{}的結構體類型定義在< include/linux/netfilter/x_tables.h >中。
struct xt_table_info { unsigned int size; //表的大小,即占用的記憶體空間 unsigned int number; //表中的規則數 unsigned int initial_entries; //初始的規則數,用于子產品計數 unsigned int hook_entry[NF_IP_NUMHOOKS]; unsigned int underflow[NF_IP_NUMHOOKS]; char *entries[NR_CPUS]; }; |
該結構描述了表中規則的一些基本資訊,同時在該結構的末尾訓示了該表中所有規則的入口點,即表中的第一條規則。記住:所有的規則是順序依次存放的,參見博文三。 回到前面注冊過濾表的地方: ipt_register_table (&packet_filter, &initial_table.repl); 給它傳遞的第一個參數 packet_filter 就是我們的 filter 表的自身一些資訊,僅此而已。
我們發現ipt_register_table()函數還有一個輸入參數:initial_table。根據其名稱不難推斷出它裡面存儲的就是我們用于初始化表的一些原始資料,該變量的結構雖然不複雜,但又引入了幾個其他的資料結構,如下:
static struct { struct ipt_replace repl; struct ipt_standard entries[3]; struct ipt_error term; } initial_table; |
在注冊過濾表時我們隻用到了該結構中的struct ipt_replace repl成員,其他成員我們暫時先不介紹,主要來看一下這個repl是個神馬東東。
ipt_replace{}結構體的定義在include/linux/netfilter_ipv4/ip_tables.h檔案中。其内容如下:
struct ipt_replace { char name[IPT_TABLE_MAXNAMELEN]; //表的名字 unsigned int valid_hooks; //所影響的HOOK點 unsigned int num_entries; //表中的規則數目 unsigned int size; //新規則所占用存儲空間的大小 unsigned int hook_entry[NF_IP_NUMHOOKS]; //進入HOOK的入口點 unsigned int underflow[NF_IP_NUMHOOKS]; unsigned int num_counters; struct xt_counters __user *counters; struct ipt_entry entries[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"表從"FILTER_VALID_HOOKS"這些hook點介入Netfilter架構,并且filter表初始化時有"4"條規則鍊,每個HOOK點(對應使用者空間的“規則鍊”)初始化成一條鍊,最後以一條“錯誤的規則”表示結束,filter表占(sizeof(struct ipt_standard) * 3+sizeof(struct ipt_error))位元組的存儲空間,每個hook點的入口規則如代碼所示。 因為初始化子產品時不存在舊的表,是以後面兩個個參數依次為0、NULL都表示“空”的意思。最後一個柔性數組struct ipt_entry entries[0]中儲存了預設的那四條規則。
由此我們可以知道,filter表初始化時其規則的分布如下圖所示:
我們繼續往下走。什麼?你說還有個ipt_error?記性真好,不過請盡情地無視吧,目前講了也沒用。那你還記得我們現在正在讨論的是什麼主題嗎?忘了吧,我再重申一下:我們目前正在讨論iptables核心中的filter資料包過濾子產品是如何被注冊到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) { int ret; struct xt_table_info *newinfo; static struct xt_table_info bootstrap = { 0, 0, 0, { 0 }, { 0 }, { } }; void *loc_cpu_entry; newinfo = xt_alloc_table_info(repl->size); //為filter表申請存儲空間 if (!newinfo) return -ENOMEM; //将filter表中的規則入口位址指派給loc_cpu_entry loc_cpu_entry = newinfo->entries[raw_smp_processor_id()]; //将repl中的所有規則,全部拷貝到newinfo->entries[]中 memcpy(loc_cpu_entry, repl->entries, repl->size); ret = translate_table(table->name, table->valid_hooks, newinfo, loc_cpu_entry, repl->size, repl->num_entries, repl->hook_entry, repl->underflow); if (ret != 0) { xt_free_table_info(newinfo); return ret; } //這才是真正注冊我們filter表的地方 ret = xt_register_table(table, &bootstrap, newinfo); if (ret != 0) { xt_free_table_info(newinfo); return ret; } return 0; } |
在該函數中我們發現點有意思的東西:還記得前面我們在定義packet_filter時是什麼情況不? packet_filter中沒對其private成員進行初始化,那麼這個工作自然而然的就留給了xt_register_table()函數來完成,它也定義在x_tables.c檔案中,它主要完成兩件事:
1)、将由newinfo參數所存儲的表裡面關于規則的基本資訊結構體xt_table_info{}變量賦給由table參數所表示的packet_filter{}的private成員變量;
2)、根據packet_filter的協定号af,将filter表挂到變量xt中tables成員變量所表示的雙向連結清單裡。
最後我們回顧一下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,代碼如下所示:
static struct 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的filter包過濾子產品在Netfilter架構的NF_IP_LOCAL_IN和NF_IP_FORWARD兩個hook點以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函數最終都被注冊到一個全局的二維的連結清單結構體數組struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS]裡了。一維表示協定号,二維表示hook點。
還記得我們給過濾子產品所有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 INPUT –p tcp –s ! 192.168.10.0/24 –j DROP
這就表示我們在filter表的NF_IP_LOCAL_IN這個HOOK點上增加了一個過濾規則。當資料包到達LOCAL_IN這個HOOK點時,那麼它就有機會被注冊在這個點的所有鈎子函數處理,按照注冊時候的優先級來。因為表在注冊時都已确定了優先級,而一個表中可能有數條規則,是以,當資料包到達某個HOOK點後。優先級最高的表(優先級的值越小表示其優先程度越高)中的所有規則被比對完之後才能輪到下一個次高優先級的表中的所有規則開始比對(如果資料包還在的話)。
是以,我們在ipt_do_table()中看到,首先就是要擷取表名,因為表名和優先級在某種程度上來說是一緻的。擷取表之後,緊接着就要擷取表中的規則的起始位址。然後用依次按順序去比較目前正在處理的這個資料包是否和某條規則中的所有過濾項相比對。如果比對,就用那條規則裡的動作target來處理包,完了之後傳回;如果不比對,當該表中所有的規則都被檢查完了之後,該資料包就轉入下一個次高優先級的過濾表中去繼續執行此操作。依次類推,直到最後包被處理或者被傳回到協定棧中繼續傳輸。
未完,待續…