Netfilter是2.4.x核心引入的,儘管它提供了對2.0.x核心中的ipfw以及2.2.x核心中的ipchains的相容,但實際上它
的工作和意義遠不止於此。從上面對IP報文的流程分析中可以看出,Netfilter和IP報文的處理是完全結合在一起的,同時由於其結構相對獨立,又是
可以完全剝離的。這種機制也是Netfilter-iptables既高效又靈活的保證之一。
在剖析Netfilter機制之前,我們還是由淺入深的從Netfilter的使用開始。
2.1 編譯
在Networking Options中選定Network packet filtering項,並將其下的IP:Netfilter
Configurations小節的所有選項設為Module模式。編譯並安裝新核心,然後重啟,系統的核內Netfilter就配置好了。以下對相關的
核心配置選項稍作解釋,也可以參閱編譯系統內建的Help:
【Kernel/User netlink socket】建立一類PF_NETLINK通訊端族,用於核心與使用者進程通訊。當Netfilter需要使用使用者隊列來管理某些報文時就要使用這一機制;
【Network packet filtering (replaces ipchains)】Netfilter主選項,提供Netfilter架構;
【Network packet filtering debugging】Netfilter主選項的分支,支援更詳細的Netfilter報告;
【IP: Netfilter Configuration】此節下是netfilter的各種選項的集合:
【Connection tracking (required for masq/NAT)】串連跟蹤,用於基於串連的報文處理,比如NAT;
【IP tables support (required for filtering/masq/NAT)】這是Netfilter的架構,NAT等應用的容器;
【ipchains (2.2-style) support】ipchains機制的相容代碼,在新的Netfilter結構上實現了ipchains介面;
【ipfwadm (2.0-style) support】2.0核心防火牆ipfwadm相容代碼,基於新的Netfilter實現。
2.2 總體結構
Netfilter是嵌入核心IP協議棧的一系列調用入口,設定在報文處理的路徑上。網路報文按照來源和去向,可以分為三類:流入的、流經的和流出
的,其中流入和流經的報文需要經過路由才能區分,而流經和流出的報文則需要經過投遞,此外,流經的報文還有一個FORWARD的過程,即從一個NIC轉到
另一個NIC。Netfilter就是根據網路報文的流向,在以下幾個點插入處理過程:
NF_IP_PRE_ROUTING,在報文作路由以前執行;
NF_IP_FORWARD,在報文轉向另一個NIC以前執行;
NF_IP_POST_ROUTING,在報文流出以前執行;
NF_IP_LOCAL_IN,在流入本地的報文作路由以後執行;
NF_IP_LOCAL_OUT,在本地報文做流出路由前執行。
:
圖5 Netfilter HOOK位置
Netfilter架構為多種協議提供了一套類似的鉤子(HOOK),用一個struct list_head
nf_hooks[NPROTO][NF_MAX_HOOKS]二維數組結構儲存,一維為協議族,二維為上面提到的各個調用入口。每個希望嵌入
Netfilter中的模組都可以為多個協議族的多個調用點註冊多個鉤子函數(HOOK),這些鉤子函數將形成一條函數指標鏈,每次協議棧代碼執行到
NF_HOOK()函數時(有多個時機),都會依次啟動所有這些函數,處理參數所指定的協議棧內容。
每個註冊的鉤子函數經過處理後都將返回下列值之一,告知Netfilter核心代碼處理結果,以便對報文採取相應的動作:
NF_ACCEPT:繼續正常的報文處理;
NF_DROP:將報文丟棄;
NF_STOLEN:由鉤子函數處理了該報文,不要再繼續傳送;
NF_QUEUE:將報文入隊,通常交由使用者程式處理;
NF_REPEAT:再次調用該鉤子函數。
2.3 IPTables
Netfilter-iptables由兩部分組成,一部分是Netfilter的"鉤子",另一部分則是知道這些鉤子函數如何工作的一套規則--
這些規則儲存在被稱為iptables的資料結構之中。鉤子函數通過訪問iptables來判斷應該返回什麼值給Netfilter架構。
在現有(kernel 2.4.21)中已內建了三個iptables:filter、nat和mangle,絕大部分報文處理功能都可以通過在這些內建(built-in)的表格中填入規則完成:
filter,該模組的功能是過濾報文,不作任何修改,或者接受,或者拒絕。它在NF_IP_LOCAL_IN、NF_IP_FORWARD和NF_IP_LOCAL_OUT三處註冊了鉤子函數,也就是說,所有報文都將經過filter模組的處理。
nat,網路位址轉譯(Network Address Translation),該模組以Connection
Tracking模組為基礎,僅對每個串連的第一個報文進行匹配和處理,然後交由Connection
Tracking模組將處理結果應用到該串連之後的所有報文。nat在NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING註冊了
鉤子函數,如果需要,還可以在NF_IP_LOCAL_IN和NF_IP_LOCAL_OUT兩處註冊鉤子,提供對本地報文(出/入)的地址轉換。nat
僅對報文頭的地址資訊進行修改,而不修改報文內容,按所修改的部分,nat可分為源NAT(SNAT)和目的NAT(DNAT)兩類,前者修改第一個報文
的源地址部分,而後者則修改第一個報文的目的地址部分。SNAT可用來實現IP偽裝,而DNAT則是透明代理的實現基礎。
mangle,屬於可以進行報文內容修改的IP Tables,可供修改的報文內容包括MARK、TOS、TTL等,mangle表的操作函數嵌入在Netfilter的NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT兩處。
核心編程人員還可以通過注入模組,調用Netfilter的介面函數建立新的iptables。在下面的Netfilter-iptables應用中我們將進一步接觸Netfilter的結構和使用方式。
2.4 Netfilter組態工具
iptables是專門針對2.4.x核心的Netfilter製作的核外組態工具,通過socket介面對Netfilter進行操作,建立socket的方式如下:
socket(TC_AF, SOCK_RAW, IPPROTO_RAW)
其中TC_AF就是AF_INET。核外程式可以通過建立一個"原始IP通訊端"獲得訪問Netfilter的控制代碼,然後通過getsockopt()和setsockopt()系統調用來讀取、更改Netfilter設定,詳情見下。
iptables功能強大,可以對核內的表進行操作,這些操作主要指對其中規則鏈的添加、修改、清除,它的命令列參數主要可分為四類:指定所操作的
IP
Tables(-t);指定對該表所進行的操作(-A、-D等);規則描述和匹配;對iptables命令本身的指令(-n等)。在下面的例子中,我們通
過iptables將訪問10.0.0.1的53連接埠(DNS)的TCP串連引導到192.168.0.1地址上。
iptables -t nat -A PREROUTING -p TCP -i eth0 -d 10.0.0.1 --dport 53 -j DNAT --to-destination 192.168.0.1
由於iptables是操作核內Netfilter的使用者介面,有時也把Netfilter-iptables簡稱為iptables,以便與ipchains、ipfwadm等老版本的防火牆並列。
2.5 iptables核心資料結構
2.5.1 表
在Linux核心裡,iptables用struct ipt_table表示,定義如下(include/linux/netfilter_ipv4/ip_tables.h):
struct ipt_table { struct list_head list; /* 錶鏈 */ char name[IPT_TABLE_MAXNAMELEN]; /* 表名,如"filter"、"nat"等,為了滿足自動模組載入的設計,包含該表的模組應命名為iptable_'name'.o */ struct ipt_replace *table; /* 表模子,初始為initial_table.repl */ unsigned int valid_hooks; /* 位向量,標示本表所影響的HOOK */ rwlock_t lock; /* 讀寫鎖,初始為開啟狀態 */ struct ipt_table_info *private; /* iptable的資料區,見下 */ struct module *me; /* 是否在模組中定義 */ }; struct ipt_table_info是實際描述表的資料結構(net/ipv4/netfilter/ip_tables.c): struct ipt_table_info { unsigned int size; /* 表大小 */ unsigned int number; /* 表中的規則數 */ unsigned int initial_entries; /* 初始的規則數,用於模組計數 */ unsigned int hook_entry[NF_IP_NUMHOOKS]; /* 記錄所影響的HOOK的規則入口相對於下面的entries變數的位移量 */ unsigned int underflow[NF_IP_NUMHOOKS]; /* 與hook_entry相對應的規則表上限位移量,當無規則錄入時,相應的hook_entry和underflow均為0 */ char entries[0] ____cacheline_aligned; /* 規則表入口 */ };
|
例如內建的filter表初始定義如下(net/ipv4/netfilter/iptable_filter.c):
static struct ipt_table packet_filter = { { NULL, NULL },// 鏈表 "filter",// 表名 &initial_table.repl,// 初始的表模板 FILTER_VALID_HOOKS,// 定義為((1 << NF_IP6_LOCAL_IN) | (1 << NF_IP6_FORWARD) | (1 << NF_IP6_LOCAL_OUT)), 即關心INPUT、FORWARD、OUTPUT三點 RW_LOCK_UNLOCKED,// 鎖 NULL,// 初始的表資料為空白 THIS_MODULE // 模組標示 };
|
經過調用ipt_register_table(&packet_filter)後,filter表的private資料區即參照模板填好了。
2.5.2 規則
規則用struct ipt_entry結構表示,包含匹配用的IP頭部分、一個Target和0個或多個Match。由於Match數不定,所以一條規則實際的佔用空間是可變的。結構定義如下(include/linux/netfilter_ipv4):
struct ipt_entry { struct ipt_ip ip; /* 所要匹配的報文的IP頭資訊 */ unsigned int nfcache; /* 位向量,標示本規則關心報文的什麼部分,暫未使用 */ u_int16_t target_offset; /* target區的位移,通常target區位於match區之後,而match區則在ipt_entry的末尾; 初始化為sizeof(struct ipt_entry),即假定沒有match */ u_int16_t next_offset; /* 下一條規則相對於本規則的位移,也即本規則所用空間的總和, 初始化為sizeof(struct ipt_entry)+sizeof(struct ipt_target),即沒有match */ unsigned int comefrom; /* 位向量,標記調用本規則的HOOK號,可用於檢查規則的有效性 */ struct ipt_counters counters; /* 記錄該規則處理過的報文數和報文總位元組數 */ unsigned char elems[0]; /*target或者是match的起始位置 */ }
|
規則按照所關注的HOOK點,被放置在struct ipt_table::private->entries之後的地區,比鄰排列。
2.5.3 規則填寫過程
在瞭解了iptables在核心中的資料結構之後,我們再通過遍曆一次使用者通過iptables配置程式填寫規則的過程,來瞭解這些資料結構是如何工作的了。
一個最簡單的規則可以描述為拒絕所有轉寄報文,用iptables命令表示就是:
iptables -A FORWARD -j DROP;
|
iptables應用程式將命令列輸入轉換為程式可讀的格式(iptables-
standalone.c::main()::do_command(),然後再調用libiptc庫提供的iptc_commit()函數向核心提交該
操作請求。在libiptc/libiptc.c中定義了iptc_commit()(即TC_COMMIT()),它根據請求設定了一個struct
ipt_replace結構,用來描述規則所涉及的表(filter)和HOOK點(FORWARD)等資訊,並在其後附接當前這條規則--一個
struct
ipt_entry結構(實際上也可以是多個規則entry)。組織好這些資料後,iptc_commit()調用setsockopt()系統調用來啟
動核心處理這一請求:
setsockopt( sockfd,//通過socket(TC_AF, SOCK_RAW, IPPROTO_RAW)建立的通訊端,其中TC_AF即AF_INET TC_IPPROTO,//即IPPROTO_IP SO_SET_REPLACE, //即IPT_SO_SET_REPLACE repl,//struct ipt_replace結構 sizeof(*repl) + (*handle)->entries.size)//ipt_replace加上後面的ipt_entry
|
核心對於setsockopt()的處理是從協議棧中一層層傳遞上來的,調用過程如所示:
圖6 規則填寫過程
nf_sockopts是在iptables進行初始化時通過nf_register_sockopt()函數產生的一個struct
nf_sockopt_ops結構,對於ipv4來說,在net/ipv4/netfilter/ip_tables.c中定義了一個
ipt_sockopts變數(struct
nf_sockopt_ops),其中的set操作指定為do_ipt_set_ctl(),因此,當nf_sockopt()調用對應的set操作時,
控制將轉入net/ipv4/netfilter/ip_tables.c::do_ipt_set_ctl()中。
對於IPT_SO_SET_REPLACE命令,do_ipt_set_ctl()調用do_replace()來處理,該函數將使用者層傳入的
struct ipt_replace和struct ipt_entry組織到filter(根據struct
ipt_replace::name項)表的hook_entry[NF_IP_FORWARD]所指向的地區,如果是添加規則,結果將是filter表
的private(struct
ipt_table_info)項的hook_entry[NF_IP_FORWARD]和underflow[NF_IP_FORWARD]的差值擴大
(用於容納該規則),private->number加1。
2.5.4 規則應用過程
以上描述了規則注入核內iptables的過程,這些規則都掛接在各自的表的相應HOOK入口處,當報文流經該HOOK時進行匹配,對於與規則匹配
成功的報文,調用規則對應的Target來處理。仍以轉寄的報文為例,假定filter表中添加了如上所述的規則:拒絕所有轉寄報文。
如1.2節所示,經由本地轉寄的報文經過路由以後將調用ip_forward()來處理,在ip_forward()返回前,將調用如下代碼:
NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2, ip_forward_finish) NF_HOOK是這樣一個宏(include/linux/netfilter.h): #define NF_HOOK(pf, hook, skb, indev, outdev, okfn)/ (list_empty(&nf_hooks[(pf)][(hook)])/ ? (okfn)(skb)/ : nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
|
也就是說,如果nf_hooks[PF_INET][NF_IP_FORWARD]所指向的鏈表為空白(即該鉤子上沒有掛處理函數),則直接調用
ip_forward_finish(skb)完成ip_forward()的操作;否則,則調用net/core
/netfilter.c::nf_hook_slow()轉入Netfilter的處理。
這裡引入了一個nf_hooks鏈表二維數組:
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
|
每一個希望使用Netfilter掛鈎的表都需要將表處理函數在nf_hooks數組的相應鏈表上進行註冊。對於filter表來說,在其初始化
(net/ipv4/netfilter/iptable_filter.c::init())時,調用了net/core
/netfilter.c::nf_register_hook(),將預定義的三個struct
nf_hook_ops結構(分別對應INPUT、FORWARD、OUTPUT鏈)連入鏈表中:
struct nf_hook_ops { struct list_head list; //鏈表 nf_hookfn *hook; //處理函數指標 int pf; //協議號 int hooknum; //HOOK號 int priority; //優先順序,在nf_hooks鏈表中各處理函數按優先順序排序 };
|
對於filter表來說,FORWARD點的hook設定成ipt_hook(),它將直接調用ipt_do_table()。幾乎所有處理函數最
終都將調用ipt_do_table()來查詢表中的規則,以調用對應的target。所示即為在FORWARD點上調用
nf_hook_slow()的過程:
圖7 規則應用流程
2.5.5 Netfilter的結構特點
由上可見,nf_hooks鏈表數組是聯絡報文處理流程和iptables的紐帶,在iptables初始化(各自的init()函數)時,一方面
調用nf_register_table()建立規則容器,另一方面還要調用nf_register_hook()將自己的掛鈎願望表達給
Netfilter架構。初始化完成之後,使用者只需要通過使用者級的iptables命令操作規則容器(添加規則、刪除規則、修改規則等),而對規則的使用
則完全不用操心。如果一個容器內沒有規則,或者nf_hooks上沒有需要表達的願望,則報文處理照常進行,絲毫不受Netfilter-
iptables的影響;即使報文經過了過濾規則的處理,它也會如同平時一樣重新回到報文處理流程上來,因此從宏觀上看,就像在行車過程中去了一趟加油站。