(三)洞悉linux下的Netfilter&iptables:核心中的rule,match和target
作為ipchains的後繼者,iptables具有更加優越的特性,良好的可擴充套件功能、更高的安全性以及更加緊湊、工整、規範的程式碼風格。
在2.6的核心中預設維護了三張表(其實是四張,還有一個名為raw的表很少被用到,這裡不對其進行分析介紹了):filter過濾表,nat地址轉換表和mangle資料包修改表,每張表各司其職。
我們對這三張表做一下簡要說明:
1)、filter表
該表是整個過濾系統中真正起“過濾”作用的地方。所有對資料包的過濾工作都在這個表裡進行,也就是說使用者如果需要對某種型別的資料包進行過濾攔截,那麼最好在這個表中進行操作。filter表會在NF_IP_LOCAL_IN、NF_IP_FORWARD和NF_IP_LOCAL_OUT三個hook點註冊鉤子函式,也就是說所有配置到filer表中的規則只可能在這三個過濾點上進行設定。
2)、nat表
主要用於DNAT和SNAT和地址偽裝等操作。用於修改資料包的源、目的地址。目前版本的核心中nat表監視四個hook點:NF_IP_PRE_ROUTING、NF_IP_LOCAL_IN/OUT、NF_IP_POST_ROUTING。但在真正的實際應用中,我們一般僅需要在nat表的PREROUTING和POSTROUTING點上註冊鉤子函式。該表有個特性:只有新連線的第一個資料包會經過這個表,隨後該連線的所有資料包將按照第一個資料包的處理動作做同樣的操作,這種特性是由連線跟蹤機制來實現的。
3)、mangle表
該表主要用於對資料包的修改,諸如修改資料包的TOS、TTL等欄位。同時該表還會對資料包打上一些特殊的標籤以便結合TC等工具,實現諸如Qos等功能。該表監視所有的hook點。
IT界有位大牛(具體是哪個我記不太清楚了)曾給程式下的定義是:程式=資料結構+演算法。可見資料結構在整個程式設計過程中的重要性了。我本人也比較贊同這種說法。我們今天主要探究一下通過使用者空間的iptables所配置到核心中的每條規則到底是個啥樣子。
在 Netfilter 中規則是順序儲存的,一條rule規則主要包括三個部分:
- ipt_entry:標準匹配結構,主要包含資料包的源、目的IP,出、入介面和掩碼等;
- ipt_entry_match:擴充套件匹配。一條rule規則可能有零個或多個ipt_entry_match結構;
- ipt_entry_target:一條rule規則有且僅有一個target動作。就是當所有的標準匹配和擴充套件匹配都符合之後才來執行該target。
結構體struct ipt_entry{}的定義在include/linux/netfilter_ipv4/ip_tables.h檔案裡。其結構圖如下:
上面這幾個結構體的成員屬性基本上已經做到了“見名知意”,而且核心原始碼也對它們做了充分的註解。這裡只對最後一個屬性elem做一下說明,其定義為unsigned char elems[0]。大家可能覺得有些奇怪,怎麼定了一個大小為零的陣列呢?而且有些面試官曾經就這樣的定義還向面試者發問過呢。這種方式定義的陣列叫柔性陣列,又叫可變長陣列。為了不至於沖淡本文主題,這裡給出一個關於柔性陣列的連結,這位大牛已經將的很清楚了,大家可以去拜讀拜讀:http://blog.csdn.net/supermegaboy/article/details/4854939。更多詳細的內容可以去研讀C99標準。
我們將看到核心中大量的在運用柔性陣列,包括我們即將要介紹的這兩個結構體:
這兩個雙胞胎兄弟一眼望去還以為它們是同一個東西,但事實並非如你所想的那樣。其中ipt_entry_match{}表示防火牆規則的匹配部分;ipt_entry_target{}表示防火牆規則的動作處理部分。大家先忽略掉它們左邊那條煩人的提示部分union,後面我會詳細介紹的,現在請跟我一樣盡情地無視它們吧。
這裡我們還注意到,這兩個傢伙分別都拖了一條小尾巴:ipt_match{}和ipt_target{}。對於前面我們提到過的標準匹配,它只會去檢查資料包的IP地址,源目的介面,掩碼等通用資訊,不會用到ipt_entry_match{}。對於以模組形式存在的擴充套件匹配,如iprange模組,ipp2p模組等,它們就得實現自己的ipt_match{}結構。也就是說,如果你要開發一個新的match模組,那麼就必須去例項化一個ipt_match{}結構體物件,並實現該結構體中相應的成員屬性(其實主要都是一些些函式指標成員,你必須實現相應的函式體),然後將該ipt_match{}物件掛在你的ipt_entry_match{}結構的match屬性裡就OK了,就這麼簡單。
同樣的,我們來說一下ipt_target{}結構體。對於target(我這裡就不翻譯了,那個叫“動作”的翻譯太難聽了,後面我都用英文表述)也分為標準target和擴充套件target。標準target就是那些ACCEPT、DROP、REJECT等等之類的處理方式;擴充套件target就是那些諸如DNAT、SNAT等以模組形式存在的target了。對於標準的target,它是不需要ipt_target{}結構的,即ipt_entry_target{}中的target屬性為NULL;而對於我們自己擴充套件target是需要我們自己手工去實現ipt_target{}物件,並完成相關回調函式的編寫。對於ipt_target{}結構體中target回撥函式的編寫有一點要注意:該函式必須向Netfilter框架返回IPT_CONTINUE、或者諸如NF_ACCEPT、NF_DROP之類的值。開發細節我會在後續動手實踐章節一一向大家說明。
結構體ipt_entry_match{}定義在include/linux/netfilter/x_tables.h檔案中。
結構體ipt_entry_target{}也定義在include/linux/netfilter/x_tables.h檔案中。
結構體ipt_match{}定義在include/linux/netfilter/ip_tables.h檔案中。
結構體ipt_target{}也定義在include/linux/netfilter/ip_tables.h檔案中。
匹配match:
上面我們說過,match分為兩種:基本match,又叫標準match;擴充套件match。
n 標準match:
標準匹配主要用於匹配由struct ipt_ip{}所定義的資料包的特徵項。標準匹配的核心資料結構就是我們上面所看到的ipt_match{}定義在include/linux/netfilter/ip_tables.h。在所有的表中我們最後真正所用到的match結構為ipt_entry_match{},它和ipt_match{}的關係我也將其畫出來了,如上所示。
既然說到這裡,那我就再囉嗦一點。對於ipt_entry_match{}的結構大家可能也留意到了它內部結構有個union成員,同時它還區分了user和kernel兩種情況。我們剛剛在上面所討論的ipt_match{}結構是核心中用來表示match的資料型別,在使用者空間我們用的是iptables_match{}結構(定義在iptables.tar.gz原始碼包中的include/iptables.h標頭檔案中)來表示match的。
也就是說,核心空間和使用者空間在註冊和維護match時使用的是各自的match結構,ipt_match{}和iptables_match{},但是當某個具體的match被應用到防火牆規則裡時,它們兩個必須統一成ipt_entry_match{},這才是防火牆規則中真正用到的match結構。
至於iptables_match{}和ipt_match{}是如何完美地統一到ipt_entry_match{}結構中的,我們在後面再來詳細分析。
n 擴充套件match:
擴充套件match通常以外掛或模組的形式存在。當我們在使用者空間通過iptables命令設定規則時如果用到了-m ‘name’ 引數時,那麼此時‘name就是一個擴充套件匹配模組。前面我們也說過,如果你需要開發一個新的match模組,那麼就必須去例項化一個ipt_match{}結構體物件,並實現其中的重要回調函式(如match()函式),最後通過xt_register_match()介面將你的ipt_match{}物件註冊到Netfilter中去就可以了。實戰篇我們講解如何開發一個新match的全過程。
動作target:
根據上面的兩幅圖我們可以看到ipt_entry_match{}和ipt_entry_target{}的結構基本如出一轍,那麼它們也就存在著很多非常相似的地方了。在所有的表中關於target的使用,我們既可以用ipt_standard_target{}又可以用ipt_entry_target{},這是為什麼呢?後面再解釋。這兩個結構體的關係如下圖所示:
怎麼樣很簡單吧,就多了一個verdict變數而已。說了半天,那麼target到底是用來幹什麼的呢?說白了,target主要用來處理:當某條規則中的所有match都被資料包匹配後該執行什麼樣的動作來處理這個報文,最後將處理後結果通過verdict值返回給Netfilter框架。
同樣的target也分核心空間和使用者空間兩種結構。在核心空間中,我們所說的target由ipt_target{}表示,定義在include/linux/netfilter/ip_tables.h檔案中;在使用者空間中,所使用的是iptables_target{}結構,該結構定義在iptables原始碼包裡的iptables.h標頭檔案中。同樣地,ipt_target{}和iptables_target{}最後也完美地統一到了ipt_entry_target{}裡。
iptables的-j引數後面即可跟諸如ACCEPT、DROP這些動作,也可以跟一條使用者自定義連結串列的名字。我們都知道iptables中的規則鏈(chain)其實就是某個hook點上所有規則的集合。除了系統內建的鏈外,使用者還可以建立自定義的新鏈。在iptables中,同一個鏈裡的規則是順序存放的。內建鏈的最後一條規則的target是鏈的policy策略,而使用者自定義鏈中是沒有policy這麼一說。使用者自定義鏈的最後一條規則的target是NF_RETURN,遍歷過程將返回原來的鏈中。當然,規則中的target也可以指定跳轉到某個使用者建立的自定義鏈上,這時這條規則的target就是ipt_standard_target{}型別,並且這個target的verdict值大於0。如果在使用者自定義鏈上沒有找到任何匹配的規則的話,遍歷過程將返回到原來呼叫這條使用者自定鏈的鏈裡去匹配下一條規則。
這裡還需要注意一點:target也分為標準target和擴充套件target,前面簡單提過一些。它和標準的match以及擴充套件match還是有些區別:
標準的target,即ipt_standard_target{}裡可以根據verdict的值再劃分為內建的動作或者跳轉到自定義鏈中。簡單了說,標準的target就是核心內建的一些處理動作或其延伸。
擴充套件的target,則完全是由使用者定義的處理動作。如果ipt_target.target()函式是空的,那就是標準target,因為它不需要使用者再去提供新的target函數了;反之,如果有target函式那就是擴充套件的target。
如果我們要開發自己的target,那麼也只需要例項化一個ipt_target{}物件,並填充其內部相關的回撥函式,然後呼叫xt_register_target()將其註冊到Netfilter框架即可。
規則rule:
其實每張table表中最後真正用於表示其內部所有規則的結構體是ipt_standard{},定義在include/linux/netfilter_ipv4/ip_tables.h檔案中。最後在我們幾張表裡,如filter表,nat表裡真正用的規則結構為ipt_standard{}。它和我們前面介紹的ipt_entry{}的關係如下: