本人研究linux的防火牆系統也有一段時間了,由於近來涉及到的工作比較紛雜,久而久之怕生熟了。趁有時間,好好把這方面的東西總結一番。一來是給自己做個沉澱,二來也歡迎這方面比較牛的前輩給小弟予以指點,共同學習,共同進步。 能在CU上混的人絕非等閑之輩。因此,小弟這裡說明一下:本系列博文主要側重於分析N ...
本人研究linux的防火牆系統也有一段時間了,由於近來涉及到的工作比較紛雜,久而久之怕生熟了。趁有時間,好好把這方面的東西總結一番。一來是給自己做個沉澱,二來也歡迎這方面比較牛的前輩給小弟予以指點,共同學習,共同進步。
能在CU上混的人絕非等閑之輩。因此,小弟這裡說明一下:本系列博文主要側重於分析Netfilter的實現機制,原理和設計思想層面的東西,同時從用戶態的iptables到內核態的Netfilter其交互過程和通信手段等。至於iptables的入門用法方面的東西,網上隨便一搜羅就有一大堆,我這裡不浪費筆墨了。
很多人在接觸iptables之後就會這麼一種感覺:我通過iptables命令配下去的每一條規則,到底是如何生效的呢?內核又是怎麼去執行這些規則匹配呢?如果iptables不能滿足我當下的需求,那麼我是否可以去對其進行擴展呢?這些問題,都是我在接下來的博文中一一和大家分享的話題。這裡需要指出:因為Netfilter與IP協議棧是無縫契合的,所以如果你要是有協議棧方面的基礎,在閱讀本文時一定會感覺輕車熟路。當然,如果沒有也沒關係,因為我會在關鍵點就協議棧的入門知識給大家做個普及。只是普及哦,不會詳細深入下去的,因為涉及的東西太多了,目前我還正在研究摸索當中呢。好了,廢話不多說,進入正題。
備註:我研究的內核版本是2.6.21,iptables的版本1.4.0。
什麼是Netfilter?
為了說明這個問題,首先看一個網路通信的基本模型:
在數據的發送過程中,從上至下依次是“加頭”的過程,每到達一層數據就被會加上該層的頭部;與此同時,接受數據方就是個“剝頭”的過程,從網卡收上包來之後,在往協議棧的上層傳遞過程中依次剝去每層的頭部,最終到達用戶那兒的就是裸數據了。
那麼,“棧”模式底層機制基本就是像下麵這個樣子:
對於發送的每個數據包,首先也有一個路由判決,以確定該包是從哪個介面出去,然後經過“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框架返回下列幾個值其中之一:
- NF_ACCEPT 繼續正常傳輸數據報。這個返回值告訴 Netfilter:到目前為止,該數據包還是被接受的並且該數據包應當被遞交到網路協議棧的下一個階段。
- NF_DROP 丟棄該數據報,不再傳輸。
- NF_STOLEN 模塊接管該數據報,告訴Netfilter“忘掉”該數據報。該回調函數將從此開始對數據包的處理,並且Netfilter應當放棄對該數據包做任何的處理。但是,這並不意味著該數據包的資源已經被釋放。這個數據包以及它獨自的sk_buff數據結構仍然有效,只是回調函數從Netfilter 獲取了該數據包的所有權。
- NF_QUEUE 對該數據報進行排隊(通常用於將數據報給用戶空間的進程進行處理)
- NF_REPEAT 再次調用該回調函數,應當謹慎使用這個值,以免造成死迴圈。
為了讓我們顯得更專業些,我們開始做些約定:上面提到的五個關鍵點後面我們就叫它們為hook點,每個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各個參數的解釋說明:
- pf:協議族名,Netfilter架構同樣可以用於IP層之外,因此這個變數還可以有諸如PF_INET6,PF_DECnet等名字。
- hook:HOOK點的名字,對於IP層,就是取上面的五個值;
- skb:不解釋;
- indev:數據包進來的設備,以struct net_device結構表示;
- outdev:數據包出去的設備,以struct net_device結構表示;
(後面可以看到,以上五個參數將傳遞給nf_register_hook中註冊的處理函數。) - 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點所有的鉤子函數的相關操作有如下幾處:
- 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函數所調用,然後根據其返回值做進一步處理。
- 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函數的執行情況。
- 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));
如果需要陷入Netfilter框架則數據包會在nf_hooks[2][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函數。
- 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所指向的函數。