一、概述
1. Netfilter/IPTables框架簡介
Netfilter/IPTables是繼2.0.x的IPfwadm、2.2.x的IPchains之後,新一代的Linux防火牆機制。Netfilter採用模塊化設計,具有良好的可擴充性。其重要工具模塊IPTables連接到Netfilter的架構中,並允許使用者對數據報進行過濾、地址轉換、處理等操作。
Netfilter提供了一個框架,將對網絡代碼的直接干涉降到最低,並允許用規定的接口將其他包處理代碼以模塊的形式添加到內核中,具有極強的靈活性。
2. 主要源代碼文件
-
Linux內核版本:2.4.21
-
Netfilter主文件:net/core/netfilter.c
Netfilter主頭文件:include/linux/netfilter.h
-
IPv4相關:
c文件:net/ipv4/netfilter/*.c
頭文件:include/linux/netfilter_ipv4.h
include/linux/netfilter_ipv4/*.h
-
IPv4協議棧主體的部分c文件,特別是與數據報傳送過程有關的部分:
ip_input.c,ip_forward.c,ip_output.c,ip_fragment.c等
二、Netfilter/IPTables-IPv4總體架構
Netfilter主要通過表、鏈實現規則,可以這麼說,Netfilter是表的容器,表是鏈的容器,鏈是規則的容器,最終形成對數據報處理規則的實現。
詳細地說,Netfilter/IPTables的體系結構可以分爲三個大部分:
1. Netfilter的HOOK機制
Netfilter的通用框架不依賴於具體的協議,而是爲每種網絡協議定義一套HOOK函數。這些HOOK函數在數據報經過協議棧的幾個關鍵點時被調用,在這幾個點中,協議棧將數據報及HOOK函數標號作爲參數,傳遞給Netfilter框架。
對於它在網絡堆棧中增加的這些HOOK,內核的任何模塊可以對每種協議的一個或多個HOOK進行註冊,實現掛接。這樣當某個數據報被傳遞給Netfilter框架時,內核能檢測到是否有任何模塊對該協議和HOOK函數進行了註冊。若註冊了,則調用該模塊的註冊時使用的回調函數,這樣這些模塊就有機會檢查、修改、丟棄該數據報及指示Netfilter將該數據報傳入用戶空間的隊列。
這樣,HOOK提供了一種方便的機制:在數據報通過Linux內核的不同位置上截獲和操作處理數據報。
2. IPTables基礎模塊
IPTables基礎模塊實現了三個表來篩選各種數據報,具體地講,Linux2.4內核提供的這三種數據報的處理功能是相互間獨立的模塊,都基於Netfilter的HOOK函數和各種表、鏈實現。這三個表包括:filter表,nat表以及mangle表。
3. 具體功能模塊
-
-
數據報過濾模塊
-
連接跟蹤模塊(Conntrack)
-
網絡地址轉換模塊(NAT)
-
數據報修改模塊(mangle)
-
其它高級功能模塊
-
於是,Netfilter/IPTables總體架構如圖http://blog.chinaunix.net/photo/24896_061206192251.jpg所示
三、HOOK的實現
1. Netfilter-IPv4中的HOOK
Netfilter模塊需要使用HOOK來啓用函數的動態鉤接,它在IPv4中定義了五個HOOK(位於文件include/linux/netfilter_ipv4.h,Line 39),分別對應0-4的hooknum
簡單地說,數據報經過各個HOOK的流程如下:
數據報從進入系統,進行IP校驗以後,首先經過第一個HOOK函數NF_IP_PRE_ROUTING進行處理;然後就進入路由代碼,其決定該數據報是需要轉發還是發給本機的;若該數據報是發被本機的,則該數據經過HOOK函數NF_IP_LOCAL_IN處理以後然後傳遞給上層協議;若該數據報應該被轉發則它被NF_IP_FORWARD處理;經過轉發的數據報經過最後一個HOOK函數NF_IP_POST_ROUTING處理以後,再傳輸到網絡上。本地產生的數據經過HOOK函數NF_IP_LOCAL_OUT 處理後,進行路由選擇處理,然後經過NF_IP_POST_ROUTING處理後發送出去。
總之,這五個HOOK所組成的Netfilter-IPv4數據報篩選體系如圖http://blog.chinaunix.net/photo/24896_061206192311.jpg: (注:下面所說Netfilter/IPTables均基於IPv4,不再贅述)
詳細地說,各個HOOK及其在IP數據報傳遞中的具體位置如圖http://blog.chinaunix.net/photo/24896_061206192340.jpg
-
NF_IP_PRE_ROUTING (0)
數據報在進入路由代碼被處理之前,數據報在IP數據報接收函數ip_rcv()(位於net/ipv4/ip_input.c,Line379)的最後,也就是在傳入的數據報被處理之前經過這個HOOK。在ip_rcv()中掛接這個HOOK之前,進行的是一些與類型、長度、版本有關的檢查。
經過這個HOOK處理之後,數據報進入ip_rcv_finish()(位於net/ipv4/ip_input.c,Line306),進行查路由表的工作,並判斷該數據報是發給本地機器還是進行轉發。
在這個HOOK上主要是對數據報作報頭檢測處理,以捕獲異常情況。
涉及功能(優先級順序):Conntrack(-200)、mangle(-150)、DNAT(-100)
-
NF_IP_LOCAL_IN (1)
目的地爲本地主機的數據報在IP數據報本地投遞函數ip_local_deliver()(位於net/ipv4/ip_input.c,Line290)的最後經過這個HOOK。
經過這個HOOK處理之後,數據報進入ip_local_deliver_finish()(位於net/ipv4/ip_input.c,Line219)
這樣,IPTables模塊就可以利用這個HOOK對應的INPUT規則鏈表來對數據報進行規則匹配的篩選了。防火牆一般建立在這個HOOK上。
涉及功能:mangle(-150)、filter(0)、SNAT(100)、Conntrack(INT_MAX-1)
-
NF_IP_FORWARD (2)
目的地非本地主機的數據報,包括被NAT修改過地址的數據報,都要在IP數據報轉發函數ip_forward()(位於net/ipv4/ip_forward.c,Line73)的最後經過這個HOOK。
經過這個HOOK處理之後,數據報進入ip_forward_finish()(位於net/ipv4/ip_forward.c,Line44)
另外,在net/ipv4/ipmr.c中的ipmr_queue_xmit()函數(Line1119)最後也會經過這個HOOK。(ipmr爲多播相關,估計是在需要通過路由轉發多播數據時的處理)
這樣,IPTables模塊就可以利用這個HOOK對應的FORWARD規則鏈表來對數據報進行規則匹配的篩選了。
涉及功能:mangle(-150)、filter(0)
-
NF_IP_LOCAL_OUT (3)
本地主機發出的數據報在IP數據報構建/發送函數ip_queue_xmit()(位於net/ipv4/ip_output.c,Line339)、以及ip_build_and_send_pkt()(位於net/ipv4/ip_output.c,Line122)的最後經過這個HOOK。(在數據報處理中,前者最爲常用,後者用於那些不傳輸有效數據的SYN/ACK包)
經過這個HOOK處理後,數據報進入ip_queue_xmit2()(位於net/ipv4/ip_output.c,Line281)
另外,在ip_build_xmit_slow()(位於net/ipv4/ip_output.c,Line429)和ip_build_xmit()(位於net/ipv4/ip_output.c,Line638)中用於進行錯誤檢測;在igmp_send_report()(位於net/ipv4/igmp.c,Line195)的最後也經過了這個HOOK,進行多播時相關的處理。
這樣,IPTables模塊就可以利用這個HOOK對應的OUTPUT規則鏈表來對數據報進行規則匹配的篩選了。
涉及功能:Conntrack(-200)、mangle(-150)、DNAT(-100)、filter(0)
-
NF_IP_POST_ROUTING (4)
所有數據報,包括源地址爲本地主機和非本地主機的,在通過網絡設備離開本地主機之前,在IP數據報發送函數ip_finish_output()(位於net/ipv4/ip_output.c,Line184)的最後經過這個HOOK。
經過這個HOOK處理後,數據報進入ip_finish_output2()(位於net/ipv4/ip_output.c,Line160)另外,在函數ip_mc_output()(位於net/ipv4/ip_output.c,Line195)中在克隆新的網絡緩存skb時,也經過了這個HOOK進行處理。
涉及功能:mangle(-150)、SNAT(100)、Conntrack(INT_MAX)
其中,入口爲net_rx_action()(位於net/core/dev.c,Line1602),作用是將數據報一個個地從CPU的輸入隊列中拿出,然後傳遞給協議處理例程。
出口爲dev_queue_xmit()(位於net/core/dev.c,Line1035),這個函數被高層協議的實例使用,以數據結構struct sk_buff *skb的形式在網絡設備上發送數據報。
2. HOOK的調用
HOOK的調用是通過宏NF_HOOK實現的,其定義位於include/linux/Netfilter.h,Line122:
#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)))
這裏先調用list_empty函數檢查HOOK點存儲數組nf_hooks是否爲空,爲空則表示沒有HOOK註冊,則直接調用okfn繼續處理。如果不爲空,則轉入nf_hook_slow()函數。
nf_hook_slow()函數(位於net/core/netfilter.c,Line449)的工作主要是讀nf_hook數組遍歷所有的nf_hook_ops結構,並調用nf_hookfn()處理各個數據報。
即HOOK的調用過程如圖http://blog.chinaunix.net/photo/24896_061206192356.jpg所示
下面說明一下NF_HOOK的各個參數:
-
pf:協議族標識,相關的有效協議族列表位於include/linux/socket.h,Line 178。對於IPv4,應該使用協議族PF_INET;
-
hook:HOOK標識,即前面所說5個HOOK對應的hooknum;
-
skb:是含有需要被處理包的sk_buuff數據結構的指針。sk_buff是Linux網絡緩存,指那些linux內核處理IP分組報文的緩存,即套接字緩衝區。
網卡收到IP分組報文後,將它們放入sk_buff,然後再傳送給網絡堆棧,網絡堆棧幾乎一直要用到sk_buff。其定義在include/linux/skbuff.h,Line 129,下面列出我認爲對分析有意義的部分成員:
-
-
`struct sock *sk;`:指向創建分組報文的socket;
-
`struct timeval stamp;`:分組報文到達系統的時間;
-
下面是三個union,存放的是各層中各種協議的報文頭指針:
-
h對應傳輸層的報頭
-
nh對應網絡層的報頭
-
mac對應MAC層的報頭
-
-
`unsigned int len;`:套接字緩存所代表的報文長度,即從`unsigned char *data;`的位置算起的當前有效報文長度。
-
`unsigned char pkt_type,`:表示報文的類型,具體類型定義在include/linux/if_packet.h,Line24:
-
#define PACKET_HOST 0 // 發送到本機的報文
#define PACKET_BROADCAST 1 // 廣播報文
#define PACKET_MULTICAST 2 // 多播報文
#define PACKET_OTHERHOST 3 // 表示目的地非本機但被本機 接收的報文
#define PACKET_OUTGOING 4 // 離開本機的報文
/* These ones are invisible by user level */
#define PACKET_LOOPBACK 5 // 本機發給自己的報文
#define PACKET_FASTROUTE 6 // 快速路由報文
-
indev:輸入設備,收到數據報的網絡設備的net_device數據結構指針,即數據報到達的接口。
-
用於NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN兩個HOOK
-
-
outdev:輸出設備,數據報離開本地所要使用的網絡設備的net_device數據結構指針。
-
用於NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING兩個HOOK
-
注意:在通常情況下,在一次HOOK調用中,indev和outdev中只有一個參數會被使用
-
-
okfn:下一步要處理的函數。即如果有HOOK函數,則處理完所有的HOOK函數,且所有向該HOOK註冊過的篩選函數都返回NF_ACCEPT時,調用這個函數繼續處理;如果沒有註冊任何HOOK,則直接調用此函數。其5個參數將由宏NF_HOOK傳入。
3. HOOK點的實現
對應於各個不同協議的不同HOOK點是由一個二維數組nf_hooks存儲的(位於net/core/netfilter.c,Line 47),具體的HOOK點則由數據結構nf_hook_ops(位於include/linux/netfilter.h,Line 44)實現。如圖http://blog.chinaunix.net/photo/24896_061206192528.jpg所示:
其中,nf_hook_ops成員中:
-
`int priority;` priority值越小,優先級越高,相關優先級在include/linux/netfilter_ipv4.h,Line52中枚舉定義:
enum NF_IP_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_CONNTRACK= -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_LAST = INT_MAX,
};
-
`nf_hookfn *hook;` 爲處理函數的指針,其函數指針類型定義位於include/linux/netfilter.h,Line38,爲:
typedef unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));
這是nf_hook_ops中最關鍵的成員,其五個參數分別對應前面所解釋的NF_HOOK中弟2到6個參數
調用HOOK的包篩選函數必須返回特定的值,這些值以宏的形式定義於頭文件include/linux/netfilter.h中(Line15),分別爲:
-
-
NF_DROP(0):丟棄此數據報,禁止包繼續傳遞,不進入此後的處理流程;
-
NF_ACCEPT(1):接收此數據報,允許包繼續傳遞,直至傳遞到鏈表最後,而進入okfn函數;
-
以上兩個返回值最爲常見
-
-
NF_STOLEN(2):數據報被篩選函數截獲,禁止包繼續傳遞,但並不釋放數據報的資源,這個數據報及其佔有的sk_buff仍然有效(e.g. 將分片的數據報一一截獲,然後將其裝配起來再進行其他處理);
-
NF_QUEQUE(3):將數據報加入用戶空間隊列,使用戶空間的程序可以直接進行處理;
-
在nf_hook_slow()以及nf_reinject()函數(位於net/core/netfilter.c,Line449,Line505)中,當由調用nf_iterate()函數(位於net/core/netfilter.c,Line339,作用爲遍歷所有註冊的HOOK函數,並返回相應的NF_XX值)而返回的verdict值爲NF_QUEUE時(即當前正在執行的這個HOOK篩選函數要求將數據報加入用戶空間隊列),會調用nf_queue()函數(位於net/core/netfilter.c,Line407)
-
nf_queue()函數將這個數據報加入用戶空間隊列nf_info(位於include/linux/netfilter.h,Line77),並保存其設備信息以備用
-
-
NF_REPEAT(4):再次調用當前這個HOOK的篩選函數,進行重複處理。
-
4. HOOK的註冊和註銷
HOOK的註冊和註銷分別是通過nf_register_hook()函數和nf_unregister_hook()函數(分別位於net/core/netfilter.c,Line60,76)實現的,其參數均爲一個nf_hook_ops結構,二者的實現也非常簡單。
nf_register_hook()的工作是首先遍歷nf_hools[][],由HOOK的優先級確定在HOOK鏈表中的位置,然後根據優先級將該HOOK的nf_hook_ops加入鏈表;
nf_unregister_hook()的工作更加簡單,其實就是將該HOOK的nf_hook_ops從鏈表中刪除。
四、IPTables系統
1. 表-規則系統
IPTables是基於Netfilter基本架構實現的一個可擴展的數據報高級管理系統,利用table、chain、rule三級來存儲數據報的各種規則。系統預定義了三個table:
-
filter:數據報過濾表(文件net/ipv4/netfilter/iptable_filter.c)
監聽NF_IP_LOCAL_IN、NF_IP_FORWARD和NF_IP_LOCAL_OUT三個HOOK,作用是在所有數據報傳遞的關鍵點上對其進行過濾。
-
nat:網絡地址轉換表
監聽NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING和NF_IP_LOCAL_OUT三個HOOK,作用是當新連接的第一個數據報經過時,在nat表中決定對其的轉換操作;而後面的其它數據報都將根據第一個數據報的結果進行相同的轉換處理。
-
mangle:數據報修改表(位於net/ipv4/netfilter/iptable_mangle.c)
監聽NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT兩個HOOK,作用是修改數據報報頭中的一些值。
2. 表的實現
-
表的基本數據結構是ipt_table(位於include/linux/netfilter_ipv4/ip_tables.h,Line413):
struct ipt_table
{
struct list_head list; // 一個雙向鏈表
char name[IPT_TABLE_MAXNAMELEN]; // 被用戶空間使用的表函數的名字
struct ipt_replace *table; // 表初始化的模板,定義了一個初始化用的該 // 表的所默認的HOOK所包含的規則等信息,
// 用戶通過系統調用進行表的替換時也要用
unsigned int valid_hooks; // 表所監聽的HOOK,實質是一個位圖
rwlock_t lock; // 整個表的讀/寫自旋鎖
struct ipt_table_info *private; // 表所存儲的數據信息,也就是實際的數據區,
// 僅在處理ipt_table的代碼內部使用
struct module *me; // 如果是模塊,那麼取THIS_MODULE,否則取NULL
};
其中:
-
`unsigned int valid_hooks;`這個位圖有兩個作用:一是檢查Netfilter中哪些HOOK對應着合法的entries;二是用來爲ipt_match以及ipt_target數據結構中的checkentry()函數覈算可能的HOOK。
-
`struct module *me;`當取值爲THIS_MODULE時,可以阻止用戶rmmod一個仍然被某個規則指向的模塊的嘗試。
-
`struct ipt_replace *table;`的數據結構是被用戶空間用來替換一個表的,其定義位於include/linux/netfilter_ipv4/ip_tables.h,Line230:
struct ipt_replace
{
char name[IPT_TABLE_MAXNAMELEN];
unsigned int valid_hooks;
unsigned int num_entries; // 規則表入口的數量
unsigned int size; // 新的規則表的總大小
/* Hook entry points. */
unsigned int hook_entry[NF_IP_NUMHOOKS]; // 表所監聽HOOK的規則入口,
// 是對於entries[ ]的偏移
unsigned int underflow[NF_IP_NUMHOOKS]; // 規則表的最大下界
unsigned int num_counters; // 舊的計數器數目,即當前的舊entries的數目
struct ipt_counters *counters; // 舊的計數器
struct ipt_entry entries[0]; // 規則表入口
};
-
上文所提到的filter、nat和mangle表分別是ipt_table這個數據結構的三個實例:packet_filter(位於net/ipv4/netfilter/iptable_filter.c,Line84)、nat_table(位於net/ipv4/netfilter/ip_nat_rule.c,Line104)以及packet_mangler(位於net/ipv4/netfilter/iptable_mangle.c,Line117)
-
ipt_table_info(位於net/ipv4/netfilter/ip_tables.c,Line86)是實際描述規則表的數據結構:
struct ipt_table_info
{
unsigned int size;
unsigned int number; // 表項的數目
unsigned int initial_entries; // 初始表項數目
unsigned int hook_entry[NF_IP_NUMHOOKS]; // 所監聽HOOK的規則入口
unsigned int underflow[NF_IP_NUMHOOKS]; // 規則表的最大下界
char entries[0] ____cacheline_aligned; // 規則表入口,即真正的規則存儲結構 // ipt_entry組成塊的起始地址,對多CPU,每個CPU對應一個
};
3. 規則的實現
IPTables中的規則表可以在用戶空間中使用,但它所採用的數據結構與內核空間中的是一樣的,只不過有些成員不會在用戶空間中使用。
一個完整的規則由三個數據結構共同實現,分別是:
-
-
一個ipt_entry結構,存儲規則的整體信息;
-
0或多個ipt_entry_match結構,存放各種match,每個結構都可以存放任意的數據,這樣也就擁有了良好的可擴展性;
-
1個ipt_entry_target結構,存放規則的target,類似的,每個結構也可以存放任意的數據。
-
下面將依次對這三個數據結構進行分析:
-
存儲規則整體的結構ipt_entry,其形式是一個鏈表(位於include/linux/netfilter_ipv4/ip_tables.h,Line122):
struct ipt_entry
{
struct ipt_ip ip;
unsigned int nfcache;
u_int16_t target_offset;
u_int16_t next_offset;
unsigned int comefrom;
struct ipt_counters counters;
unsigned char elems[0];
};
其成員如下:
-
-
`struct ipt_ip ip;`:這是對其將要進行匹配動作的IP數據報報頭的描述,其定義於include/linux/netfilter_ipv4/ip_tables.h,Line122,其成員包括源/目的IP及其掩碼,出入端口及其掩碼,協議族、標誌/取反flag等信息。
-
`unsigned int nfcache;`:HOOK函數返回的cache標識,用以說明經過這個規則後數據報的狀態,其可能值有三個,定義於include/linux/netfilter.h,Line23:
-
#define NFC_ALTERED 0x8000 //已改變
#define NFC_UNKNOWN 0x4000 //不確定
另一個可能值是0,即沒有改變。
-
-
`u_int16_t target_offset;`:指出了target的數據結構ipt_entry_target的起始位置,即從ipt_entry的起始地址到match存儲結束的位置
-
`u_int16_t next_offset;`:指出了整條規則的大小,也就是下一條規則的起始地址,即ipt_entry的起始地址到match偏移再到target存儲結束的位置
-
`unsigned int comefrom;`:所謂的“back pointer”,據引用此變量的代碼(主要是net/ipv4/netfilter/ip_tables.c中)來看,它應該是指向數據報所經歷的上一個規則地址,由此實現對數據報行爲的跟蹤
-
`struct ipt_counters counters;`:說明了匹配這個規則的數據報的計數以及字節計數(定義於include/linux/netfilter_ipv4/ip_tables.h,Line100)
-
`unsigned char elems[0];`:表示擴展的match開始的具體位置(因爲它是大小不確定的),當然,如果不存在擴展的match那麼就是target的開始位置
-
-
擴展match的存儲結構ipt_entry_match,位於include/linux/netfilter_ipv4/ip_tables.h,Line48:
struct ipt_entry_match
{
union {
struct {
u_int16_t match_size;
char name[IPT_FUNCTION_MAXNAMELEN];
} user;
struct {
u_int16_t match_size;
struct ipt_match *match;
} kernel;
u_int16_t match_size; //總長度
} u;
unsigned char data[0];
};
其中描述match大小的`u_int16_t match_size;`,從涉及這個變量的源碼看來,在使用的時候需要注意使用一個宏IPT_ALIGN(位於include/linux/netfilter_ipv4/ip_tables.h,Line445)來進行4的對齊處理(0x3 & 0xfffffffc),這應該是由於match、target擴展後大小的不確定性決定的。
在結構中,用戶空間與內核空間爲不同的實現,內核空間中的描述擁有更多的信息。在用戶空間中存放的僅僅是match的名稱,而在內核空間中存放的則是一個指向ipt_match結構的指針
結構ipt_match位於include/linux/netfilter_ipv4/ip_tables.h,Line342:
struct ipt_match
{
struct list_head list;
const char name[IPT_FUNCTION_MAXNAMELEN];
int (*match)(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const void *matchinfo, // 指向規則中match數據的指針,
// 具體是什麼數據結構依情況而定
int offset, // IP數據報的偏移
const void *hdr, // 指向協議頭的指針
u_int16_t datalen, // 實際數據長度,即數據報長度-IP頭長度
int *hotdrop);
int (*checkentry)(const char *tablename, // 可用的表
const struct ipt_ip *ip,
void *matchinfo,
unsigned int matchinfosize,
unsigned int hook_mask); // 對應HOOK的位圖
void (*destroy)(void *matchinfo, unsigned int matchinfosize);
struct module *me;
};
其中幾個重要成員:
-
-
`int (*match)(……);`:指向用該match進行匹配時的匹配函數的指針,match相關的核心實現。返回0時hotdrop置1,立即丟棄數據報;返回非0表示匹配成功。
-
`int (*checkentry)(……);`:當試圖插入新的match表項時調用這個指針所指向的函數,對新的match表項進行有效性檢查,即檢查參數是否合法;如果返回false,規則就不會被接受(譬如,一個TCP的match只會TCP包,而不會接受其它)。
-
`void (*destroy)(……);`:當試圖刪除一個使用這個match的表項時,即模塊釋放時,調用這個指針所指向的函數。我們可以在checkentry中動態地分配資源,並在destroy中將其釋放。
-
-
擴展target的存儲結構ipt_entry_target,位於include/linux/netfilter_ipv4/ip_tables.h,Line71,這個結構與ipt_entry_match結構類似,同時其中描述內核空間target的結構ipt_target(位於include/linux/netfilter_ipv4/ip_tables.h,Line375)也與ipt_match類似,只不過其中的target()函數返回值不是0/1,而是verdict。
而target的實際使用中,是用一個結構ipt_standard_target專門來描述,這纔是實際的target描述數據結構(位於include/linux/netfilter_ipv4/ip_tables.h,Line94),它實際上就是一個ipt_entry_target加一個verdict。
-
其中成員verdict這個變量是一個很巧妙的設計,也是一個非常重要的東東,其值的正負有着不同的意義。我沒有找到這個變量的中文名稱,在內核開發者的新聞組中稱這個變量爲“a magic number”。 它的可能值包括IPT_CONTINUE、IPT_RETURN以及前文所述的NF_DROP等值,那麼它的作用是什麼呢?
-
-
由於IPTables是在用戶空間中執行的,也就是說Netfilter/IPTables這個框架需要用戶態與內核態之間的數據交換以及識別。而在具體的程序中,verdict作爲`struct ipt_standard_target`的一個成員,也是對於`struct ipt_entry_target`中的target()函數的返回值。這個返回值標識的是target()所對應的執行動作,包括系統的默認動作以及外部提交的自定義動作。
-
但是,在用戶空間中提交的數據往往是類似於“ACCPET”之類的字符串,在程序處理時則是以`#define NF_ACCEPT 1`的形式來進行的;而實際上,以上那些執行動作是以鏈表的數據結構進行存儲的,在內核空間中表現爲偏移。
-
於是,verdict實際上描述了兩個本質相同但實現不同的值:一個是用戶空間中的執行動作,另一個則是內核空間中在鏈表中的偏移——而這就出現了衝突。
-
解決這種衝突的方法就是:用正值表示內核中的偏移,而用負值來表示數據報的那些默認動作,而外部提交的自定義動作則也是用正值來表示。這樣,在實際使用這個verdict時,我們就可以通過判斷值的正負來進行相應的處理了。
-
位於net/ipv4/netfilter/ip_tables.h中的函數ipt_do_table()中有一個典型的verdict使用(Line335,其中v是一個verdict的實例):
-
if (v !=IPT_RETURN) {
verdict = (unsigned)(-v) - 1;
break;
}
其中的IPT_RETURN定義爲:
#define IPT_RETURN (-NF_MAX_VERDICT – 1)
而宏NF_MAX_VERDICT實際上就是:
#define NF_MAX_VERDICT NF_REPEAT
這樣,實際上IPT_RETURN的值就是-NF_REPEAT-1,也就是對應REPEAT,這就是對執行動作的實際描述;而我們可以看到,在下面對verdict進行賦值時,它所使用的值是`(unsigned)(-v) – 1`,這就是在內核中實際對偏移進行定位時所使用的值。
那麼總之呢,表和規則的實現如圖http://blog.chinaunix.net/photo/24896_061206192551.jpg所示:
從上圖中不難發現,match的定位如下:
-
-
起始地址爲:當前規則(起始)地址+sizeof(struct ipt_entry);
-
結束地址爲:當前規則(起始)地址+ipt_entry->target_offset;
-
每一個match的大小爲:ipt_entry_match->u.match_size。
-
target的定位則爲:
-
-
起始地址爲match的結束地址,即:當前規則(起始)地址+ipt_entry-> target_offset;
-
結束地址爲下一條規則的起始地址,即:當前規則(起始)地址+ipt_entry-> next_offset;
-
每一個target的大小爲:ipt_entry_target->u.target_size。
-
這些對於理解match以及target相關函數的實現是很有必要明確的。
同時,include/linux/netfilter_ipv4/ip_tables.h中提供了三個“helper functions”,可用於使對於entry、tartget和match的操作變得方便,分別是:
-
函數ipt_get_target():Line274,作用是取得target的起始地址,也就是上面所說的當前規則(起始)地址+ipt_entry-> target_offset;
-
宏IPT_MATCH_ITERATE():Line281,作用是遍歷規則的所有match,並執行同一個(參數中)給定的函數。其參數爲一個ipt_entry_match結構和一個函數,以及函數需要的參數。當返回值爲0時,表示遍歷以及函數執行順利完成;返回非0值時則意味着出現問題已終止。
-
宏IPT_ENTRY_ITERATE():Line300,作用是遍歷一個表中的所有規則,並執行同一個給定的函數。其參數爲一個ipt_entry結構、整個規則表的大小,以及一個函數和其所需參數。其返回值的意義與宏IPT_MATCH_ITERATE()類似。
-
那麼如何保證傳入的ipt_entry結構是整個規則表的第一個結構呢?據源碼看來,實際調用這個宏的時候傳入的第一個參數都是某個ipt_table_info結構的實例所指向的entries成員,這樣就保證了對整個規則表的完整遍歷。
-
4. 規則的使用
當一個特定的HOOK被激活時,數據報就開始進入Netfilter/IPTables系統進行遍歷,首先檢查`struct ipt_ip ip`,然後數據報將依次遍歷各個match,也就是`struct ipt_entry_match`,並執行相應的match函數,即ipt_match結構中的*match所指向的函數。當match函數匹配不成功時返回0,或者hotdrop被置爲1時,遍歷將會停止。
對match的遍歷完成後,會開始檢查`struct ipt_entry_target`,其中如果是一個標準的target,那麼會檢查`struct ipt_standard_target`中的verdict,如果verdict值是正的而偏移卻指向不正確的位置,那麼ipt_entry中的comefrom成員就有了用武之地——數據報返回所經歷的上一個規則。對於非標準的target呢,就會調用target()函數,然後根據其返回值進行後面的處理。
5. 規則的擴展
Netfilter/IPTables提供了對規則進行擴展的機制:可以寫一個LKM來擴展內核空間的功能,也可以寫一個共享庫來擴展用戶空間中IPTables的功能。
-
內核的擴展
要對內核空間的功能進行擴展,實際上就是寫一個具有表、match以及target增加功能的模塊,相關的函數爲(位於net/ipv4/netfilter/ip_tables.c,Line1318 to 1444):
-
-
ipt_register_table()、ipt_unregister_table(),參數爲struct ipt_table *。
-
ipt_register_table()函數是這三對函數中最複雜的一個,涉及了內存、信號量等方方面面的東西,但總起來說就做了初始化表以及加入雙向鏈表兩件事。
-
其複雜一是因爲涉及到多CPU的處理(每個CPU擁有各自獨立的“規則空間”),需要首先將新的entries放入第一個CPU空間,在檢查完畢後再複製到其他CPU中;二是就是上面所說對新table各個entry的檢查,包括邊界檢查以及完整性檢查等。
-
其中的重要函數有這麼幾個:
-
translate_table()(位於net/ipv4/netfilter/ip_tables.c,Line797):這個函數的主要作用是檢查並應用用戶空間傳來的規則 :
-
-
-
-
-
-
-
對新表進行邊界檢查(由宏IPT_ENTRY_ITERATE()調用函數check_entry_size_and_blocks(),位於net/ipv4/netfilter/ip_tables.c,Line732),包括對齊、過大過小等,特別是保證賦給hook_entries和underflows值的正確性。
-
調用函數make_source_chains()(位於net/ipv4/netfilter/ip_tables.c,Line499)檢查新的表中是否存在規則環,同時將HOOK的規則遍歷順序存入comefrom變量。(這個函數我沒有仔細看,只是大概略了一下)
-
對ipt_entry依次進行ipt_ip頭、match以及target的完整性檢查(由宏IPT_ENTRY_ITERATE()調用函數check_entry(),位於net/ipv4/netfilter/ip_tables.c,Line676),保證ipt_entry的正確性。
-
將正確的 ipt_tables複製給其他的CPU。
-
-
-
這個函數另外還在do_replace()函數(僅在一個源碼中沒有被調用過的函數中被調用,不予分析)中被調用。
-
-
-
-
replace_table()(位於net/ipv4/netfilter/ip_tables.c,Line877):這個函數的主要作用是:將得到模塊初始值的ipt_table_info結構(newinfo)中的值傳給ipt_table中的private,並返回ip_table中舊的private。
-
list_prepend()(位於include/linux/netfilter_ipv4/listhelp.h,Line75):在這個函數被調用之前,整個初始化的過程就已經結束了,這個函數的主要作用是:互斥地調用Linux源碼中的list_add()函數(位於include/linux/list.h,Line55),將新的table加入到雙向鏈表之中。
-
-
-
-
-
ipt_register_match()、ipt_unregister_match(),參數爲struct ipt_match *。
-
ipt_register_target()、ipt_unregister_target(),參數爲struct ipt_target *。
-
這三對函數除了ipt_register_table()外的5個函數主要就是互斥地將table/match/target加入到雙向鏈表中或者從雙向鏈表中刪除。
其中向雙向鏈表中加入新節點是通過調用list_named_insert()函數(位於include/linux/netfilter_ipv4/listhelp.h,Line101)實現的。這個函數的主要工作是首先確定待插入的match名字是否已經存在,只有不存在時才進行插入的操作。
-
用戶空間的擴展
用戶空間中的擴展用的是共享庫配合libiptc庫的機制,但這種機制是在單獨的IPTbales程序中提供的,內核源碼中並沒有提供,這裏就不做分析了。
五、數據報過濾模塊——filter表
1. 概述
filter表的功能僅僅是對數據報進行過濾,並不對數據報進行任何的修改。
filter模塊在Netfilter中是基於下列HOOK點的:
-
-
NF_IP_LOCAL_IN
-
NF_IP_FORWARD
-
NF_IP_LOCAL_OUT
-
這幾個HOOK分別對應着filter表中的INPUT、FORWARD、OUTPUT三條規則鏈,對於任何一個數據報都會經過這3個HOOK之一。
filter模塊的接口位於文件net/ipv4/netfilter/iptables_filter.c中。
2. filter表的定義和初始化
filter表是前面所述數據結構ipt_table的一個實例,它的定義和初始化位於net/ipv4/netfilter/iptable_filter.c,Line84:
static struct ipt_table packet_filter
= { { NULL, NULL }, "filter", &initial_table.repl,
FILTER_VALID_HOOKS, RW_LOCK_UNLOCKED, NULL, THIS_MODULE };
對照結構ipt_table的定義,我們可以發現,filter表的初始化數據爲:
-
鏈表初始化爲空
-
表名爲filter
-
初始化的模板爲&initial_table.repl
-
初始化的模板表定義於net/ipv4/netfilter/iptable_filter.c,Line30,是一個很簡單的數據結構,只是賦值有些複雜,因爲要對所涉及的各個HOOK進行不同的處理:
-
static struct
{
struct ipt_replace repl;
struct ipt_standard entries[3];
struct ipt_error term;
} initial_table __initdata
= { { "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, { } },
{
/* LOCAL_IN */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* FORWARD */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } },
/* LOCAL_OUT */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_standard),
0, { 0, 0 }, { } },
{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },
-NF_ACCEPT - 1 } }
},
/* ERROR */
{ { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 },
0,
sizeof(struct ipt_entry),
sizeof(struct ipt_error),
0, { 0, 0 }, { } },
{ { { { ipt_ALIGN(sizeof(struct ipt_error_target)), ipt_ERROR_TARGET } },
{ } },
"ERROR"
}
}
};
我們可以看到,一個initial_table包含三個成員:
-
-
`struct ipt_replace repl;`:是對一個表進行初始化的最主要部分,這個ipt_replace結構在前面已經分析過了;
-
`struct ipt_standard entries[3];`:是對這個表所監聽的各個HOOK上對應的初始化信息,實際上就是一個ipt_entry結構加一個ipt_standard_target結構;
-
`struct ipt_error term;`:是這個表出錯時對應的信息,實際上就是一個ipt_entry結構、一個ipt_entry_target結構再加一個errorname。
-
-
當前表所監聽的HOOK位圖爲FILTER_VALID_HOOKS,位於net/ipv4/netfilter/iptable_filter.c,Line9:
#define FILTER_VALID_HOOKS ((1 << NF_IP_LOCAL_IN) | (1 << NF_IP_FORWARD) | (1 << NF_IP_LOCAL_OUT))
我們可以看到,實際上就是IN,FORWARD和OUT。
-
讀寫鎖爲RW_LOCK_UNLOCKED,即爲打開狀態
-
實際數據區ipt_table_info爲空
-
定義爲模塊
3. filter表的實現
filter表的實現函數實際上就是模塊iptable_filter.o的init函數,位於net/ipv4/netfilter/iptable_filter.c,Line128。其主要工作是首先通過ipt_register_table()函數進行表的註冊,然後用nf_register_hook()函數註冊表所監聽的各個HOOK。
其中,對HOOK進行註冊時,是通過對數據結構nf_hook_ops的一個實例ipt_ops進行操作來實現的,這個實例的定義及初始化位於net/ipv4/netfilter/iptable_filter.c,Line117:
static struct nf_hook_ops ipt_ops[]
= { { { NULL, NULL }, ipt_hook, PF_INET, NF_IP_LOCAL_IN, NF_IP_PRI_FILTER },
{ { NULL, NULL }, ipt_hook, PF_INET, NF_IP_FORWARD, NF_IP_PRI_FILTER },
{ { NULL, NULL }, ipt_local_out_hook, PF_INET, NF_IP_LOCAL_OUT,
NF_IP_PRI_FILTER }
};
對應前面所分析nf_hook_ops的各個成員,不難確定這些初始化值的意義。
其中,對應IN和FORWARD的處理函數均爲ipt_hook,OUT的處理函數則爲ipt_local_out_hook,下面依次分析之:
-
ipt_hook,定義於net/ipv4/netfilter/iptable_filter.c,Line89:
static unsigned int
ipt_hook(unsigned int hook,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return ipt_do_table(pskb, hook, in, out, &packet_filter, NULL);
}
實際上它就是調用了ipt_do_table()函數,也就是說,註冊時首先註冊一個ipt_hook()函數,然後ipt_hook()通過調用ipt_do_table()函數對傳入的數據進行真正的處理。下面我們來看一下ipt_do_table()這個函數:
它位於net/ipv4/netfilter/ip_tables.c,Line254,是一個很長的函數,其主要功能是對數據報進行各種匹配、過濾(包括基本規則、matches以及target),具體些說,其工作大致爲:
-
初始化各種變量,如IP頭、數據區、輸入輸出設備、段偏移、規則入口及偏移量等等;
-
進行規則的匹配,首先調用ip_packet_match()函數(位於net/ipv4/netfilter/ip_tables.c,Line121)確定IP數據報是否匹配規則,若不匹配則跳到下一條規則(這個函數的主要工作大致爲:依次處理源/目的IP地址、輸入輸出接口,然後對基本的規則進行匹配);
-
如果數據報匹配,則下面開始繼續匹配matches和target,首先利用宏IPT_MATCH_ITERATE調用do_match()函數(下面單獨分析)對擴展的match進行匹配,若不匹配則跳到下一條規則;
-
擴展match匹配後,首先調用ipt_get_target()獲得target的偏移地址,然後對target進行匹配,這個匹配的過程要比match的匹配過程複雜一些,同樣在下面單獨分析。
下面首先來分析do_match()函數,它位於net/ipv4/netfilter/ip_tables.c,Line229,它的實現只有一個if語句:
if (!m->u.kernel.match->match(skb, in, out, m->data, offset, hdr, datalen, hotdrop))
return 1;
else
return 0;
其中的`m->u.kernel.match->match(skb, in, out, m->data, offset, hdr, datalen, hotdrop)`是用來定位match的。
因爲如果僅僅是根據match的名字遍歷鏈表來進行查找的話,效率會非常低下。Netfilter源碼中採用的方法是在進行match的檢測之前,也就是在ipt_register_table()函數中通過translate_table()函數由宏IPT_ENTRY_ITERATE調用函數check_entry()時,在check_entry()中通過宏IPT_MATCH_ITERATE調用了check_match()函數(位於net/ipv4/netfilter/ip_tables.c,Line640),在這個函數中,有一個對m->u.kernel.match的賦值:
m->u.kernel.match = match;
這樣,每條規則的u.kernel.match 就與內核模塊中的struct ipt_match鏈表關聯了起來,也就是說,這樣一來,根據match的名字,其對應的match函數就與鏈表中對應的函數關聯了起來。於是,上面的那條定位match的語句的意義也就開始明瞭了:
利用宏IPT_MATCH_ITERATE來遍歷規則中的所有mach,然後直接調用`m->u.kernel.match->match`來進行對數據報的匹配工作——這樣的效率顯然要比簡單的遍歷要高許多。
然後我們來看一下對target的匹配,從數據結構的實現上看,似乎這個過程與match的匹配應該是相似的,但實際上target存在標準的和非標準的兩種,其中標準的target與非標準的target的處理是不一樣的。在這裏我遇到了問題,如下:
首先,在Netfilter的源碼中,存在兩個ipt_standard_target,其中一個是一個struct,位於include/linux/netfilter_ipv4/ip_tables.h,Line94;另一個是`struct ipt_target`的一個實例,位於net/ipv4/netfilter/IPtables.c,Line1684,而在target的匹配過程中,它是這樣處理的(ipt_do_tables(),net/ipv4/netfilter/ip_tables.c,Line329):
/* Standard target? */
if (!t->u.kernel.target->target) {……}
從這裏看來,它應該是當 t->u.kernel.target的target函數爲空時,表明其爲標準的target。那麼結合上述兩者的定義,似乎用的是後者,因爲後者的定義及初始化如下:
/* The built-in targets: standard (NULL) and error. */
static struct ipt_target ipt_standard_target
= { { NULL, NULL }, IPT_STANDARD_TARGET, NULL, NULL, NULL };
但是問題出現在:初始化中的IPT_STANDARD_TARGET被定義爲””!!並且在整個源碼中,用到實例化的ipt_standard_target的地方僅有兩處,即上面的這個定義以及ip_tables.c中將ipt_standard_target加入到target鏈表之中。也就是說這個實例的名字一直爲空,這一點如何理解?
-
ipt_local_out_hook,定義於net/ipv4/netfilter/iptable_filter.c,Line99其功能與ipt_hook()相似,只不過爲了防止DOS攻擊而增加了對ratelimit的檢查。
這樣,到這裏,filter表的實現已經分析完畢,至於具體的過濾功能如何實現,那就是每個HOOK處理函數的問題了。
六、連接跟蹤模塊(Conntrack)
1. 概述
連接跟蹤模塊是NAT的基礎,但作爲一個單獨的模塊實現。它用於對包過濾功能的一個擴展,管理單個連接(特別是TCP連接),並負責爲現有的連接分配輸入、輸出和轉發IP數據報,從而基於連接跟蹤來實現一個“基於狀態”的防火牆。
當連接跟蹤模塊註冊一個連接建立包之後,將生成一個新的連接記錄。此後,所有屬於此連接的數據報都被唯一地分配給這個連接。如果一段時間內沒有流量而超時,連接將被刪除,然後其他需要使用連接跟蹤的模塊就可以重新使用這個連接所釋放的資源了。
如果需要用於比傳輸協議更上層的應用協議,連接跟蹤模塊還必須能夠將建立的數據連接與現有的控制連接相關聯。
Conntrack在Netfilter中是基於下列HOOK的:
-
NF_IP_PRE_ROUTING
-
NF_IP_LOCAL_OUT
-
同時當使用NAT時,Conntrack也會有基於NF_IP_LOCAL_IN和NF_IP_POST_ROUTING的,只不過優先級很小。
在所有的HOOK上,NF_IP_PRI_CONNTRACK的優先級是最高的(-200),這意味着每個數據報在進入和發出之前都首先要經過Conntrack模塊,然後纔會被傳到鉤在HOOK上的其它模塊。
Conntrack模塊的接口位於文件net/ipv4/netfilter/ip_conntrack_standalone.c中。
2. 連接狀態的管理
-
多元組
在連接跟蹤模塊中,使用所謂的“tuple”,也就是多元組,來小巧銳利地描述連接記錄的關鍵部分,主要是方便連接記錄的管理。其對應的數據結構是ip_conntrack_tuple(位於include/linux/netfilter_ipv4/ip_conntrack_tuple.h,Line38):
struct ip_conntrack_tuple
{
struct ip_conntrack_manip src;
struct {
u_int32_t ip;
union {
u_int16_t all;
struct { u_int16_t port; } tcp;
struct { u_int16_t port; } udp;
struct { u_int8_t type, code; } icmp;
} u;
u_int16_t protonum;
} dst;
};
從它的定義可以看出,一個多元組實際上包括兩個部分:一是所謂的“unfixed”部分,也就是源地址以及端口;二是所謂的“fixed”部分,也就是目的地址、端口以及所用的協議。這樣,連接的兩端分別用地址+端口,再加上所使用的協議,一個tuple就可以唯一地標識一個連接了(對於沒有端口的icmp協議,則用其它東東標識)。
-
連接記錄
那麼真正的完整連接記錄則是由數據結構ip_conntrack(位於include/linux/netfilter_ipv4/ip_conntrack.h,Line160)來描述的,其成員有:
-
`struct nf_conntrack ct_general;`:nf_conntrack結構定義於include/linux/skbuff.h,Line89,其中包括一個計數器use和一個destroy函數。計數器use對本連接記錄的公開引用次數進行計數
-
`struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];`:其中的IP_CT_DIR_MAX是一個枚舉類型ip_conntrack_dir(位於include/linux/netfilter_ipv4/ip_conntrack_tuple.h,Line65)的第3個成員,從這個結構實例在源碼中的使用看來,實際上這是定義了兩個tuple多元組的hash表項tuplehash[IP_CT_DIR_ORIGINAL/0]和tuplehash[IP_CT_DIR_REPLY/1],利用兩個不同方向的tuple定位一個連接,同時也可以方便地對ORIGINAL以及REPLY兩個方向進行追溯
-
`unsigned long status;`:這是一個位圖,是一個狀態域。在實際的使用中,它通常與一個枚舉類型ip_conntrack_status(位於include/linux/netfilter_ipv4/ip_conntrack.h,Line33)進行位運算來判斷連接的狀態。其中主要的狀態包括:
-
IPS_EXPECTED(_BIT),表示一個預期的連接
-
IPS_SEEN_REPLY(_BIT),表示一個雙向的連接
-
IPS_ASSURED(_BIT),表示這個連接即使發生超時也不能提早被刪除
-
IPS_CONFIRMED(_BIT),表示這個連接已經被確認(初始包已經發出)
-
-
`struct timer_list timeout;`:其類型timer_list位於include/linux/timer.h,Line11,其核心是一個處理函數。這個成員表示當發生連接超時時,將調用此處理函數
-
`struct list_head sibling_list;`:所謂“預期的連接”的鏈表,其中存放的是我們所期望的其它相關連接
-
`unsigned int expecting;`:目前的預期連接數量
-
`struct ip_conntrack_expect *master;`:結構ip_conntrack_expect位於include/linux/netfilter_ipv4/ip_conntrack.h,Line119,這個結構用於將一個預期的連接分配給現有的連接,也就是說本連接是這個master的一個預期連接
-
`struct ip_conntrack_helper *helper;`:helper模塊。這個結構定義於include/linux/netfilter_ipv4/ip_conntrack_helper.h,Line11,這個模塊提供了一個可以用於擴展Conntrack功能的接口。經過連接跟蹤HOOK的每個數據報都將被髮給每個已經註冊的helper模塊(註冊以及卸載函數分別爲ip_conntrack_helper_register()以及ip_conntrack_helper_unregister(),分別位於net/ipv4/netfilter/ip_conntrack_core.c,Line1136、1159)。這樣我們就可以進行一些動態的連接管理了
-
`struct nf_ct_info infos[IP_CT_NUMBER];`:一系列的nf_ct_info類型(定義於include/linux/skbuff.h ,Line92,實際上就是nf_conntrack結構)的結構,每個結構對應於某種狀態的連接。這一系列的結構會被sk_buff結構的nfct指針所引用,描述了所有與此連接有關係的數據報。其狀態由枚舉類型ip_conntrack_info定義(位於include/linux/netfilter_ipv4/ip_conntrack.h,Line12),共有5個成員:
-
IP_CT_ESTABLISHED:數據報屬於已經完全建立的連接
-
IP_CT_RELATED: 數據報屬於一個新的連接,但此連接與一個現有連接相關(預期連接);或者是ICMP錯誤
-
IP_CT_NEW:數據報屬於一個新的連接
-
IP_CT_IS_REPLY:數據報屬於一個連接的回覆
-
IP_CT_NUMBER:不同IP_CT類型的數量,這裏爲7,NEW僅存於一個方向上
-
-
爲NAT模塊設置的信息(在條件編譯中)
-
hash表
Netfilter使用一個hash表來對連接記錄進行管理,這個hash表的初始指針爲*ip_conntrack_hash,位於net/ipv4/netfilter/ip_conntrack_core.c,Line65,這樣我們就可以使用ip_conntrack_hash[num]的方式來直接定位某個連接記錄了。
而hash表的每個表項則由數據結構ip_conntrack_tuple_hash(位於include/linux/netfilter_ipv4/ip_conntrack_tuple.h,Line86)描述 :
struct ip_conntrack_tuple_hash
{
struct list_head list;
struct ip_conntrack_tuple tuple;
struct ip_conntrack *ctrack;
};
可見,一個hash表項中實質性的內容就是一個多元組ip_conntrack_tuple;同時還有一個指向連接的ip_conntrack結構的指針;以及一個鏈表頭(這個鏈表頭不知是幹嘛的)。
3. 連接跟蹤的實現
有了以上的數據結構,連接跟蹤的具體實現其實就非常簡單而常規了,無非就是初始化、連接記錄的創建、在連接跟蹤hash表中搜索並定位數據報、將數據報轉換爲一個多元組、判斷連接的狀態以及方向、超時處理、協議的添加、查找和註銷、對不同協議的不同處理、以及在兩個連接跟蹤相關的HOOK上對數據報的處理等。
下面重點說明一下我在分析中遇到的幾個比較重要或者比較難理解的地方:
- 所謂“預期鏈接”
可以將預期連接看作父子關係來理解,如圖 http://blog.chinaunix.net/photo/24896_061206192612.jpg
- ip_conntrack的狀態轉換
ip_conntrack的狀態轉換分兩種,同樣用圖來描述。首先是正常的狀態轉換,如圖http://blog.chinaunix.net/photo/24896_061206192631.jpg,
然後是ICMP error時的狀態轉換(由函數icmp_error_track()來判斷,位於net/ipv4/netfilter/ip_conntrack_core.c,Line495),
如圖http://blog.chinaunix.net/photo/24896_061206192648.jpg
- 在NF_IP_PRE_ROUTING上對分片IP數據報的處理
在經過HOOK中的NF_IP_PRE_ROUTING時(函數ip_conntrack_in(),位於net/ipv4/netfilter/ip_conntrack_core.c,Line796),由於外來的數據報有可能是經過分片的,所以必須對分片的情形進行處理,將IP數據報組裝後才能分配給連接。
具體的操作是首先由函數ip_ct_gather_frags()對分片的數據報進行收集,然後調用ip_defrag()函數(位於net/ipv4/ip_fragment.c,Line632)組裝之
4. 協議的擴展
由於我們可能要添加新的協議,所以單獨對協議的擴展進行分析。
各種協議使用一個全局的協議列表存放,即protocol_list(位於include/linux/netfilter_ipv4/ip_conntrack_core.h,Line21),使用結構ip_conntrack_protocol(位於include/linux/netfilter_ipv4/ip_conntrack_protocol.h,Line6)來表示:
struct ip_conntrack_protocol
{
struct list_head list;
u_int8_t proto; //協議號
const char *name;
int (*pkt_to_tuple)(const void *datah, size_t datalen,
struct ip_conntrack_tuple *tuple);
int (*invert_tuple)(struct ip_conntrack_tuple *inverse,
const struct ip_conntrack_tuple *orig);
unsigned int (*print_tuple)(char *buffer,
const struct ip_conntrack_tuple *);
unsigned int (*print_conntrack)(char *buffer,
const struct ip_conntrack *);
int (*packet)(struct ip_conntrack *conntrack,
struct iphdr *iph, size_t len,
enum ip_conntrack_info ctinfo);
int (*new)(struct ip_conntrack *conntrack, struct iphdr *iph,
size_t len);
void (*destroy)(struct ip_conntrack *conntrack);
int (*exp_matches_pkt)(struct ip_conntrack_expect *exp,
struct sk_buff **pskb);
struct module *me;
};
其中重要成員:
-
`int (*pkt_to_tuple)(……)`:其指向函數的作用是將協議加入到ip_conntrack_tuple的dst子結構中
-
`int (*invert_tuple)(……)`:其指向函數的作用是將源和目的多元組中協議部分的值進行互換,包括IP地址、端口等
-
`unsigned int (*print_tuple)(……)`:其指向函數的作用是打印多元組中的協議信息
-
`unsigned int (*print_conntrack)(……)`:其指向函數的作用是打印整個連接記錄
-
`int (*packet)(……)`:其指向函數的作用是返回數據報的verdict值
-
`int (*new)(……)`:當此協議的一個新連接發生時,調用其指向的這個函數,調用返回true時再繼續調用packet()函數
-
`int (*exp_matches_pkt)(……)`:其指向函數的作用是判斷是否有數據報匹配預期的連接
添加/刪除協議使用的是函數ip_conntrack_protocol_register()以及ip_conntrack_protocol_unregister(),分別位於net/ipv4/netfilter/ip_conntrack_standalone.c,Line298 & 320,其工作就是將ip_conntrack_protocol添加到全局協議列表protocol_list。
七、網絡地址轉換模塊(Network Address Translation)
1. 概述
網絡地址轉換的機制一般用於處理IP地址轉換,在Netfilter中,可以支持多種NAT類型,而其實現的基礎是連接跟蹤。
NAT可以分爲SNAT和DNAT,即源NAT和目的NAT,在Netfilter中分別基於以下HOOK:
-
NF_IP_PRE_ROUTING:可以在這裏定義DNAT的規則,因爲路由器進行路由時只檢查數據報的目的IP地址,所以爲了使數據報得以正確路由,我們必須在路由之前就進行DNAT
-
NF_IP_POST_ROUTING:可以在這裏定義SNAT的規則,系統在決定了數據報的路由以後在執行該HOOK上的規則
-
NF_IP_LOCAL_OUT:定義對本地產生的數據報的DNAT規則
-
CONFIG_IP_NF_NAT_LOCAL定義後,NF_IP_LOCAL_IN上也可以定義DNAT規則。
同時,MASQUERADE(僞裝)是SNAT的一種特例,它與SNAT幾乎一樣,只有一點不同:如果連接斷開,所有的連接跟蹤信息將被丟棄,而去使用重新連接以後的IP地址進行IP僞裝;而REDIRECT(重定向)是DNAT的一種特例,這時候就相當於將符合條件的數據報的目的IP地址改爲數據報進入系統時的網絡接口的IP地址。
2. 基於連接跟蹤的相關數據結構
NAT是基於連接跟蹤實現的,NAT中所有的連接都由連接跟蹤模塊來管理,NAT模塊的主要任務是維護nat表和進行實際的地址轉換。這樣,我們來回頭重新審視一下連接跟蹤模塊中由條件編譯決定的部分。
首先,是連接的描述ip_conntrack,在連接跟蹤模塊部分中提到,這個結構的最後有“爲NAT模塊設置的信息”,即:
#ifdef CONFIG_IP_NF_NAT_NEEDED
struct {
struct ip_nat_info info;
union ip_conntrack_nat_help help;
#if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || /
defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)
int masq_index;
#endif
} nat;
#endif /* CONFIG_IP_NF_NAT_NEEDED */
這是一個叫做nat的子結構,其中有3個成員:
-
-
一個ip_nat_info結構,這個結構下面會具體分析
-
一個ip_conntrack_nat_help結構,是一個空結構,爲擴展功能而設
-
一個爲僞裝功能而設的index,從源碼中對這個變量的使用看來,是對應所僞裝網絡接口的ID,也就是net_device中的ifindex成員
-
好,下面我們來看一下這個ip_nat_info結構,這個結構存儲了連接中的地址綁定信息,其定義位於include/linux/netfilter_ipv4/ip_nat.h,Line98:
struct ip_nat_info
{
int initialized;
unsigned int num_manips;
struct ip_nat_info_manip manips[IP_NAT_MAX_MANIPS];
const struct ip_nat_mapping_type *mtype;
struct ip_nat_hash bysource, byipsproto;
struct ip_nat_helper *helper;
struct ip_nat_seq seq[IP_CT_DIR_MAX];
};
-
`int initialized;`:這是一個位圖,表明源地址以及目的地址的地址綁定是否已被初始化
-
`unsigned int num_manips;`:這個成員指定了存放在下面的manip數組中的可執行操作的編號,在不同的HOOK以及不同的方向上,可執行操作是分別進行計數的。
-
`struct ip_nat_info_manip manips[IP_NAT_MAX_MANIPS];`:ip_nat_info_manip結構定義於include/linux/netfilter_ipv4/ip_nat.h,Line66,一個ip_nat_info_manip結構對應着一個可執行操作或地址綁定,其成員包括方向(ORIGINAL以及REPLY)、HOOK號、操作類型(由一個枚舉類型ip_nat_manip_type定義,有IP_NAT_MANIP_SRC和IP_NAT_MANIP_DST兩種)以及一個ip_conntrack_manip結構
-
`const struct ip_nat_mapping_type *mtype;`:ip_nat_mapping_type這個結構在整個內核源碼中都沒有定義,根據註釋來看應該也是一個預留的擴展,一般就是NULL
-
`struct ip_nat_hash bysource, byipsproto;`:ip_nat_hash結構定義於include/linux/netfilter_ipv4/ip_nat.h,Line89,實際上就是一個帶表頭的ip_conntrack結構,跟連接跟蹤中hash表的實現類似。其中,
-
bysource表是用來管理現有連接的
-
byipsproto表則管理已經完成的轉換映射,以保證同一個IP不會同時有兩個映射,避免地址轉換衝突
-
-
`struct ip_nat_helper *helper;`:擴展用
-
`struct ip_nat_seq seq[IP_CT_DIR_MAX];`:這是爲每一個方向(其實就兩個方向)記錄一個序列號。ip_nat_seq結構定義於include/linux/netfilter_ipv4/ip_nat.h,Line33,這個結構用得並不多,應該是用於TCP連接的計數和對涉及TCP的修改的定位
3. nat表的實現
nat表的初始化和實現與filter極爲相似。
在初始化上,其初始化位於net/ipv4/netfilter/ip_nat_rule.c,Line104,初始化所用模板則位於net/ipv4/netfilter/ip_nat_rule.c,Line50。
在實現上,其實現函數就是NAT模塊的初始化函數init_or_cleanup()(位於net/ipv4/netfilter/ip_nat_standalone.c,Line278)。其工作主要是依次調用ip_nat_rule_init()、ip_nat_init()以及nf_register_hook()。
-
首先ip_nat_rule_init()(位於net/ipv4/netfilter/ip_nat_rule.c,Line278)調用ipt_register_table()來初始化並註冊nat表,然後利用ipt_register_target()來初始化並註冊SNAT和DNAT,在這個註冊過程中,關鍵的函數是ip_nat_setup_info(位於net/ipv4/netfilter/ip_nat_core.c,Line511),其工作是:
-
-
首先調用invert_tupler()(net/ipv4/netfilter/ip_conntrack_core.c,Line879),將記錄反轉
-
然後調用get_unique_tuple()(net/ipv4/netfilter/ip_nat_core.c,Line393,在指定的地址範圍(ip_nat_multi_range結構)中查找空閒的地址),如果沒有空閒地址可用則會返回NF_DROP
-
判斷源和目的是否改變,如果改變,則更新ip_nat_info。
-
-
然後ip_nat_init()(位於net/ipv4/netfilter/ip_nat_core.c,Line953)會給nat所用的兩個hash表(bysource、byipsproto)分配空間並初始化各種協議
-
最後會通過nf_register_hook()註冊相應HOOK的函數ip_nat_fn()、ip_nat_local_fn()和ip_nat_out(),並增加連接跟蹤的計數器。
在具體的HOOK函數實現上,後兩者其實都是基於ip_nat_fn()的,而這其中最重要的處理函數,也就是實際的網絡地址轉換函數是do_bindings(),下面將對其進行分析:
do_bindings()位於net/ipv4/netfilter/ip_nat_core.c,Line747,其主要工作是將ip_nat_info中的地址綁定應用於數據報:
-
它首先在ip_nat_info->manip數組中查找能夠匹配的綁定
-
然後調用manip_pkt()函數(位於net/ipv4/netfilter/ip_nat_core.c,Line701)進行相應的地址轉換:
-
-
manip_pkt()這個函數會根據不同的方向進行轉換,並且對校驗和進行處理
-
同時,它是遞歸調用自己以處理不同協議的情況
-
-
最後調用helper模塊進行執行(當然,如果有的話),特別是避免在同一個數據報上執行多次同一個helper模塊
nat表與filter表還有一個較大的不同:在一個連接中,只有第一個數據報纔會經過nat表,而其轉換的結果會作用於此連接中的其它所有數據報。
4. 協議的擴展
要想擴展NAT的協議,那麼必須寫入兩個模塊,一個用於連接跟蹤,一個用於NAT實現。
與連接跟蹤類似,nat中協議也由一個列表protos存放,位於include/linux/netfilter_ipv4/ip_nat_core.h,Line17。協議的註冊和註銷分別是由函數ip_nat_protocol_register()和ip_nat_protocol_unregister()實現的,位於net/ipv4/netfilter/ip_nat_standalone.c,Line242。
nat的協議是由結構ip_nat_protocol描述的,其定義位於include/linux/netfilter_ipv4/ip_nat_protocol.h,Line10:
struct ip_nat_protocol
{
struct list_head list;
const char *name;
unsigned int protonum;
void (*manip_pkt)(struct iphdr *iph, size_t len,
const struct ip_conntrack_manip *manip,
enum ip_nat_manip_type maniptype);
int (*in_range)(const struct ip_conntrack_tuple *tuple,
enum ip_nat_manip_type maniptype,
const union ip_conntrack_manip_proto *min,
const union ip_conntrack_manip_proto *max);
int (*unique_tuple)(struct ip_conntrack_tuple *tuple,
const struct ip_nat_range *range,
enum ip_nat_manip_type maniptype,
const struct ip_conntrack *conntrack);
unsigned int (*print)(char *buffer,
const struct ip_conntrack_tuple *match,
const struct ip_conntrack_tuple *mask);
unsigned int (*print_range)(char *buffer,
const struct ip_nat_range *range);
};
其中重要成員:
-
`void (*manip_pkt)(……)`:其指向的函數會根據ip_nat_info->manip參數進行數據報的轉換,即do_bindings()中調用的manip_pkt()函數
-
`int (*in_range)(……)`:其指向的函數檢查多元組的協議部分值是否在指定的區間之內
-
`int (*unique_tuple)(……)`:其指向函數的作用是根據manip的類型修改多元組中的協議部分,以獲得一個唯一的地址,多元組的協議部分被初始化爲ORIGINAL方向
八、數據報修改模塊──mangle表
1. 概述
mangle這個詞的原意是撕裂、破壞,這裏所謂“packet mangling”是指對packet的一些傳輸特性進行修改。mangle表被用來真正地對數據報進行修改,它可以在所有的5個HOOK上進行操作。
從源碼看來,在mangle表中所允許修改的傳輸特性目前有:
-
TOS(服務類型):修改IP數據報頭的TOS字段值
-
TTL(生存時間):修改IP數據報頭的TTL字段值
-
MARK:修改skb的nfmark域設置的nfmark字段值。
-
nfmark是數據報的元數據之一,是一個用戶定義的數據報的標記,可以是unsigned long範圍內的任何值。該標記值用於基於策略的路由,通知ipqmpd(運行在用戶空間的隊列分揀器守護進程)將該數據報排隊給哪個進程等信息。
-
-
TCP MSS(最大數據段長度):修改TCP數據報頭的MSS字段值
2. mangle表的實現
遍歷整個源碼,沒有發現mangle表的獨有數據結構。
mangle表的初始化和實現與filter極爲相似。在初始化上,其初始化位於net/ipv4/netfilter/iptable_mangle.c,Line117,初始化所用模板則位於net/ipv4/netfilter/iptable_mangle.c,Line43。
在實現上,其實現函數就是mangle模塊的初始化函數init()(位於net/ipv4/netfilter/iptable_mangle.c,Line183)。其工作就是依次註冊packet_mangler表以及5個HOOK。其中NF_IP_LOCAL_OUT的處理函數爲ipt_local_hook(),其餘均爲ipt_route_hook(),分別位於net/ipv4/netfilter/iptable_mangle.c,Line132 & 122,二者的關鍵實現都是通過調用ipt_do_table()實現的。
3. 數據報的修改
對數據報不同位的修改都是通過單獨的模塊實現的,也就是說由ipt_tos.o、ipt_ttl.o、ipt_mark.o、ipt_tcpmss.o實現上面所說的四種傳輸特性的修改。
以TOS爲例,其涉及的文件爲net/ipv4/netfilter/ipt_tos.c以及include/linux/netfilter_ipv4/ipt_tos.h。
其專有數據結構爲ipt_tos_info,定義於ipt_tos.h中:
struct ipt_tos_info {
u_int8_t tos;
u_int8_t invert;
};
其模塊的添加/卸載函數很簡單,其實就是添加/刪除TOS的MATCH:tos_match(定義並初始化於ipt_tos.c,Line37):
static struct ipt_match tos_match
= { { NULL, NULL }, "tos", &match, &checkentry, NULL, THIS_MODULE };
而在tos_match的處理函數match()中,已經完成了對相應位的賦值,這似乎是違反match僅僅匹配而不修改的一個特例。
九、其它高級功能模塊
Netfilter中還有一些其它的高級功能模塊,基本是爲了用戶操作方便的,沒有對它們進行分析,如:
-
REJECT,丟棄包並通知包的發送者,同時返回給發送者一個可配置的ICMP錯誤信息,由ipt_REJECT.o完成
-
MIRROR,互換源和目的地址以後並重新發送,由ipt_MIRROR.o完成
-
LOG, 將匹配的數據報傳遞給系統的syslog()進行記錄,由ipt_LOG.o完成
-
ULOG,Userspace logging,將數據報排隊轉發到用戶空間中,將匹配的數據適用用戶空間的log進程進行記錄,由ip_ULOG.o完成。這是Netfilter的一個關鍵技術,可以使用戶進程可以進行復雜的數據報操作,從而減輕內核空間中的複雜度
-
Queuing,這是上面ULOG技術的基礎,由ip_queue.o完成,提供可靠的異步包處理以及性能兩號的libipq庫來進行用戶空間數據報操作的開發。
-
等等……