什么是Netfilter?
為了說明這個問題,首先看一個網絡通信的基本模型:
在數據的發送過程中,從上至下依次是“加頭”的過程,每到達一層數據就被會加上該層的頭部;與此同時,接受數據方就是個“剝頭”的過程,從網卡收上包來之后,在往協議棧的上層傳遞過程中依次剝去每層的頭部,最終到達用戶那兒的就是裸數據了。
那么,“棧”模式底層機制基本就是像下面這個樣子:
對於收到的每個數據包,都從“A”點進來,經過路由判決,如果是發送給本機的就經過“B”點,然后往協議棧的上層繼續傳遞;否則,如果該數據包的目的地是不本機,那么就經過“C”點,然后順着“E”點將該包轉發出去。
對於發送的每個數據包,首先也有一個路由判決,以確定該包是從哪個接口出去,然后經過“D”點,最后也是順着“E”點將該包發送出去。
協議棧那五個關鍵點A,B,C,D和E就是我們Netfilter大展拳腳的地方了。
Netfilter是Linux 2.4.x引入的一個子系統,它作為一個通用的、抽象的框架,提供一整套的hook函數的管理機制,使得諸如數據包過濾、網絡地址轉換(NAT)和基於協議類型的連接跟蹤成為了可能。Netfilter在內核中位置如下圖所示:
這幅圖,很直觀的反應了用戶空間的iptables和內核空間的基於Netfilter的ip_tables模塊之間的關系和其通訊方式,以及Netfilter在這其中所扮演的角色。
回到前面討論的關於協議棧那五個關鍵點“ABCDE”上來。Netfilter在netfilter_ipv4.h中將這個五個點重新命了個名,如下圖所示,意思我就不再解釋了,貓叫咪咪而已:
在每個關鍵點上,有很多已經按照優先級預先注冊了的回調函數(后面再說這些函數是什么,干什么用的。有些人喜歡把這些函數稱為“鈎子函數”,說的是同一個東西)埋伏在這些關鍵點,形成了一條鏈。對於每個到來的數據包會依次被那些回調函數“調戲”一番再視情況是將其放行,丟棄還是怎么滴。但是無論如何,這些回調函數最后必須向Netfilter報告一下該數據包的死活情況,因為畢竟每個數據包都是Netfilter從人家協議棧那兒借調過來給兄弟們Happy的,別個再怎么滴也總得“活要見人,死要見屍”吧。每個鈎子函數最后必須向Netfilter框架返回下列幾個值其中之一:
n NF_ACCEPT 繼續正常傳輸數據報。這個返回值告訴 Netfilter:到目前為止,該數據包還是被接受的並且該數據包應當被遞交到網絡協議棧的下一個階段。
n NF_DROP 丟棄該數據報,不再傳輸。
n NF_STOLEN 模塊接管該數據報,告訴Netfilter“忘掉”該數據報。該回調函數將從此開始對數據包的處理,並且Netfilter應當放棄對該數據包做任何的處理。但是,這並不意味着該數據包的資源已經被釋放。這個數據包以及它獨自的sk_buff數據結構仍然有效,只是回調函數從Netfilter 獲取了該數據包的所有權。
n NF_QUEUE 對該數據報進行排隊(通常用於將數據報給用戶空間的進程進行處理)
n NF_REPEAT 再次調用該回調函數,應當謹慎使用這個值,以免造成死循環。
為了讓我們顯得更專業些,我們開始做些約定:上面提到的五個關鍵點后面我們就叫它們為hook點,每個hook點所注冊的那些回調函數都將其稱為hook函數。
Linux 2.6版內核的Netfilter目前支持IPv4、IPv6以及DECnet等協議棧,這里我們主要研究IPv4協議。關於協議類型,hook點,hook函數,優先級,通過下面這個圖給大家做個詳細展示:
對於每種類型的協議,數據包都會依次按照hook點的方向進行傳輸,每個hook點上Netfilter又按照優先級掛了很多hook函數。這些hook函數就是用來處理數據包用的。
Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在協議棧內部切入到Netfilter框架中。相比於2.4版本,2.6版內核在該宏的定義上顯得更加靈活一些,定義如下:
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)
關於宏NF_HOOK各個參數的解釋說明:
1) pf:協議族名,Netfilter架構同樣可以用於IP層之外,因此這個變量還可以有諸如PF_INET6,PF_DECnet等名字。
2) hook:HOOK點的名字,對於IP層,就是取上面的五個值;
3) skb:不解釋;
4) indev:數據包進來的設備,以struct net_device結構表示;
5) outdev:數據包出去的設備,以struct net_device結構表示;
(后面可以看到,以上五個參數將傳遞給nf_register_hook中注冊的處理函數。)
6) okfn:是個函數指針,當所有的該HOOK點的所有登記函數調用完后,轉而走此流程。
而NF_HOOK_THRESH又是一個宏:
#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \
({int __ret; \
if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, thresh, 1)) == 1)\
__ret = (okfn)(skb); \
__ret;})
我們發現NF_HOOK_THRESH宏只增加了一個thresh參數,這個參數就是用來指定通過該宏去遍歷鈎子函數時的優先級,同時,該宏內部又調用了nf_hook_thresh函數:
static inline int nf_hook_thresh(int pf, unsigned int hook, struct sk_buff **pskb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct sk_buff *), int thresh, int cond) { if (!cond) return 1; #ifndef CONFIG_NETFILTER_DEBUG if (list_empty(&nf_hooks[pf][hook])) return 1; #endif return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh); } |
這個函數又只增加了一個參數cond,該參數為0則放棄遍歷,並且也不執行okfn函數;為1則執行nf_hook_slow去完成鈎子函數okfn的順序遍歷(優先級從小到大依次執行)。
在net/netfilter/core.h文件中定義了一個二維的結構體數組,用來存儲不同協議棧鈎子點的回調處理函數。
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
其中,行數NPROTO為32,即目前內核所支持的最大協議簇;列數NF_MAX_HOOKS為掛載點的個數,目前在2.6內核中該值為8。nf_hooks數組的最終結構如下圖所示。
在include/linux/socket.h中IP協議AF_INET(PF_INET)的序號為2,因此我們就可以得到TCP/IP協議族的鈎子函數掛載點為:
PRE_ROUTING: nf_hooks[2][0]
LOCAL_IN: nf_hooks[2][1]
FORWARD: nf_hooks[2][2]
LOCAL_OUT: nf_hooks[2][3]
POST_ROUTING: nf_hooks[2][4]
同時我們看到,在2.6內核的IP協議棧里,從協議棧正常的流程切入到Netfilter框架中,然后順序、依次去調用每個HOOK點所有的鈎子函數的相關操作有如下幾處:
1)、net/ipv4/ip_input.c里的ip_rcv函數。該函數主要用來處理網絡層的IP報文的入口函數,它到Netfilter框架的切入點為:
NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,ip_rcv_finish)
根據前面的理解,這句代碼意義已經很直觀明確了。那就是:如果協議棧當前收到了一個IP報文(PF_INET),那么就把這個報文傳到Netfilter的NF_IP_PRE_ROUTING過濾點,去檢查[R]在那個過濾點(nf_hooks[2][0])是否已經有人注冊了相關的用於處理數據包的鈎子函數。如果有,則挨個去遍歷鏈表nf_hooks[2][0]去尋找匹配的match和相應的target,根據返回到Netfilter框架中的值來進一步決定該如何處理該數據包(由鈎子模塊處理還是交由ip_rcv_finish函數繼續處理)。
[R]:剛才說到所謂的“檢查”。其核心就是nf_hook_slow()函數。該函數本質上做的事情很簡單,根據優先級查找雙向鏈表nf_hooks[][],找到對應的回調函數來處理數據包:
struct list_head **i;
list_for_each_continue_rcu(*i, head) {
struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
if (hook_thresh > elem->priority)
continue;
verdict = elem->hook(hook, skb, indev, outdev, okfn);
if (verdict != NF_ACCEPT) { … … }
return NF_ACCEPT;
}
上面的代碼是net/netfilter/core.c中的nf_iterate()函數的部分核心代碼,該函數被nf_hook_slow函數所調用,然后根據其返回值做進一步處理。
2)、net/ipv4/ip_forward.c中的ip_forward函數,它的切入點為:
NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev,ip_forward_finish);
在經過路由抉擇后,所有需要本機轉發的報文都會交由ip_forward函數進行處理。這里,該函數由NF_IP_FOWARD過濾點切入到Netfilter框架,在nf_hooks[2][2]過濾點執行匹配查找。最后根據返回值來確定ip_forward_finish函數的執行情況。
3)、net/ipv4/ip_output.c中的ip_output函數,它切入Netfilter框架的形式為:
NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
這里我們看到切入點從無條件宏NF_HOOK改成了有條件宏NF_HOOK_COND,調用該宏的條件是:如果協議棧當前所處理的數據包skb中沒有重新路由的標記,數據包才會進入Netfilter框架。否則直接調用ip_finish_output函數走協議棧去處理。除此之外,有條件宏和無條件宏再無其他任何差異。
如果需要陷入Netfilter框架則數據包會在nf_hooks[2][4]過濾點去進行匹配查找。
4)、還是在net/ipv4/ip_input.c中的ip_local_deliver函數。該函數處理所有目的地址是本機的數據包,其切入函數為:
NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish);
發給本機的數據包,首先全部會去nf_hooks[2][1]過濾點上檢測是否有相關數據包的回調處理函數,如果有則執行匹配和動作,最后根據返回值執行ip_local_deliver_finish函數。
5)、net/ipv4/ip_output.c中的ip_push_pending_frames函數。該函數是將IP分片重組成完整的IP報文,然后發送出去。進入Netfilter框架的切入點為:
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output);
對於所有從本機發出去的報文都會首先去Netfilter的nf_hooks[2][3]過濾點去過濾。一般情況下來來說,不管是路由器還是PC中端,很少有人限制自己機器發出去的報文。因為這樣做的潛在風險也是顯而易見的,往往會因為一些不恰當的設置導致某些服務失效,所以在這個過濾點上攔截數據包的情況非常少。當然也不排除真的有特殊需求的情況。
小節:整個Linux內核中Netfilter框架的HOOK機制可以概括如下:
在數據包流經內核協議棧的整個過程中,在一些已預定義的關鍵點上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING會根據數據包的協議簇PF_INET到這些關鍵點去查找是否注冊有鈎子函數。如果沒有,則直接返回okfn函數指針所指向的函數繼續走協議棧;如果有,則調用nf_hook_slow函數,從而進入到Netfilter框架中去進一步調用已注冊在該過濾點下的鈎子函數,再根據其返回值來確定是否繼續執行由函數指針okfn所指向的函數。