洞悉linux下的Netfilteriptables
原網址:http://blog.chinaunix.net/uid-23069658-id-3160506.html
(一)洞悉linux下的Netfilter&iptables:什么是Netfilter?
很多人在接觸iptables之后就會這么一種感覺:我通過iptables命令配下去的每一條規則,到底是如何生效的呢?內核又是怎么去執行這些規則匹配呢?如果iptables不能滿足我當下的需求,那么我是否可以去對其進行擴展呢?這些問題,都是我在接下來的博文中一一和大家分享的話題。這里需要指出:因為Netfilter與IP協議棧是無縫契合的,所以如果你要是有協議棧方面的基礎,在閱讀本文時一定會感覺輕車熟路。當然,如果沒有也沒關系,因為我會在關鍵點就協議棧的入門知識給大家做個普及。只是普及哦,不會詳細深入下去的,因為涉及的東西太多了,目前我還正在研究摸索當中呢。好了,廢話不多說,進入正題。
備注:我研究的內核版本是2.6.21,iptables的版本1.4.0。
?
什么是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所指向的函數。
(二)洞悉linux下的Netfilter&iptables:內核中的ip_tables小覷
Netfilter框架為內核模塊參與IP層數據包處理提供了很大的方便,內核的防火墻模塊(ip_tables)正是通過把自己所編寫的一些鉤子函數注冊到Netfilter所監控的五個關鍵點(NF_IP_PRE_ROUTING,
NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING)這種方式介入到對數據包的處理。這些鉤子函數功能非常強大,按功能可分為四大類:連接跟蹤、數據包的過濾、網絡地址轉換(NAT)和數據包的修改。它們之間的關系,以及和Netfilter、ip_tables難舍難分的纏綿可以用下圖來表示:從上圖我們可以看出,ip_tables模塊它是防火墻的核心模塊,負責維護防火墻的規則表,通過這些規則,實現防火墻的核心功能。歸納起來,主要有三種功能:包過濾(filter)、NAT以及包處理(mangle)。同進該模塊留有與用戶空間通訊的接口。如第一篇博文中Netfilter處于內核中位置那副圖所描述的情形。
在內核中我們習慣將上述的filter,nat和mangle等稱之為模塊。連接跟蹤conntrack有些特殊,它是NAT模塊和狀態防火墻的功能基礎,其實現機制我們也會在后面詳細分析的。
OK,回到開篇的問題,我們來看一下基于Netfilter的防火墻系統到底定義了哪些鉤子函數?而這些鉤子函數都是分別掛載在哪些hook點的?按照其功能結構劃分,我將這些hook函數總結如下:
包過濾子功能:包過濾一共定義了四個hook函數,這四個hook函數本質最后都調用了ipt_do_table()函數。
網絡地址轉換子功能:該模塊也定義了四個hook函數,其中有三個最終也都調用了ip_nat_fn()函數,ip_nat_adjust()有自己另外的功能。
連接跟蹤子功能:這里連接跟蹤應該稱其為一個子系統更合適些。它也定義四個hook函數,其中ip_conntrack_local()最后其實也調用了ip_conntrack_in()函數。
以上便是Linux的防火墻---iptables在內核中定義的所有hook函數。接下來我們再梳理一下這些hook函數分別是被掛載在哪些hook點上的。還是先貼個三維框圖,因為我覺得這個圖是理解Netfilter內核機制最有效,最直觀的方式了,所以屢用不爽!然后,我們拿一把大刀,從協議棧的IPv4點上順著hook點延伸的方向一刀切下去,就會得到一個平面,如上圖所示。前面這些hook函數在這個平面上的分布情況如下所示:
這幅圖徹底暴露了ip_tables內核模塊中那些hook函數在各個hook點分布情況。與此同時,這個圖還告訴了我們很多信息:所有由網卡收上來的數據包率先被ip_conntrack_defrag處理;鏈接跟蹤系統的入口函數以-200的優先級被注冊到了PRE_ROUTING和LOCAL_OUT兩個hook點上,且其優先級高于mangle操作,NAT和包過濾等其他模塊;DNAT可以在PRE_ROUTING和LOCAL_OUT兩個hook點來做,SNAT可以在LOCAL_IN和POST_ROUTING兩個hook點上。如果你認真研究會發現這個圖確實很有用。因為當初為了畫這個圖我可是兩個晚上沒睡好覺啊,畫出來后還要驗證自己的想法,就得一步一步給那些關鍵的hook點和hook函數分別加上調試打印信息,重新編譯內核然后確認這些hook函數確實是按照我所分析的那樣被調用的。因為對學術嚴謹就是對自己負責,一直以來我也都這么堅信的。“沒有調查就沒發言權”;在我們IT行業,“沒有親自動手做過就更沒有發言權”。又扯遠了,趕緊收回來。
? ? 框架的東西多看些之上從宏觀上可以使我們對整個系統的架構和設計有個比較全面的把握,接下來在分析每個細節的時候才會做到心中有數,不至于“盲人摸象”的境地。在本章即將結束之際,我們來看點代碼級的東西。我保證只是個簡單的入門了解,因為重頭戲我打算放到后面,大家也知道分析代碼其實是最頭疼的,關鍵還是看自己的心態。
?
Netfilter的實現方式:
? ? 第一篇我們講了Netfilter的原理,這里我們談談其實現機制的問題。
我們回頭分析一下那個用于存儲不同協議簇在每個hook點上所注冊的hook函數鏈的二維數組?nf_hooks[][],其類型為list_head:
????struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
? ? list_head{}結構體定義在include/linux/list.h頭文件中
struct list_head {
? ? ? ? ? ?structlist_head *next, *prev;
};
這是Linux內核中處理雙向鏈表的標準方式。當某種類型的數據結構需要被組織成雙向鏈表時,會在該數據結構的第一個字段放置一個list_head{}類型的成員。在后面的使用過程中可以通過強制類型轉換來實現雙向鏈表的遍歷操作。
在Netfilter中一個非常重要的數據結構是nf_hook_ops{}<include/linux/netfilter.h>:
| struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; struct module *owner; int pf; int hooknum; /* Hooks are ordered in ascending priority. */ int priority; }; |
對該結構體中的成員參數做一下解釋:
n? list:因為在一個HOOK點有可能注冊多個鉤子函數,因此這個變量用來將某個HOOK點所注冊的所有鉤子函數組織成一個雙向鏈表;
n? hook:該參數是一個指向nf_hookfn類型的函數的指針,由該函數指針所指向的回調函數在該hook被激活時調用【nf_hookfn在后面做解釋】;
n? owner:表示這個hook是屬于哪個模塊的
n? pf:該hook函數所處理的協議。目前我們主要處理IPv4,所以該參數總是PF_INET;
n? hooknum:鉤子函數的掛載點,即HOOK點;
n? priority:優先級。前面也說過,一個HOOK點可能掛載了多個鉤子函數,當Netfilter在這些HOOK點上遍歷查找所注冊的鉤子函數時,這些鉤子函數的先后執行順序便由該參數來制定。
nf_hookfn所定義的回調函數的原型在include/linux/netfilter.h文件中:
| typedef unsigned int nf_hookfn(unsigned int hooknum,??? //HOOK點 ? ? ? struct sk_buff **skb,?????????????? //不解釋 ? ? ? const struct net_device *in,?????? ??//數據包的網絡如接口 ? ? ? const struct net_device *out,?????? //數據包的網絡出接口 ? ? ? ? ? int (*okfn)(struct sk_buff *));???? //后續的處理函數 |
我們可以到,上面這五個參數最后將由NF_HOOK或NF_HOOK_COND宏傳遞到Netfilter框架中去。
如果要增加新的鉤子函數到Netfilter中相應的過濾點,我們要做的工作其實很簡單:
1)、編寫自己的鉤子函數;
2)、實例化一個structnf_hook_ops{}結構,并對其進行適當的填充,第一個參數list并不是用戶所關心的,初始化時必須設置成{NULL,NULL};
3)、用nf_register_hook()<net/netfilter/core.c>函數將我們剛剛填充的nf_hook_ops結構體注冊到相應的HOOK點上,即nf_hooks[prot][hooknum]。
這也是最原生的擴展方式。有了上面這個對nf_hook_ops{}及其用法的分析,后面我們再分析其他模塊,如filter模塊、nat模塊時就會不那么難懂了。
內核在網絡協議棧的關鍵點引入NF_HOOK宏,從而搭建起了整個Netfilter框架。但是NF_HOOK宏僅僅只是一個跳轉而已,更重要的內容是“內核是如何注冊鉤子函數的呢?這些鉤子函數又是如何被調用的呢?誰來維護和管理這些鉤子函數?”
(三)洞悉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。
上面這幾個結構體的成員屬性基本上已經做到了“見名知意”,而且內核源碼也對它們做了充分的注解。這里只對最后一個屬性elem做一下說明,其定義為unsignedchar 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:
標準匹配主要用于匹配由structipt_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{}的關系如下:
(四)洞悉linux下的Netfilter&iptables:包過濾子系統iptable_filter
今天我們討論一下防火墻的數據包過濾模塊iptable_filter的設計原理及其實現方式。
????內核中將filter模塊被組織成了一個獨立的模塊<net/ipv4/netfilter/iptable_filter.c>,每個這樣獨立的模塊中都有個類似的init()初始化函數。編寫完該函數后,用module_init()宏調用初始化函數;同樣當模塊被卸載時調用module_exit()宏將該模塊卸載掉,該宏主要調用模塊的“析構”函數。這當中就牽扯到內核ko模塊的一些知識,但這并不妨礙我們理解。
?
? ? 整個filter模塊就一百多行代碼,但要將其理解清楚還是需要一些功夫。我們首先來看一下filter模塊是如何將自己的鉤子函數注冊到netfilter所管轄的幾個hook點的。?
| static int __init iptable_filter_init(void) { ? ? ? int ret; ??????if (forward < 0 || forward > NF_MAX_VERDICT) { ???????????????printk("iptables forward must be 0 or 1\n"); ???????????????return -EINVAL; ??????} ??????/* Entry 1 is the FORWARD hook */ ??????initial_table.entries[1].target.verdict = -forward - 1;
??????/* Register table */ ?????ret = ipt_register_table(&packet_filter, &initial_table.repl); ?????if (ret < 0) ???????????return ret;
?????/* Register hooks */ ?????ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops)); ?????if (ret < 0) ?????????goto cleanup_table; ?????return ret;
?cleanup_table: ?????ipt_unregister_table(&packet_filter); ?????return ret; } |
????這里我只看關鍵部分,根據上面的代碼我們已經知道。filter模塊初始化時先調用ipt_register_table向Netfilter完成filter過濾表的注冊,然后調用ipt_register_hooks完成自己鉤子函數的注冊,就這么簡單。至于這兩個注冊的動作分別都做了哪些東西,我們接下來詳細探究一下。
?
注冊過濾表:ipt_register_table(&packet_filter,&initial_table.repl);
????Netfilter在內核中為防火墻系統維護了一個結構體,該結構體中存儲的是內核中當前可用的所有match,target和table,它們都是以雙向鏈表的形式被組織起來的。這個全局的結構體變量staticstruct xt_af *xt定義在net/netfilter/x_tables.c當中,其結構為:
| struct xt_af { ???????? struct mutex mutex; ???????? struct list_head ?match; //每個match模塊都會被注冊到這里 ???????? struct list_head ?target; //每個target模塊都會被注冊到這里 ???????? struct list_head ?tables; //每張表都被被注冊到這里 ???????? struct mutex compat_mutex; }; |
? ? 其中xt變量是在net/netfilter/x_tables.c文件中的xt_init()函數中被分配存儲空間并完成初始化的,xt分配的大小以當前內核所能支持的協議簇的數量有關,其代碼如下:
初始化完成后xt的結構圖如下所示,這里我們只以IPv4協議為例加以說明:? ? 每注冊一張表,就會根據該表所屬的協議簇,找到其對應的xt[]成員,然后在其tables雙向鏈表中掛上該表結構即完成了表的注冊。接下來我們再看一下Netfilter是如何定義內核中所認識的“表”結構的。
????關于表結構,內核中有兩個結構體xt_table{}和xt_table_info{}來表示“表”的信息。
? ? structipt_table{}的結構體類型定義在<include/linux/netfilter/x_tables.h>中,它主要定義表自身的一些通用的基本信息,如表名稱,所屬的協議簇,所影響的hook點等等。
| struct xt_table??????????? //其中#defineipt_table xt_table { ?????struct list_head ?list; ?????char ?name[XT_TABLE_MAXNAMELEN];? //表的名字 ?????unsigned int ?valid_hooks;?? //該表所檢測的HOOK點 ?????rwlock_t ?lock;?? //讀寫鎖 ? ? ?void ?*private;?//描述表的具體屬性,如表的size,表中的規則數等 ?????struct module ?*me; //如果要設計成模塊,則為THIS_MODULE;否則為NULL ?????int ?af;??? ??????? //協議簇 ,如PF_INET(或PF_INET) }; |
而每張表中真正和規則相關的信息,則由該結構的的private屬性來指向。從2.6.18版內核開始,該變量被改成了void*類型,目的是方便日后對其進行擴充需要。通常情況下,private都指向一個xt_table_info{}類型的結構體變量。
????structxt_table_info{}的結構體類型定義在< include/linux/netfilter/x_tables.h >中。?
| struct xt_table_info { ????unsigned int ?size; //表的大小,即占用的內存空間 ????unsigned int ?number; //表中的規則數 ????unsigned int ?initial_entries; //初始的規則數,用于模塊計數 ? ????/* 記錄所影響的HOOK的規則入口相對于下面的entries變量的偏移量*/ ????unsigned int ?hook_entry[NF_IP_NUMHOOKS]; ??? /* 與hook_entry相對應的規則表上限偏移量,當無規則錄入時,相應的hook_entry和underflow均為0 */ ????unsigned int ?underflow[NF_IP_NUMHOOKS]; ????char ?*entries[NR_CPUS]; }; |
? ? 我們發現ipt_register_table()函數還有一個輸入參數:initial_table。根據其名稱不難推斷出它里面存儲的就是我們用于初始化表的一些原始數據,該變量的結構雖然不復雜,但又引入了幾個其他的數據結構,如下:?
| static ?struct { ????struct ipt_replace ?repl; ????struct ipt_standard ?entries[3]; ????struct ipt_error ?term; } initial_table; |
????在注冊過濾表時我們只用到了該結構中的struct ipt_replace repl成員,其他成員我們暫時先不介紹,主要來看一下這個repl是個神馬東東。
ipt_replace{}結構體的定義在include/linux/netfilter_ipv4/ip_tables.h文件中。其內容如下:?
| struct ipt_replace { ????char ?name[IPT_TABLE_MAXNAMELEN];? //表的名字 ????unsigned int ?valid_hooks;?? //所影響的HOOK點 ????unsigned int ?num_entries;?? //表中的規則數目 ????unsigned int ?size; //新規則所占用存儲空間的大小 ? ????unsigned int ?hook_entry[NF_IP_NUMHOOKS]; //進入HOOK的入口點 ????unsigned int ?underflow[NF_IP_NUMHOOKS]; /* Underflow points. */ ? ????/* 這個結構不同于ipt_table_info之處在于它還要保存舊的規則信息*/ ????/* Number of counters (must be equal to current number of entries). */ ????unsigned int ?num_counters; ????/* The old entries' counters. */ ????struct xt_counters __user ?*counters; ? ????/* The entries (hang off end: not really an array). */ ????struct ipt_entry ?entries[0]; }; |
????之所以要設計ipt_replace{}這個結構體,是因為在1.4.0版的iptables中有規則替換這個功能,它可以用一個新的規則替換掉指定位置上的已存在的現有規則(關于iptables命令行工具的詳細用法請參見man手冊或iptables指南)。最后我們來看一下initial_table.repl的長相:?
| initial_table.repl= { "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, { } }; |
????根據上面的初始化代碼,我們就可以弄明白initial_table.repl成員的意思了:
????"filter"表從"FILTER_VALID_HOOKS"這些hook點介入Netfilter框架,并且filter表初始化時有"4"條規則鏈,每個HOOK點(對應用戶空間的“規則鏈”)初始化成一條鏈,最后以一條“錯誤的規則”表示結束,filter表占(sizeof(struct ipt_standard) * 3+sizeof(struct ipt_error))字節的存儲空間,每個hook點的入口規則如代碼所示。???? 因為初始化模塊時不存在舊的表,因此后面兩個個參數依次為0、NULL都表示“空”的意思。最后一個柔性數組struct ipt_entry ?entries[0]中保存了默認的那四條規則。
????由此我們可以知道,filter表初始化時其規則的分布如下圖所示:
我們繼續往下走。什么?你說還有個ipt_error?記性真好,不過請盡情地無視吧,目前講了也沒用。那你還記得我們現在正在討論的是什么主題嗎?忘了吧,我再重申一下:我們目前正在討論iptables內核中的filter數據包過濾模塊是如何被注冊到Netfilter中去的!!
有了上面這些基礎知識我們再分析ipt_register_table(&packet_filter,&initial_table.repl)函數就容易多了,該函數定義在net/ipv4/netfilter/ip_tables.c中:?
| int ipt_register_table(struct xt_table *table, const struct ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ipt_replace *repl) { ????int ret; ? ? struct xt_table_info *newinfo; ? ??static struct xt_table_info bootstrap = { ? ? ? ? ?0, 0, 0, { 0 }, { 0 }, { }? ? ? }; ? ?void *loc_cpu_entry; ???newinfo = xt_alloc_table_info(repl->size); //為filter表申請存儲空間 ? ?if (!newinfo) ???????return -ENOMEM; ? ? ?//將filter表中的規則入口地址賦值給loc_cpu_entry ?????loc_cpu_entry = newinfo->entries[raw_smp_processor_id()]; ???//將repl中的所有規則,全部拷貝到newinfo->entries[]中 ?????memcpy(loc_cpu_entry, repl->entries, repl->size); ???/*translate_table函數將由newinfo所表示的table的各個規則進行邊界檢查,然后對于newinfo所指的xt_talbe_info結構中的hook_entries和underflows賦予正確的值,最后將表項向其他cpu拷貝*/ ? ? ?ret = translate_table(table->name, table->valid_hooks, ??????????????????????????? ????? newinfo, loc_cpu_entry, repl->size, ??????????????????????????? ????? repl->num_entries, ??????????????????????????? ????? repl->hook_entry, ??????????????????????????? ????? repl->underflow); ????if (ret != 0) { ???????????xt_free_table_info(newinfo); ???????????return ret; ????} ??? //這才是真正注冊我們filter表的地方 ????ret = xt_register_table(table, &bootstrap,newinfo); ????if (ret != 0) { ???????????xt_free_table_info(newinfo); ???????????return ret; ????} ????return 0; } |
????在該函數中我們發現點有意思的東西:還記得前面我們在定義packet_filter時是什么情況不? packet_filter中沒對其private成員進行初始化,那么這個工作自然而然的就留給了xt_register_table()函數來完成,它也定義在x_tables.c文件中,它主要完成兩件事:
????1)、將由newinfo參數所存儲的表里面關于規則的基本信息結構體xt_table_info{}變量賦給由table參數所表示的packet_filter{}的private成員變量;
2)、根據packet_filter的協議號af,將filter表掛到變量xt中tables成員變量所表示的雙向鏈表里。
最后我們回顧一下ipt_register_table(&packet_filter,&initial_table.repl)的初始化流程:
? ? 簡而言之ipt_register_table()所做的事情就是從模板initial_table變量的repl成員里取出初始化數據,然后申請一塊內存并用repl里的值來初始化它,之后將這塊內存的首地址賦給packet_filter表的private成員,最后將packet_filter掛載到xt[2].tables的雙向鏈表中。
注冊鉤子函數:nf_register_hooks(ipt_ops,ARRAY_SIZE(ipt_ops));
? ? 在第二篇博文中我們已經簡單了解nf_hook_ops{}結構了,而且我們也知道該結構在整個Netfilter框架中的具有相當重要的作用。當我們要向Netfilter注冊我們自己的鉤子函數時,一般的思路都是去實例化一個nf_hook_ops{}對象,然后通過nf_register_hook()接口其將其注冊到Netfilter中即可。當然filter模塊無外乎也是用這種方式來實現自己的吧,那么接下來我們來研究一下filter模塊注冊鉤子函數的流程。
? ? 首先,我們看到它也實例化了一個nf_hook_ops{}對象——ipt_ops,代碼如下所示:?
| static ?struct nf_hook_ops ?ipt_ops[] = { ???{ ???????.hook????????????????? = ipt_hook, ???????.owner?????????????? = THIS_MODULE, ???????.pf????????????? = PF_INET, ? ? ? ?.hooknum???????? = NF_IP_LOCAL_IN, ? ? ? ?.priority??? = NF_IP_PRI_FILTER, ? ? }, ? ? { ? ? ? ?.hook????????????????? = ipt_hook, ? ? ? ?.owner?????????????? = THIS_MODULE, ? ? ? ?.pf????????????? = PF_INET, ? ? ? ?.hooknum???????? = NF_IP_FORWARD, ? ? ? ?.priority??? = NF_IP_PRI_FILTER, ? ? }, ? ? { ? ? ? ?.hook????????????????? = ipt_local_out_hook, ? ? ? ?.owner?????????????? = THIS_MODULE, ? ? ? ?.pf????????????? = PF_INET, ? ? ? ?.hooknum???????? = NF_IP_LOCAL_OUT, ? ? ? ?.priority??? = NF_IP_PRI_FILTER, ? ? }, }; |
? ? 對上面這種定義的代碼我們現在應該已經很清楚其意義了:iptables的filter包過濾模塊在Netfilter框架的NF_IP_LOCAL_IN和NF_IP_FORWARD兩個hook點以NF_IP_PRI_FILTER(0)優先級注冊了鉤子函數ipt_hook(),同時在NF_IP_LOCAL_OUT過濾點也以同樣的優先級注冊了鉤子函數ipt_local_out_hook()。
? ? 然后,在nf_register_hooks()函數內部通過循環調用nf_register_hook()接口來完成所有nf_hook_ops{}對象的注冊任務。在nf_register_hook()函數里所執行的操作就是一個雙向鏈表的查找和插入,沒啥難度。考大家一個問題,測試一下你看博客的認真和專心程度:filter模塊所定義的這些hook函數是被注冊到哪里去了呢?
?
=================================華麗麗的分割線================================
? ? 想不起的話可以去復習一下第一篇博文結尾部分的內容,不過我知道大多數人都懶的翻回去了。好吧,我再強調一遍:所有的hook函數最終都被注冊到一個全局的二維的鏈表結構體數組struct list_headnf_hooks[NPROTO][NF_MAX_HOOKS]里了。一維表示協議號,二維表示hook點。
? ? 還記得我們給過濾模塊所有hook函數所劃的分類圖么:
? ? 目前只出現了ipt_hook和ipt_local_out_hook,不過這四個函數本質上最后都調用了ipt_do_table()函數,而該函數也是包過濾的核心了。
?
數據包過濾的原理:
? ? 根據前面我們的分析可知,ipt_do_table()函數是最終完成包過濾功能的這一點現在已經非常肯定了,該函數定義在net/ipv4/netfilter/ip_tables.c文件中。實際上,90%的包過濾函數最終都調用了該接口,它可以說是iptables包過濾功能的核心部分。在分析該函數之前,我們把前幾章中所有的相關數據結構再梳理一遍,目的是為了在分析該函數時達到心中有數。
? ??我們前面提到過的核心數據結構有initial_table、ipt_replace、ipt_table、ipt_table_info、ipt_entry、ipt_standard、ipt_match、ipt_entry_match、ipt_target、ipt_entry_target,這里暫時沒有涉及到對用戶空間的相應的數據結構的討論。以上這些數據結構之間的關系如下:
? ? 我們還是先看一下ipt_do_table()函數的整體流程圖:
? ? 我們分析一下整個ipt_do_table()函數執行的過程:
? ? 對某個hook點注冊的所有鉤子函數,當數據包到達該hook點后,該鉤子函數便會被激活,從而開始對數據包進行處理。我們說過:規則就是“一組匹配+一個動作”,而一組規則又組成了所謂的“表”,因此,每條規則都屬于唯一的一張表。前面我們知道,每張表都對不同的幾個HOOK點進行了監聽,而且這些表的優先級是不相同的,我們在用戶空間里去配置iptables規則的時候恰恰也是必須指定鏈名和表名,在用戶空間HOOK點就被抽象成了“鏈”的概念,例如:
? ? iptables?–A ?INPUT ?–p?tcp ?–s ?!192.168.10.0/24 ?–j ?DROP
? ? 這就表示我們在filter表的NF_IP_LOCAL_IN這個HOOK點上增加了一個過濾規則。當數據包到達LOCAL_IN這個HOOK點時,那么它就有機會被注冊在這個點的所有鉤子函數處理,按照注冊時候的優先級來。因為表在注冊時都已確定了優先級,而一個表中可能有數條規則,因此,當數據包到達某個HOOK點后。優先級最高的表(優先級的值越小表示其優先程度越高)中的所有規則被匹配完之后才能輪到下一個次高優先級的表中的所有規則開始匹配(如果數據包還在的話)。
? ? 所以,我們在ipt_do_table()中看到,首先就是要獲取表名,因為表名和優先級在某種程度上來說是一致的。獲取表之后,緊接著就要獲取表中的規則的起始地址。然后用依次按順序去比較當前正在處理的這個數據包是否和某條規則中的所有過濾項相匹配。如果匹配,就用那條規則里的動作target來處理包,完了之后返回;如果不匹配,當該表中所有的規則都被檢查完了之后,該數據包就轉入下一個次高優先級的過濾表中去繼續執行此操作。依次類推,直到最后包被處理或者被返回到協議棧中繼續傳輸。
(五)洞悉linux下的Netfilter&iptables:如何理解連接跟蹤機制?【上】
如何理解Netfilter中的連接跟蹤機制?
? ??本篇我打算以一個問句開頭,因為在知識探索的道路上只有多問然后充分調動起思考的機器才能讓自己走得更遠。連接跟蹤定義很簡單:用來記錄和跟蹤連接的狀態。
問:為什么又需要連接跟蹤功能呢?
答:因為它是狀態防火墻和NAT的實現基礎。
OK,算是明白了。Neftiler為了實現基于數據連接狀態偵測的狀態防火墻功能和NAT地址轉換功能才開發出了連接跟蹤這套機制。那就意思是說:如果編譯內核時開啟了連接跟蹤選項,那么Linux系統就會為它收到的每個數據包維持一個連接狀態用于記錄這條數據連接的狀態。接下來我們就來研究一下Netfilter的連接跟蹤的設計思想和實現方式。
? ? 之前有一副圖,我們可以很明確的看到:用于實現 連接跟蹤入口的hook函數以較高的優先級分別被注冊到了netfitler的NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT兩個hook點上;用于實現 連接跟蹤出口的hook函數以非常低的優先級分別被注冊到了netfilter的NF_IP_LOCAL_IN和NF_IP_POST_ROUTING兩個hook點上。其實PRE_ROUTING和LOCAL_OUT點可以看作是整個netfilter的入口,而POST_ROUTING和LOCAL_IN可以看作是其出口。在只考慮連接跟蹤的情況下,一個數據包無外乎有以下三種流程可以走:
一、發送給本機的數據包
流程:PRE_ROUTING----LOCAL_IN---本地進程
二、需要本機轉發的數據包
流程:PRE_ROUTING---FORWARD---POST_ROUTING---外出
三、從本機發出的數據包
流程:LOCAL_OUT----POST_ROUTING---外出
我們都知道在INET層用于表示數據包的結構是大名鼎鼎的sk_buff{}(后面簡稱skb),如果你不幸的沒聽說過這個東東,那么我強烈的建議你先補一下網絡協議棧的基礎知識再繼續閱讀這篇文章。在skb中有個成員指針nfct,類型是struct nf_conntrack{},該結構定義在include/linux/skbuff.h文件中。該結構記錄了連接記錄被公開應用的計數,也方便其他地方對連接跟蹤的引用。連接跟蹤在實際應用中一般都通過強制類型轉換將nfct轉換成指向ip_conntrack{}類型(定義在include/linux/netfilter_ipv4/ip_conntrack.h里)來獲取一個數據包所屬連接跟蹤的狀態信息的。即:Neftilter框架用ip_conntrack{}來記錄一個數據包與其連接的狀態關系。
同時在include/linux/netfilter_ipv4/ip_conntrack.h文件中還提供了一個非常有用的接口:struct ip_conntrack *ip_conntrack_get(skb,ctinfo)用于獲取一個skb的nfct指針,從而得知該數據包的連接狀態和該連接狀態的相關信息ctinfo。從連接跟蹤的角度來看,這個ctinfo表示了每個數據包的幾種連接狀態:
l? IP_CT_ESTABLISHED
Packet是一個已建連接的一部分,在其初始方向。
l? IP_CT_RELATED
Packet屬于一個已建連接的相關連接,在其初始方向。
l? IP_CT_NEW
Packet試圖建立新的連接
l? IP_CT_ESTABLISHED+IP_CT_IS_REPLY
Packet是一個已建連接的一部分,在其響應方向。
l? IP_CT_RELATED+IP_CT_IS_REPLY
Packet屬于一個已建連接的相關連接,在其響應方向。
? ? 在連接跟蹤內部,收到的每個skb首先被轉換成一個ip_conntrack_tuple{}結構,也就是說ip_conntrack_tuple{}結構才是連接跟蹤系統所“認識”的數據包。那么skb和ip_conntrack_tuple{}結構之間是如何轉換的呢?這個問題沒有一個統一的答案,與具體的協議息息相關。例如,對于TCP/UDP協議,根據“源、目的IP+源、目的端口”再加序列號就可以唯一的標識一個數據包了;對于ICMP協議,根據“源、目的IP+類型+代號”再加序列號才可以唯一確定一個ICMP報文等等。對于諸如像FTP這種應用層的“活動”協議來說情況就更復雜了。本文不試圖去分析某種具體協議的連接跟蹤實現,而是探究連接跟蹤的設計原理和其工作流程,使大家掌握連接跟蹤的精髓。因為現在Linux內核更新的太快的都到3.4.x,變化之大啊。就算是2.6.22和2.6.21在連接跟蹤這塊還是有些區別呢。一旦大家理解了連接跟蹤的設計思想,掌握了其神韻,它再怎么也萬變不離其宗,再看具體的代碼實現時就不會犯迷糊了。俗話說“授人一魚,不如授人一漁”,我們教給大家的是方法。有了方法再加上自己的勤學苦練,那就成了技能,最后可以使得大家在為自己的協議開發連接跟蹤功能時心里有數。這也是我寫這個系列博文的初衷和目的。與君共勉。
在開始分析連接跟蹤之前,我們還是站在統帥的角度來俯視一下整個連接跟蹤的布局。這里我先用比較粗略的精簡流程圖為大家做個展示,目的是方便大家理解,好入門。當然,我的理解可能還有不太準確的地方,還請大牛們幫小弟指正。
? ? 我還是重申一下:連接跟蹤分入口和出口兩個點。 謹記:入口時創建連接跟蹤記錄,出口時將該記錄加入到連接跟蹤表中。我們分別來看看。入口:
整個入口的流程簡述如下:對于每個到來的skb,連接跟蹤都將其轉換成一個tuple結構,然后用該tuple去查連接跟蹤表。如果該類型的數據包沒有被跟蹤過,將為其在連接跟蹤的hash表里建立一個連接記錄項,對于已經跟蹤過了的數據包則不用此操作。緊接著,調用該報文所屬協議的連接跟蹤模塊的所提供的packet()回調函數,最后根據狀態改變連接跟蹤記錄的狀態。
出口:
整個出口的流程簡述如下:對于每個即將離開Netfilter框架的數據包,如果用于處理該協議類型報文的連接跟蹤模塊提供了helper函數,那么該數據包首先會被helper函數處理,然后才去判斷,如果該報文已經被跟蹤過了,那么其所屬連接的狀態,決定該包是該被丟棄、或是返回協議棧繼續傳輸,又或者將其加入到連接跟蹤表中。
連接跟蹤的協議管理:
? ? 我們前面曾說過,不同協議其連接跟蹤的實現是不相同的。每種協議如果要開發自己的連接跟蹤模塊,那么它首先必須實例化一個ip_conntrack_protocol{}結構體類型的變量,對其進行必要的填充,然后調用ip_conntrack_protocol_register()函數將該結構進行注冊,其實就是根據協議類型將其設置到全局數組ip_ct_protos[]中的相應位置上。
? ? ?ip_ct_protos變量里保存連接跟蹤系統當前可以處理的所有協議,協議號作為數組唯一的下標,如下圖所示。
? ? 結構體ip_conntrack_protocol{}中的每個成員,內核源碼已經做了很詳細的注釋了,這里我就不一一解釋了,在實際開發過程中我們用到了哪些函數再具體分析。
? 連接跟蹤的輔助模塊:
? ? Netfilter的連接跟蹤為我們提供了一個非常有用的功能模塊:helper。該模塊可以使我們以很小的代價來完成對連接跟蹤功能的擴展。這種應用場景需求一般是,當一個數據包即將離開Netfilter框架之前,我們可以對數據包再做一些最后的處理。從前面的圖我們也可以看出來,helper模塊以較低優先級被注冊到了Netfilter的LOCAL_OUT和POST_ROUTING兩個hook點上。
每一個輔助模塊都是一個ip_conntrack_helper{}結構體類型的對象。也就是說,如果你所開發的協議需要連接跟蹤輔助模塊來完成一些工作的話,那么你必須也去實例化一個ip_conntrack_helper{}對象,對其進行填充,最后調用ip_conntrack_helper_register{}函數將你的輔助模塊注冊到全局變量helpers里,該結構是個雙向鏈表,里面保存了當前已經注冊到連接跟蹤系統里的所有協議的輔助模塊。
全局helpers變量的定義和初始化在net/netfilter/nf_conntrack_helper.c文件中完成的。
最后,我們的helpers變量所表示的雙向鏈表一般都是像下圖所示的這樣子:
由此我們基本上就可以知道,注冊在Netfilter框架里LOCAL_OUT和POST_ROUTING兩個hook點上ip_conntrack_help()回調函數所做的事情基本也就很清晰了:那就是通過依次遍歷helpers鏈表,然后調用每個ip_conntrack_helper{}對象的help()函數。
期望連接:
? ? Netfilter的連接跟蹤為支持諸如FTP這樣的“活動”連接提供了一個叫做“期望連接”的機制。我們都知道FTP協議服務端用21端口做命令傳輸通道,主動模式下服務器用20端口做數據傳輸通道;被動模式下服務器隨機開一個高于1024的端口,然后客戶端來連接這個端口開始數據傳輸。也就是說無論主、被動,都需要兩條連接:命令通道的連接和數據通道的連接。連接跟蹤在處理這種應用場景時提出了一個“期望連接”的概念,即一條數據連接和另外一條數據連接是相關的,然后對于這種有“相關性”的連接給出自己的解決方案。我們說過,本文不打算分析某種具體協議連接跟蹤的實現。接下來我們就來談談期望連接。
? ? 每條期望連接都用一個ip_conntrack_expect{}結構體類型的對象來表示,所有的期望連接存儲在由全局變量ip_conntrack_expect_list所指向的雙向鏈表中,該鏈表的結構一般如下:
? ? ? ? ?結構體ip_conntrack_expect{}中的成員及其意義在內核源碼中也做了充分的注釋,這里我就不逐一介紹了,等到需要的時候再詳細探討。
連接跟蹤表:
? ? 說了半天終于到我們連接跟蹤表拋頭露面的時候了。連接跟蹤表是一個用于記錄所有數據包連接信息的hash散列表,其實連接跟蹤表就是一個以數據包的hash值組成的一個雙向循環鏈表數組,每條鏈表中的每個節點都是ip_conntrack_tuple_hash{}類型的一個對象。連接跟蹤表是由一個全局的雙向鏈表指針變量ip_conntrack_hash[]來表示。為了使我們更容易理解ip_conntrack_hash[]這個雙向循環鏈表的數組,我們將前面提到的幾個重要的目前還未介紹的結構ip_conntrack_tuple{}、ip_conntrack{}和ip_conntrack_tuple_hash{}分別介紹一下。
? ? 我們可以看到ip_conntrack_tuple_hash{}僅僅是對ip_conntrack_tuple{}的封裝而已,將其組織成了一個雙向鏈表結構。因此,在理解層面上我們可以認為它們是同一個東西。
在分析ip_conntrack{}結構時,我們將前面所有和其相關的數據結構都列出來,方便大家對其理解和記憶。
該圖可是說是連接跟蹤部分的數據核心,接下來我們來詳細說說ip_conntrack{}結構中相關成員的意義。
l? ct_general:該結構記錄了連接記錄被公開應用的計數,也方便其他地方對連接跟蹤的引用。
l? status:數據包連接的狀態,是一個比特位圖。
l? timeout:不同協議的每條連接都有默認超時時間,如果在超過了該時間且沒有屬于某條連接的數據包來刷新該連接跟蹤記錄,那么會調用這種協議類型提供的超時函數。
l? counters:該成員只有在編譯內核時打開了CONFIG_IP_NF_CT_ACCT開完才會存在,代表某條連接所記錄的字節數和包數。
l? master:該成員指向另外一個ip_conntrack{}。一般用于期望連接場景。即如果當前連接是另外某條連接的期望連接的話,那么該成員就指向那條我們所屬的主連接。
l? helper:如果某種協議提供了擴展模塊,就通過該成員來調用擴展模塊的功能函數。
l??proto:該結構是ip_conntrack_proto{}類型,和我們前面曾介紹過的用于存儲不同協議連接跟蹤的ip_conntrack_protocol{}結構不要混淆了。前者是個枚舉類型,后者是個結構體類型。這里的proto表示不同協議為了實現其自身的連接跟蹤功能而需要的一些額外參數信息。目前這個枚舉類型如下:
? ?如果將來你的協議在實現連接跟蹤時也需要一些額外數據,那么可以對該結構進行擴充。
l? help:該成員代表不同的應用為了實現其自身的連接跟蹤功能而需要的一些額外參數信息,也是個枚舉類型的ip_conntrack_help{}結構,和我們前面剛介紹過的結構體類型ip_conntrack_helpers{}容易混淆。ip_conntrack_proto{}是為協議層需要而存在的,而ip_conntrack_help{}是為應用層需要而存在。
l? tuplehash:該結構是個ip_conntrack_tuple_hash{}類型的數組,大小為2。tuplehash[0]表示一條數據流“初始”方向上的連接情況,tuplehash[1]表示該數據流“應答”方向的響應情況,見上圖所示。
? ? 到目前為止,我們已經了解了連接跟蹤設計思想和其工作機制:連接跟蹤是Netfilter提供的一套基礎框架,不同的協議可以根據其自身協議的特殊性在連接跟蹤機制的指導和約束下來開發本協議的連接跟蹤功能,最后將其交給連接跟蹤機制來統一管理。
(六)洞悉linux下的Netfilter&iptables:如何理解連接跟蹤機制?【中】
Netfilter連接跟蹤的詳細流程
????上一篇我們了解了連接跟蹤的基本框架和大概流程,本篇我們著重分析一下,數據包在連接跟蹤系統里的旅程,以達到對連接跟蹤運行原理深入理解的目的。
????連接跟蹤機制在Netfilter框架里所注冊的hook函數一共就五個:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local()、ip_conntrack_help()
和ip_confirm()。前幾篇博文中我們知道ip_conntrack_local()最終還是調用了ip_conntrack_in()。這五個hook函數及其掛載點,想必現在大家應該也已經爛熟于心了,如果記不起來請看【上】篇博文。
????在連接跟蹤的入口處主要有三個函數在工作:ip_conntrack_defrag()、ip_conntrack_in()、ip_conntrack_local();在出口處就兩個:ip_conntrack_help()和ip_confirm()。
接下來的事情就變得非常奇妙,請大家將自己當作一個需要轉發的數據包,且是一條新的連接。然后跟隨我去連接跟蹤里耍一圈吧。在進入連接跟蹤之前,我需要警告大家:連接跟蹤雖然不會改變數據包本身,但是它可能會將數據包丟棄。
ip_conntrack_defrag()
????當我們初到連接跟蹤門口的時候,是這位小生來招待我們。這個函數主要是完成IP報文分片的重新組裝,將屬于一個IP報文的多個分片重組成一個真正的報文。關于IP分片,大家可以去閱讀《TCP/IP詳解卷1》了解一點基礎,至于IP分片是如何被重新組裝一個完整的IP報文也不是我們的重心,這里不展開講。該函數也向我們透露了一個秘密,那就是連接跟蹤只跟蹤完整的IP報文,不對IP分片進行跟蹤,所有的IP分片都必須被還原成原始報文,才能進入連接跟蹤系統。
?
ip_conntrack_in()
????該函數的核心是resolve_normal_ct()函數所做的事情,其執行流程如下所示:在接下來的分析中,需要大家對上一篇文章提到的幾個數據結構:
ip_conntrack{}、ip_conntrack_tuple{}、ip_conntrack_tuple_hash{}和ip_conntrack_protocol{}以及它們的關系必須弄得很清楚,你才能徹底地讀懂resolve_normal_ct()函數是干什么。最好手頭再有一份2.6.21的內核源碼,然后打開source insight來對照著閱讀效果會更棒!
第一步:ip_conntrack_in()函數首先根據數據包skb的協議號,在全局數組ip_ct_protos[]中查找某種協議(如TCP,UDP或ICMP等)所注冊的連接跟蹤處理模塊ip_conntrack_protocol{},如下所示。
在結構中,具體的協議必須提供將屬于它自己的數據包skb轉換成ip_conntrack_tuple{}結構的回調函數pkt_to_tuple()和invert_tuple(),用于處理新連接的new()函數等等。
第二步:找到對應的協議的處理單元proto后,便調用該協議提供的錯誤校驗函數(如果該協議提供的話)error來對skb進行合法性校驗。
? ? 第三步:調用resolve_normal_ct()函數。該函數的重要性不言而喻,它承擔連接跟蹤入口處剩下的所有工作。該函數根據skb中相關信息,調用協議提供的pkt_to_tuple()函數生成一個ip_conntrack_tuple{}結構體對象tuple。然后用該tuple去查找連接跟蹤表,看它是否屬于某個tuple_hash{}鏈。請注意,一條連接跟蹤由兩條ip_conntrack_tuple_hash{}鏈構成,一“去”一“回”,參見上一篇博文末尾部分的講解。為了使大家更直觀地理解連接跟蹤表,我將畫出來,如下圖,就是個雙向鏈表的數組而已。如果找到了該tuple所屬于的tuple_hash鏈表,則返回該鏈表的地址;如果沒找到,表明該類型的數據包沒有被跟蹤,那么我們首先必須建立一個ip_conntrack{}結構的實例,即創建一個連接記錄項。
然后,計算tuple的應答repl_tuple,對這個ip_conntrack{}對象做一番必要的初始化后,其中還包括,將我們計算出來的tuple和其反向tuple的地址賦給連接跟蹤ip_conntrack里的tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]。
最后,把ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]的地址返回。這恰恰是一條連接跟蹤記錄初始方向鏈表的地址。Netfilter中有一條鏈表unconfirmed,里面保存了所有目前還沒有收到過確認報文的連接跟蹤記錄,然后我們的ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL]就會被添加到unconfirmed鏈表中。
第四步:調用協議所提供的packet()函數,該函數承擔著最后向Netfilter框架返回值的使命,如果數據包不是連接中有效的部分,返回-1,否則返回NF_ACCEPT。也就是說,如果你要為自己的協議開發連接跟蹤功能,那么在實例化一個ip_conntrack_protocol{}對象時必須對該結構中的packet()函數做仔細設計。
雖然我不逐行解釋代碼,只分析原理,但有一句代碼還是要提一下。
resolve_normal_ct()函數中有一行ct = tuplehash_to_ctrack(h)的代碼,參見源代碼。其中h是已存在的或新建立的ip_conntrack_tuple_hash{}對象,ct是ip_conntrack{}類型的指針。不要誤以為這一句代碼的是在創建ct對象,因為創建的工作在init_conntrack()函數中已經完成。本行代碼的意思是根據ip_conntrack{}結構體中tuplehash[IP_CT_DIR_ORIGINAL]成員的地址,反過來計算其所在的結構體ip_conntrack{}對象的首地址,請大家注意。
大家也看到ip_conntrack_in()函數只是創建了用于保存連接跟蹤記錄的ip_conntrack{}對象而已,并完成了對其相關屬性的填充和狀態的設置等工作。簡單來說,我們這個數據包目前已經拿到連接跟蹤系統辦法的“綠卡”ip_conntrack{}了,但是還沒有蓋章生效。
?
ip_conntrack_help()
大家只要把我前面關于鉤子函數在五個HOOK點所掛載情況的那張圖記住,就明白ip_conntrack_help()函數在其所注冊的hook點的位置了。當我們這個數據包所屬的協議在其提供的連接跟蹤模塊時已經提供了ip_conntrack_helper{}模塊,或是別人針對我們這種協議類型的數據包提供了擴展的功能模塊,那么接下來的事兒就很簡單了:
首先,判斷數據包是否拿到“綠卡”,即連接跟蹤是否為該類型協議的包生成了連接跟蹤記錄項ip_conntrack{};
其次,該數據包所屬的連接狀態不屬于一個已建連接的相關連接,在其響應方向。
兩個條件都成立,就用該helper模塊提供的help()函數去處理我們這個數據包skb。最后,這個help()函數也必須向Netfilter框架返回NF_ACCEPT或NF_DROP等值。任意一個條件不成立則ip_conntrack_help()函數直接返回NF_ACCEPT,我們這個數據包繼續傳輸。
?
ip_confirm()
????該函數是我們離開Netfilter時遇到的最后一個家伙了,如果我們這個數據包已經拿到了“綠卡”ip_conntrack{},并且我們這個數據包所屬的連接還沒收到過確認報文,并且該連接還未失效。然后,我們這個ip_confirm()函數要做的事就是:
????拿到連接跟蹤為該數據包生成ip_conntrack{}對象,根據連接“來”、“去”方向tuple計算其hash值,然后在連接跟蹤表ip_conntrack_hash[]見上圖中查找是否已存在該tuple。如果已存在,該函數最后返回NF_DROP;如果不存在,則將該連接“來”、“去”方向tuple插入到連接跟蹤表ip_conntrack_hash[]里,并向Netfilter框架返回NF_ACCEPT。之所以要再最后才將連接跟蹤記錄加入連接跟蹤表是考慮到數據包可能被過濾掉。
????至此,我們本次旅行就圓滿結束了。這里我們只分析了轉發報文的情況。發送給本機的報文流程與此一致,而對于所有從本機發送出去的報文,其流程上唯一的區別就是在調用ip_conntrack_in()的地方換成了ip_conntrack_local()函數。前面說過,ip_conntrack_local()里面其實調用的還是ip_conntrack_in()。ip_conntrack_local()里只是增加了一個特性:那就是對于從本機發出的小數據包不進行連接跟蹤。
(七)洞悉linux下的Netfilter&iptables:如何理解連接跟蹤機制?【下】
連接跟蹤系統的初始化流程分析
????有了前面的知識,我們再分析連接跟蹤系統的初始化ip_conntrack_standalone_init()函數就太容易不過了。還是先上ip_conntrack_standalone_init()函數的流程圖:
該函數的核心上圖已經標出來了“初始化連接跟蹤系統”和“注冊連接跟蹤的hook函數”。其他兩塊這里簡單做個普及,不展開講。至少讓大家明白連接跟蹤為什么需要兩中文件系統。
1、? procfs(/proc文件系統)
這是一個虛擬的文件系統,通常掛載在/proc,允許內核以文件的形式向用戶空間輸出內部信息。該目錄下的所有文件并沒有實際存在在磁盤里,但可以通過cat、more或>shell重定向予以寫入,這些文件甚至可以像普通文件那樣指定其讀寫權限。創建這些文件的內核組件可以說明任何一個文件可以由誰讀取或寫入。但是:用戶不能在/proc目錄下新增,移除文件或目錄。
2、? sysctl(/proc/sys目錄)
此接口允許用戶空間讀取或修改內核變量的值。不能用此接口對每個內核變量進行操作:內核應該明確指出哪些變量從此接口對用戶空間是可見的。從用戶空間,你可以用兩種方式訪問sysctl輸出的變量:sysctl系統調用接口;procfs。當內核支持procfs文件系統時,會在/proc中增加一個特殊目錄(/proc/sys),為每個由sysctl所輸出的內核變量引入一個文件,我們通過對這些文件的讀寫操作就可以影響到內核里該變量的值了。
? ??除此之外還有一種sysfs文件系統,這里就不介紹了,如果你感興趣可以去研讀《Linux設備驅動程序》一書的詳細講解。
? ??那么回到我們連接跟蹤系統里來,由此我們可以知道:連接跟蹤系統向用戶空間輸出一些內核變量,方便用戶對連接跟蹤的某些特性加以靈活控制,如改變最大連接跟蹤數、修改TCP、UDP或ICMP協議的連接跟蹤超時是時限等等。
????注意一點:/proc/sys目錄下的任何一個文件名,對應著內核中有一個一模一樣同名的內核變量。例如,我的系統中該目錄是如下這個樣子:ip_conntrack_init()函數
????該函數承擔了連接跟蹤系統初始化的絕大部分工作,其流程我們也畫出來了,大家可以對照源碼來一步一步分析。
? ? 第一步:連接跟蹤的表大小跟系統內存相關,而最大連接跟蹤數和連接跟蹤表容量的關系是:最大連接跟蹤數=8×連接跟蹤表容量。代碼中是這樣的:
ip_conntrack_max = 8 ×ip_conntrack_htable_size;那么從上面的圖我們可以看出來,我們可以通過手工修改/proc/sys/net/ipv4/netfilter目錄下同名的ip_conntrack_max文件即可動態修改連接系統的最大連接跟蹤數了。
????第二步:注冊Netfilter所用的sockopt,先不講,以后再說。只要知道是這里注冊的就行了。
????第三步:為連接跟蹤hash表ip_conntrack_hash分配內存并進行初始化。并創建連接跟蹤和期望連接跟蹤的高速緩存。
? ? 第四步:將TCP、UDP和ICMP協議的連接跟蹤協議體,根據不同協議的協議號,注冊到全局數組ip_ct_protos[]中,如下所示:? ? 最后再做一些善后工作,例如注冊DROP這個target所需的功能函數,為其他諸如NAT這樣的模塊所需的參數ip_conntrack_untracked做初始化,關于這個參數我們在NAT模塊中再詳細討論它。
????這樣,我們連接跟蹤系統的初始化工作就算徹底完成了。有了前幾篇關于連接跟蹤的基礎知識,再看代碼是不是有種神清氣爽,豁然開朗的感覺。
????至于連接跟蹤系統所提供的那五個hook函數的注冊,我想現在的你應該連都不用看就知道它所做的事情了吧。
(八)洞悉linux下的Netfilter&iptables:狀態防火墻
基于連接跟蹤機制的狀態防火墻的設計與實現
連接跟蹤本身并沒有實現什么具體功能,它為狀態防火墻和NAT提供了基礎框架。前面幾章節我們也看到:從連接跟蹤的職責來看,它只是完成了數據包從“個性”到“共性”抽象的約定,即它的核心工作是如何針對不同協議報文而定義一個通用的“連接”的概念出來,具體的實現由不同協議自身根據其報文特殊性的實際情況來提供。那么連接跟蹤的主要工作其實可以總結為:入口處,收到一個數據包后,計算其hash值,然后根據hash值查找連接跟蹤表,如果沒找到連接跟蹤記錄,就為其創建一個連接跟蹤項;如果找到了,則返回該連接跟蹤項。出口處,根據實際情況決定該數據包是被還給協議棧繼續傳遞還是直接被丟棄。
????我們先看一下iptables指南中關于用戶空間中數據包的四種狀態及其解釋:
| 狀態 | 解釋 |
| NEW | NEW說明這個包是我們看到的第一個包。意思就是,這是conntrack模塊看到的某個連接第一個包,它即將被匹配了。比如,我們看到一個SYN包,是我們所留意的連接的第一個包,就要匹配它。第一個包也可能不是SYN包,但它仍會被認為是NEW狀態。這樣做有時會導致一些問題,但對某些情況是有非常大的幫助的。例如,在我們想恢復某條從其他的防火墻丟失的連接時,或者某個連接已經超時,但實際上并未關閉時。 |
| ESTABLISHED | ESTABLISHED已經注意到兩個方向上的數據傳輸,而且會繼續匹配這個連接的包。處于ESTABLISHED狀態的連接是非常容易理解的。只要發送并接到應答,連接就是ESTABLISHED的了。一個連接要從NEW變為ESTABLISHED,只需要接到應答包即可,不管這個包是發往防火墻的,還是要由防火墻轉發的。ICMP的錯誤和重定向等信息包也被看作是ESTABLISHED,只要它們是我們所發出的信息的應答。 |
| RELATED | RELATED是個比較麻煩的狀態。當一個連接和某個已處于ESTABLISHED狀態的連接有關系時,就被認為是RELATE的了。換句話說,一個連接要想是RELATED的,首先要有一個ESTABLISHED的連接。這個ESTABLISHED連接再產生一個主連接之外的連接,這個新的連接就是RELATED的了,當然前提是conntrack模塊要能理解RELATED。ftp是個很好的例子,FTP-data 連接就是和FTP-control有RELATED的。還有其他的例子,比如,通過IRC的DCC連接。有了這個狀態,ICMP應答、FTP傳輸、DCC等才能穿過防火墻正常工作。注意,大部分還有一些UDP協議都依賴這個機制。這些協議是很復雜的,它們把連接信息放在數據包里,并且要求這些信息能被正確理解。 |
| INVALID | INVALID說明數據包不能被識別屬于哪個連接或沒有任何狀態。有幾個原因可以產生這種情況,比如,內存溢出,收到不知屬于哪個連接的ICMP 錯誤信息。一般地,我們DROP這個狀態的任何東西。 |
????認真體會這個表格所表達意思對我們理解狀態防火墻的機制和實現有很大的幫助。我們以最常見的TCP、UDP和ICMP協議為例來分析,因為他們最常見。對于TCP/UDP來說,我們可以用“源/目的IP+源/目的端口”唯一的標識一條連接;因為ICMP沒有端口的概念,因此對ICMP而言,其“連接”的表示方法為“源/目的IP+類型+代碼+ID”。因此,你就可以明白,如果你有一種不同于目前所有協議的新協議要為其開發連接跟蹤功能,那么你必須定以一個可以唯一標識該報文的規格,這是必須的。
????接下來我就拋磚引玉,分析一下NEW、ESTABLISHED、RELATED和INVALID幾種狀態內核中的變遷過程。
依舊在ip_conntrack_in()函數中,只不過我們這次的側重點不同。由于該報文是某條連接的第一個數據包,ip_conntrack_find_get()函數中根據該數據包的tuple在連接跟蹤表ip_conntrack_hash中肯定找不到對應的連接跟蹤記錄,然后重任就交給了init_conntrack()函數:
如果連接跟蹤數已滿,或沒有足夠的內存時,均會返回錯誤。否則,將新連接跟蹤記錄的引用計數置為1,設置連接跟蹤記錄“初始”和“應答”方向的tuple鏈,同時還設置了連接跟蹤記錄被銷毀和超時的回調處理函數destroy_conntrack()和death_by_timeout()等。
????至此,我們新的連接記錄ip_conntrack{}就華麗麗滴誕生了。每種協議必須對其“新連接記錄項”提供一個名為new()的回調函數。該函數的主要作用就是針對不同協議,什么樣的報文才被稱為“new”狀態必須由每種協議自身去考慮和實現。具體我就不深入分析了,大家只要知道這里有這么一出戲就可以了,感興趣的朋友可以去研究研究。當然,這需要對協議字段和意義有比較透徹清晰的了解才能完全弄明白別人為什么要那么設計。畢竟我們不是去為TCP、UDP或ICMP開發連接跟蹤,開源界的信條就是“永遠不要重復發明車輪”。如果你想深入研究現有的東西,目的只有一個:那就是學習別人的優點和長處,要有重點,有主次的去學習,不然會讓自己很累不說,還會打擊求知的積極性和動力。
閑話不都說,我們繼續往下分析。如果該數據包所屬的協議集提供了helper接口,那么將其掛到conntrack->helper的回調接口上。最后,將該連接跟蹤項“初始”方向的tuple鏈添加到一條名為unconfirmed的全局鏈表中,該鏈表里存儲的都是截止到目前為止還未曾收到“應答”方向數據包的連接跟蹤記錄。
費了老半天勁兒,狀態防火墻終于出來和大家見面了:
在ip_ct_get_tuple()函數里初始化時tuple.dst.dir就被設置為了IP_CT_DIR_ORIGINAL,因為我們討論的就是NEW狀態的連接,tuple.dst.dir字段到目前為止還未被改變過,與此同時,ip_conntrack.status位圖自從被創建之日起經過memset()操作后就一直為全0狀態,才有最后的skb->nfctinfo=*ctinfo=IP_CT_NEW和skb->nfct = &ip_conntrack->ct_general。
繼續回到ip_conntrack_in()函數里,此時調用協議所提供的回調packet()函數。在博文六中我們曾提及過,該函數承擔著數據包生死存亡的使命。這里我們有必要注意一下packet()函數最后給Netfilter框架返回值的一些細節:
-1,其實就是-NF_ACCEPT,意思是:連接跟蹤出錯了,該數據包不是有效連接的一部分,Netfilter不要再對這類報文做跟蹤了,調用前面的回調函數destroy()清除已經為其設置的連接跟蹤項記錄項,釋放資源。最后向Netfilter框架返回ACCEPT,讓該數據包繼續傳輸。
0,就是NF_DROP,返回給Netfilter框架的也是該值,那么這數據包就掛在這里了。
1,就是NF_ACCEPT,同樣,該數據包已經被正確跟蹤了,通知Netfilter框架繼續傳輸該數據包。
對于像TCP這樣非常復雜的協議才用到了NF_DROP操作,像UDP、ICMP、GRE、SCTP等協議都沒有到,但不排除你的項目中使用NF_DROP的情形。
在連接跟蹤的出口處的ip_conntrack_confirm()函數中,如果已經為該數據包skb創建了連接跟蹤記錄ip_conntrack{}(即skb->nfct有值),則做如下處理:
如果該連接還沒有收到回復報文----明顯如此;
如果該連接沒有掛掉----毫無疑問。
因為是新連接,因此在全局鏈表數組ip_conntrack_hash[]就沒有記錄該連接“初始”和“應答”方向的tuplehash鏈。然后,緊接著我把該連接初始方向的tuplehash鏈ip_conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list從unconfirmed鏈表上卸下來。并且,將該連接初始和應答方向的tuplehash鏈表根據其各自的hash之加入到ip_conntrack_hash[]里,最后啟動連接跟蹤老化時間定時器,修改引用計數,將ip_conntrack.status狀態位圖更新為IPS_CONFIRMED_BIT,并向Netfilter框架返回NF_ACCEPT。數據包離開Netfilter繼續在協議棧中傳遞。針對于ESTABLISHED狀態的理解:
????每個ip_conntrack{}結構末尾有兩條tuplehash鏈,分別代表“初始”和“應答”方向的數據流向,如下圖所示:
????如果一條連接進入ESTABLISHED,那么的前一狀態一定是NEW。因此,我們繼續前面的分析分析過程,當我們的連接跟蹤記錄收到了其對應的響應報文后的處理流程。注意前面剛分析過的:對于新連接的狀態位圖status已經被設置成IPS_CONFIRMED_BIT了。
????繼續在ip_conntrack_in()函數,所以數據包從skb到tuple的生成過程中,初始化時都有tuple->dst.dir=IP_CT_DIR_ORIGINAL;那么tuple->dst.dir是何時被改變狀態的呢?這就牽扯到一種很重要的通信機制netlink。連接跟蹤框架還為連接記錄的躍遷改變定義了一些事件處理和通知機制,而這目前不是本文的重點。在入口處,雖然從連接跟蹤表中找到了該tuple所屬的連接跟蹤記錄項,但在過濾表中該報文有可能會被丟棄,因此不應該急于改變回應報文所屬的連接跟蹤記錄的狀態,回應報文也有skb->nfctinfo = *ctinfo=IP_CT_NEW,該數據包所屬的連接跟蹤記錄保存在skb->nfct里。在出口處,函數ip_conntrack_confirm()中,由于在NEW狀態時,連接跟蹤記錄項中status=IPS_CONFIRMED_BIT了,因此這里對于響應報文,不會重復執行函數__ip_conntrack_confirm()。
緊接著,當“回應”報文被連接跟蹤框架看到后,它會調用ip_ct_deliver_cached_events()函數,以某種具體的事件通過netlink機制來通知ip_conntrack_netlink.c文件中的ctnetlink_parse_tuple()函數將初始方向的tuple->dst.dir=IP_CT_DIR_REPLY。這里理解起來稍微有點抽象,不過還是提醒大家抓重點思路,我們后面這幾章內容相對來說比前面幾章要稍微復雜些,這也是能力提升必須要經歷的過程。
? ? 至此,該連接跟蹤記錄ip_conntrack{}的數據我們來欣賞一下:? ? 如果該連接后續一個初始方向的數據包又到達了,那么在resolve_normal_ct()函數中,便會執行設置skb->nfctinfo=*ctinfo =IP_CT_ESTABLISHED + IP_CT_IS_REPLY和set_reply=1,然后退出到ip_conntrack_in()里,將上圖中ip_conntrack{}結構體里的status成員屬性由原來的IPS_CONFIRMED_BIT設置為IPS_SEEN_REPLY_BIT,并通過函數ip_conntrack_event_cache()觸發一個netlink狀態改變事件。最后,在ip_conntrack_confirm()里也只出發netlink事件而已。緊接著,第二個應答方向的報文也到達了,和上面的處理動作一樣。
?
????針對于RELATED狀態的理解:
????很多文章都從FTP協議的角度來剖析這個狀態,確實FTP也是最能體現RELATED特性的協議。假如有個報文屬于某條已經處于ESTABLISHED狀態的連接,我們來看看狀態防火墻是如何來識別這種情況。
????依然在resolve_normal_ct()函數中,執行到init_conntrack()里面時,通過數據包相對應的tuple即可在全局鏈表ip_conntrack_expect_list里找到該連接所屬的主連接。然后將我們這條RELATED連接記錄的status=IPS_EXPECTED_BIT,并建立我們RELATED連接和它所屬的主連接之間的對應關系conntrack->master= exp->master,同樣將其掛載到unconfirmed鏈里。返回到resolve_normal_ct()里,為數據包設置狀態值skb->nfctinfo = *ctinfo=IP_CT_RELATED。當數據包將要離開時,在ip_conntrack_confirm()函數中也會將其加入連接跟蹤表,并設置status為IPS_CONFIRMED_BIT。剩下的流程就和前面我們討論的是一樣了,唯一卻別的地方在于屬于RELATED的連接跟蹤,其master指向了它所屬的主連接跟蹤記錄項。
????INVALID狀態壓根兒就沒找著,汗。跟俺玩躲貓貓,哥還不鳥你捏。。。
????本篇的知識點相對來說總體來說比較抽象,內容比較多,實現上也較為復雜,我在省略了其狀態躍遷流程情況下都還寫了這么多東西,很多地方研究的其實都不是很深入,只能感慨Netfilter的博大。
(九)洞悉linux下的Netfilter&iptables:網絡地址轉換原理之DNAT
網絡地址轉換:NAT
?????Netfitler為NAT在內核中維護了一張名為nat的表,用來處理所有和地址映射相關的操作。諸如filter、nat、mangle抑或raw這些在用戶空間所認為的“表”的概念,在內核中有的是以模塊的形式存在,如filter;有的是以子系統方式存在的,如nat,但它們都具有“表”的性質。因此,內核在處理它們時有很大一部操作都是相同的,例如表的初始化數據、表的注冊、鉤子函數的注冊等等。關于NAT表的初始化模板數據和表的注冊流程并不是本文的重點,大家可以對照第四篇博文中filter表的相關分析來研究。本文還是側重于從整體上對整個NAT子系統的設計思想和原理進行,當然,有時間我還是會簡單和大家分析一NAT表的東西。因為最近確實太忙了,本來想著在四月份結束這個系列,無奈一轉眼就晃到了五月份,做IT的娃,都不容易啊!
?????通過前面的幾篇文章我們已經知道,NAT的設計是獨立于連接跟蹤系統的,即連接跟蹤是NAT的基礎框架,我們還了解到連接跟蹤不會修改數據包,它只是負責維護數據包和其所屬的業務會話或數據連接狀態的相關信息而已。連接跟蹤最終是被iptables模塊所使用的,它所定義的那些狀態信息如NEW、ESTABLISHED、RELEATED等等,NAT統統不用關心。
?????根據前面的hook函數掛載圖我們可以清晰的知道,對于那些需要被本機轉發的數據包,注冊在NF_IP_PRE_ROUTING點的ip_nat_in ()函數完成對其目的地址轉換的操作,注冊在NF_IP_POST_ROUTING點的ip_nat_out()函數完成源地址轉換任務。如果在編譯Linux內核源碼時打開了CONFIG_IP_NF_NAT_LOCAL選項,則注冊在NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN點的hook函數就會工作,最常見的用法的是用NF_IP_LOCAL_OUT點的ip_nat_local_fn()函數來改變本機發出報文的目的地址。至于注冊在NF_IP_LOCAL_IN點的ip_nat_fn()函數一般不會起作用,只是當數據包到達該HOOK點后會例行被調用一下。因為,NAT的所有規則只可能被配置到nat表的PREROUTING、POSTROUTING和OUTPUT三個點上,一般也很少有人去修改那些路由給本機的報文的源地址。
???????? NAT的分類如下圖所示:? ? 相信大家在看iptables用戶指南都見過這么一句解釋:
????只有每條連接的第一個數據包才會經過nat表,而屬于該連接的后續數據包會按照第一個數據包則會按照第一個報所執行的動作進行處理,不再經過nat表。Netfilter為什么要做這個限制?有什么好處?它又是如何實現的?我們在接下來的分析中,將一一和大家探討這些問題。
????在ip_nat_rule.c文件中定義了nat表的初始化數據模板nat_table,及相應的target實體:SNAT和DNAT,并將其掛在到全局xt[PF_INET].target鏈表中。關于NAT所注冊的幾個hook函數,其調用關系我們在前幾篇博文中也見過:
因此,我們的核心就集中在ip_nat_in()上。也就是說,當我們弄明白了ip_nat_fn()函數,你就差不多已經掌握了nat的精髓。ip_nat_in()函數定義定在ip_nat_standalone.c文件里。連接跟蹤作為NAT的基礎,而建立在連接跟蹤基礎上的狀態防火墻同樣服務于NAT系統。
? ? 關于ip_nat_fn()函數我們還是先梳理整體流程,以便大家對其有一個宏觀整體的把握,然后我們再來分析其實現細節。這里需要大家對連接跟蹤的狀態躍遷有一定了了解。? ? 從流程圖可以看出,牽扯到的幾個關鍵函數都土黃色標注出來了。ip_nat_setup_info()函數主要是完成對數據包的連接跟蹤記錄ip_conntrack對象中相應成員的修改和替換,而manip_pkt()中才是真正對skb里面源/目的地址,端口以及數據包的校驗和字段修改的地方。
目的地址轉換:DNAT
???????? DNAT主要適用于將內部私有地址的服務發布到公網的情形。情形如下:? ? 服務器上架設了Web服務,其私有地址是B,代理防火墻服務器有一個公網地址A。想在需要通過A來訪問B上的Web服務,此時就需要DNAT出馬才行。根據前面的流程圖,我們馬上殺入內核。
當client通過Internet訪問公網地址A時,通過配置在防火墻上的DNAT將其映射到了對于私網內服務器B的訪問。接下來我們就分析一下當在client和server的交互過程中架設在防火墻上NAT是如何工作。
還是看一下hook函數在內核中的掛載分布圖。
?
? ? 在PREROUTING點當一個skb被連接跟蹤過后,那么skb->ctinfo和skb->nfct兩個字段均被設置了值。在接下來的分析中,對那些梢枝末節的代碼我們都將不予理睬。盜個圖:這里牽扯到一個變量ip_conntrack_untracked,之前我們見過,但是還沒討論過。該變量定義在ip_conntrack_core.c文件里,并在ip_conntrack_init()函數進行部分初始化:
| atomic_set(&ip_conntrack_untracked.ct_general.use, 1); set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status); |
同時,在ip_nat_core.c文件里的ip_nat_init()函數中又有如下設置:
| ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK; |
該變量又是何意呢?我們知道iptables維護的其實是四張表,有一張raw不是很常用。該表以-300的優先級在PREROUTING和LOCAL_OUT點注冊了ipt_hook函數,其優先級要高于連接跟蹤。當每個數據包到達raw表時skb->nfct字段缺省都被設置成了ip_conntrack_untracked,所以當該skb還沒被連接蹤的話,其skb->nfct就一直是ip_conntrack_untracked。對于沒有被連接跟蹤處理過的skb是不能進行NAT的,因此遇到這種情況代碼中直接返回ACCEPT。
從上面的流程圖可以看出,無論是alloc_null_binding_confirmed()、alloc_null_binding()還是ip_nat_rule_find()函數其本質上最終都調用了ip_nat_setup_info()函數。
?
ip_nat_setup_info()函數:
該函數中主要完成了對連接跟蹤記錄ip_conntrack.status字段的設置,同時根據可能配置在nat表中的DNAT規則對連接跟蹤記錄里的響應tuple進行修改,最后將該ip_conntrack實例掛載到全局雙向鏈表bysource里。
? ? 在連接跟蹤系統里根據skb的源/目的IP分別已經構建生成初始tuple和響應tuple,我們通過一個簡單的示意圖來回顧一下其流程,并加深對ip_nat_setup_info()函數執行過程的理解。在圖1中,根據skb的源、目的IP生成了其連接跟蹤記錄的初始和響應tuple;
在圖2中,以初始tuple為輸入數據,根據DNAT規則來修改將要被改變的地址。這里是當我們訪問目的地址是A的公網地址時,DNAT規則將其改成對私網地址B的訪問。然后,計算被DNAT之后的數據包新的響應。最后用新的響應tuple替換ip_conntrack實例中舊的響應tuple,因為數據包的目的地址已經被改變了,所以其響應tuple也必須跟著變。
在圖3中,會根據初始tuple計算一個hash值出來,然后以ip_conntrack結構中的nat.info字段會被組織成一個雙向鏈表,將其插入到全局鏈表bysource里。
最后,將ip_conntrack.status字段的IPS_DST_NAT和IPS_DST_NAT_DONE_BIT位均置為1。
這里必須明確一點:在ip_nat_setup_info()函數中僅僅是對ip_conntrack結構實例中相關字段進行了設置,并沒有修改原始數據包skb里的源、目的IP或任何端口。
?
ip_nat_packet()函數的核心是調用manip_pkt()函數:
在manip_pkt()里主要完成對數據包skb結構中源/目的IP和源/目的端口的修改,并且修改了IP字段的校驗和。從mainip_pkt()函數中返回就回到了ip_nat_in()函數中(節選):
| ret = ip_nat_fn(hooknum, pskb, in, out, okfn); if (ret != NF_DROP && ret != NF_STOLEN&& daddr != (*pskb)->nh.iph->daddr) { ???????? dst_release((*pskb)->dst); ???????? (*pskb)->dst = NULL; } return ret; |
????正常情況下返回值ret一般都為NF_ACCEPT,因此會執行if條件語句,清除skb原來的路由信息,然后在后面協議棧的ip_rcv_finish()函數中重新計算該數據包的路由。
在數據包即將離開NAT框架時,還有一個名為ip_nat_adjust()的函數。參見hook函數的掛載示意圖。該函數主要是對那些執行了NAT的數據包的序列號進行適當的調整。如果調整出錯,則丟棄該skb;否則,將skb繼續向后傳遞,即將到達連接跟蹤的出口ip_confirm()。至于,ip_confirm()函數的功能說明我們在連接跟蹤章節已經深入討論了,想不起來的童鞋可以回頭復習一下連接跟蹤的知識點。
?
前面我們僅分析了從client發出的第一個請求報文到server服務器時,防火墻的處理工作。緊接著我們順著前面的思路繼續分析,當server收到該數據包后回應時防火墻的處理情況。
server收到數據包時,該skb的源地址(記為X)從未變化,但目的地址被防火墻從A改成了B。server在響應這個請求時,它發出的回應報文目的地址是X,源地址是自己的私有地址B。大家注意到這個源、目的地址剛好匹配被DNAT之后的那個響應tuple。
當該回應報文到達防火墻后,首先是被連接跟蹤系統處理。顯而易見,在全局的連接跟蹤表ip_conntrack_hash[]中肯定可以找到這個tuple所屬的連接跟蹤記錄ip_conntrack實例。關于狀態的變遷參見博文八。
然后,該回應報文到達NAT框架的ip_nat_in()函數,流程和前面一樣,但處理方式肯定不同。我們還是先看一下截止到目前為止,這條連接跟蹤結構圖: 直接跳到ip_nat_packet()函數里,當netlink通知機制將連接跟蹤狀態由NEW變為REPLY后,此時dir=1,那么根據初始tuple求出原來的響應tuple:源地址為A,目的之為X。此時,server的響應報文,源地址為私有網段B,目的地址為X。路由尋址是以目的地址為依據,防火墻上有直接到client的路由,所以響應報文是可以被client正確收到的。但是,但可是,蛋炒西紅柿,對于UDP來說client收到這樣的回復沒有任何問題的,但是對于TCPU而言確實不行的。這就引出我們接下來將要討論的SNAT。
(十)洞悉linux下的Netfilter&iptables:網絡地址轉換原理之SNAT
源地址轉換:SNAT
? ? SNAT主要應用于下列場景:? ? 這種情況下,我們只有一個公網地址A,而又有三臺主機需要同時上網,這時就需要SNAT了。它的主要作用是將那些由私網發來的數據包skb的源地址改成防火墻的公網地址A,這是因為目的主機在響應源地址為私網地址的數據包時,私網地址不能在網絡上路由的緣故。
????SNAT僅可以在LOCAL_OUT和POSTROUTING點生效,這也說明了為什么用戶空間的iptables命令在配置SNAT規則時只能配置到nat表的OUTPUT和POSTROUTING鏈的緣由。
????我們現在假設的情形是:其他四個HOOK點都沒配任何規則,且內置鏈的缺省處理策略為ACCEPT,然后在防火墻上配置了一條SNAT規則,私網地址B1要訪問公網地址C的情況。
????和前面DNAT類似,當第一個由B1發往C的數據包到達POSTROUTING點時,在連接跟蹤階段就已經為該連接建立好了ip_conntrack實例并進行過適當的初始化。SNAT主要是在ip_nat_out()函數中完成,而該函數本質上也調用了ip_nat_fn(),所以流程和DNAT一樣,但執行的操作有所差別。
????還是先回顧一下ip_nat_fn()的流程圖:? ? 注意,當執行ip_nat_out()函數時,該skb已經被正確路由過了。此時,在ip_nat_fn()里執行的是ip_nat_rule_find()分支,然后進入ip_nat_setup_info()函數中。
????接下來我們簡單說一下get_unique_tuple()函數。
????如果屬于某條連接的數據包之前已經被執行過NAT了,則其連接跟蹤記錄會被添加到bysource鏈表中。對于SNAT操作,如果是第一個數據包,其流程和DNAT非常相似,在函數find_best_ips_proto()中完成對臨時tuple的源地址的修改。然后,以修改后的tuple計算其響應tuple,最終用該響應tuple替換掉連接跟蹤記錄中原來的響應tuple。替換的結果是:? ? 此時,連接跟蹤記錄中響應tuple的源地址已經被替換成防火墻的公網地址A了。等會兒當服務器C回應時,它所發出的報文目的地址就是A,這樣防火墻就可以正確接收到服務器C的回應報文。最后,會根據該連接跟蹤記錄實例的初始tuple來計算一個hash值,然后將其插入到bysource里,并對ip_conntrack.status狀態進行適當設置。
????和DNAT類似,最后也是在manip_pkt()函數中完成對skb源地址、IP校驗和進行修改。對skb里端口的修改也是在manip_pkt()里完成的。
?
????OK,關于SNAT我們第一階段的分析就完成了,接下來我們來看一下當服務器C收到這個請求報文后,在對其響應的后續流程里防火墻是如何實現所謂的自動De-SNAT功能。
?
????服務器的響應報文其源地址為自己的公網地址C,目的地址為防火墻的公網地址A。當該響應報文到達防火墻后,連接跟蹤系統可以準確地識別該數據包所欲的連接跟蹤記錄,因為存在一個源地址是C,目的地址是A的響應tuple(如上圖所示)與其匹配。該響應數據包還是會先到達PREROUTING點的ip_nat_in()函數,因為沒有配置任何DNAT規則,同時ct.status字段又設置了IPS_SRC_NAT和IPS_SRC_NAT_DONE_BIT標志位,所以在進入到ip_nat_in()函數的ip_nat_fn()里時就直接調用ip_nat_packet()接口。
????在ip_nat_packet()中以連接跟蹤記錄的初始tuple計算原來舊的響應tuple:源地址是C,目的地址是B1。因為在PREROUTING點上要做DNAT,所以此時skb中的目的地址A就被改成了原來的響應tuple中的目的地址B1了。后面的流程和DNAT完全一樣。
????當該響應報文來到POSTROUTING點時,又調用ip_nat_out(),和前面分析SNAT一階段時的流程一樣。
?
????現在我們就明白了,De-SNAT功能是基于manip_pkt()函數實現的。在結尾之際,我們來解釋一下上篇博文中關于NAT的一段描述:“只有每條連接的第一個數據包才會經過nat表,而屬于該連接的后續數據包會按照第一個數據包則會按照第一個報所執行的動作進行處理,不再經過nat表”。
????從用戶空間的iptables規則來看,每條規則都有一個counter計數器,該計數器記錄的是被該規則成功匹配了數據包的數目。而內核中對該計數器的修改是在ipt_do_table()函數。從ip_nat_fn()函數的執行流程可以看出,當連接跟蹤記錄項被設置了IPS_SRC_NAT_DONE_BIT狀態位,或者連接跟蹤的狀態不再是IP_CT_NEW狀態時,ipt_do_table()函數就不再被調用了,反應在用戶空間所看到的直觀現象就是,nat表的規則計數器不再增長了。
????基于連接跟蹤的NAT,其特殊之處就在于初始和響應tuple不再一致了,而這也是NAT得以正確運行的關鍵所在。通過如下這張圖,讓大家再復習一下NAT的初始和響應tuple之間的關系:? ? 至此,Linux中Netfilter框架下的nat子系統我們就全部學習完了。用了十個章節基本將Netfilter框架的各個功能子模塊、架構、原理、流程等作了初步簡單的分析了解。由于知識有限,本人的分析難免存在疏漏,還請各位大俠不吝指正。在接下來后續的文章中,主要探討以下主題:
1、? 用戶空間的iptables是怎樣識別傳遞給它的每個參數?
2、? 通過iptables配置的每條規則是如何進到內核里的?它們又有什么關系?
3、? 內核是如何判斷數據包到底匹配還是不匹配某條具體規則?
4、? 如何自己動手擴展iptables的功能模塊?
(十一)洞悉linux下的Netfilter&iptables:iptables命令行工具源碼解析【上】
預備知識:
part1: 初見getopt_long()??
在分析iptables源碼時,作為命令解析的核心函數getopt_long()不得不提。隨便百度或google搜索關于該函數的介紹有很多例子和解釋,這里我只舉一例,目的是讓大家了解傳遞給iptables命令的每個參數是如何被正確識別并處理的。
getopt_long(int argc,char * constargv[],const char *optstring,const struct option *longopts,int *longindex)
? ? 參數說明:
? ? argc和argv來自main函數的輸入;
? ? optstring:表示可以接受的參數。可以是下列值:1.單個字符,表示選項;2.單個字符后接一個冒號“:”表示該選項后必須跟一個參數。參數緊跟在選項后或者以空格隔開,該參數的指針賦給optarg。3.單個字符后跟兩個冒號,表示該選項后必須跟一個參數。參數必須緊跟在選項后不能以空格隔開。該參數的指針賦給optarg。(這個特性是GNU的擴展)。例如,"a:b:cd",表示可以接受的參數選項是a,b,c,d,其中a和b參數后面跟有參數值。
? ? longopts:是一個結構類型,描述如下:
? ? ? ? struct option {
? ? ? ? ? ? ? ? const char *name;????//name表示的是長參數名
? ? ? ? ? ? ? ? int has_arg; ? ? ? ? //0-無參;1-一定要有;2-可有可無
? ? ? ? ? ? ??? int *flag; ? ? ?? ? //用來決定,getopt_long()的返回值到底是什么。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//如果flag是null,則函數會返回與該項option匹配的val值。? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? int val; //和flag聯合決定返回值
? ? ? ? }
? ? 在iptables的do_command()命令解析函數中,見到最多的就是optarg和optind。
? ? optarg: 如果字符串optstring中某個選項后面需要跟參數,該參數值一般保存在optarg中;
? ? optind: 該參數比較費神,輸入參數列表不同,其取值也不一樣。
? ? 說了半天估計大家都快暈了,還是通過例子來說明這兩個值隨著輸入參數的不同其變化情況吧。
| #include <stdio.h> #include <getopt.h> char *l_opt_arg; char* const short_options = "nb:ls:"; struct option long_options[] = { ???? { "name",???? 0,?? NULL,??? 'n'???? }, ???? { "bf_name",? 1,?? NULL,??? 'b'???? }, ???? { "love",???? 0,?? NULL, ???'l'???? }, ???? { "speed",???? 1,?? NULL,??? 's'???? }, ???? {????? 0,???? 0,???? 0,???? 0}, }; ? int main(int argc, char *argv[]) { ???? int c; ??? printf("init otpind=%d\n",optind); ???? while((c = getopt_long (argc, argv, short_options, long_options, NULL)) != -1) ???? { ??????? printf("option=%c,optind=%d,optarg=%s\n",c,optind,optarg); ??????? printf("args[%d]=%s\n",optind,argv[optind]); ???????? switch (c) ???????? { ???????? case 'n': ???????????? printf("My name is XL.\n"); ???????????? break; ???????? case 'b': ???????????? printf("His name is ST.\n"); ???????????? break; ???????? case 'l': ??????????? ?printf("Our love is ok!\n"); ???????????? break; ???????? case 's': ???????? ????printf("SHit of son.\n"); ???????????? break; ???????? } ???? } ???? return 0; } |
? ?
? ? 該測試程序接受的可選參數為-n -b -l -s,其中b和s選項后面要跟值。
? ? 如果我們執行./test -n -b boy -l -s son
? ? optind依次取值為1、2、4、5、7。默認值就是1,當解析-n時,因為發現-n不需要值,所以當調用了getopt_long后,optind自動加1,指向-b選項。而-b是需要參數的,那么跟在-b后面的一定是它的值,所以當解析-b時,optind自動跳到下一個選項-l所在位置。同樣-s也需要跟參數,那么當解析-l時optind就自動跳到-s所在的位置了,即5。
?
? ? 如果我們執行./test -n --b=boy -l -s son
? ? 這樣的格式,optind依次為1、2、3、4、6。大家基本已經可以看出些眉目了吧。因為-b參數用長參格式加等號的賦值方式,所以optind的移動稍微有些變化。但它終歸可以正確識別傳給它的所有命令行參數及其格式。
?
? ? 如果我們執行./test -nl -b boy -s son
? ? optind依次取值1、1、2、4、6。第一個選項是nl組合項,而且這兩個選項都不需要跟參數。
關于getopt_long函數的更多用法參見man幫助手冊。自己再對上面這個程序摸索摸索體會要更深刻些。
?
part2:iptables的自動加載模塊原理
? ? ? ?無論是match還是target,在用戶空間都有其對應的so庫文件,關于動態庫大家可以參閱讀我的另一篇博文《Linux系統中“動態庫”和“靜態庫”那點事兒》。這里我們注意到一點的就是無論是諸如libxt_tcp.c這樣的協議模塊,還是libxt_limit.c這樣的match模塊,又抑或libipt_REJECT.c這樣的target模塊,每個模塊中都有一個名為_init()的函數。為什么我們的自己平時開發so庫時,怎么沒見過這個家伙?大家可能會有這疑問。接下來我們就來跟您抽絲剝繭,步步深入,看看它到底是何方妖孽。
? ? ? ?iptables在加載動態庫時用的是dlopen()函數,在這篇博文中我有介紹。_init()定義在xtables.h中,是共享庫用來初始化全局變量和對象用的,其定義如下:
? ? #define _init__attribute__((constructor)) my_init
? ? ? ?用__attribute__((constructor))來定義的函數,表示函數是構造函數,在main執行之前被調用;相應的用__attribute__((destructor))析構函數,在main退出時執行。void _init(void)就相當于是__attribute__((constructor)) _INIT(void),其實不管函數名定義成什么都會被執行到。
在iptables中當我們調用dlopen函數來加載動態庫時,率先執行每個動態庫里的_init()函數,而該函數要么是將該match注冊到全局鏈表xtables_matches里,或者是將target注冊到全局鏈表xtables_targets中。=================================華麗麗的開始=======================================
iptables的命令解析流程
這里我們僅以ipv4協議為例進行分析。iptables-1.4.0.tar.gz源代碼中,iptables命令的入口文件為iptables-standalone.c,其中主函數為main或者iptables_main。主函數中,所作的事情也很明了,其流程如下:? ? ? ?當前,用戶空間的iptables工具的絕大多數模塊都是以動態共享庫so的形式。使用動態庫的優點也是顯而易見的:編譯出來的iptables命令比較小,動態庫方式使得對于iptables的擴充非常方便。如果你非要去研究一下init_extensions函數的話,那么可以在iptables源碼包的extensions/Makefile文件里找點思路。這里,我不會對其進行分析。
?
? ? ? ?命令行參數解析do_command()【位于iptable.c文件中】
該函數是iptables用于解析用戶輸入參數的核心接口函數,其函數原型為:
int do_command(int argc, char *argv[],char **table, iptc_handle_t *handle);
argc和argv是由用戶傳遞過來的命令行參數;
table所操作的表名,對應命令行就是-t參數后面的值,如果用戶沒有指定-t參數時,默認為filter表;
? ? ? ?handle這個結構比較重要,它用于保存從內核返回的由table所指定的表的所有信息,后續對表及其其中的規則操作時都用的該變量;
前面我們在分析netfilter的時候提到過,用戶空間和內核空間在表示match以及target時采用了不同的結構體定義。用戶空間的match結構體定義為:
| struct xtables_match?????????????????????? #defineiptables_target xtables_target { ???????? struct xtables_match *next; ???????? … ???????? void (*help)(void); ???????? /* Initialize the match. */ ???????? void (*init)(struct xt_entry_match *m); ???????? … ???????? /* Ignore these men behind the curtain: */ ???????? unsigned int option_offset; ???????? struct xt_entry_match *m;? #內核中的match結構 ???????? unsigned int mflags; ??? … }; |
? ? ? ?該結構是iptables在加載它所支持的所有match模塊的時候所用到的結構體,例如time匹配模塊、iprange匹配模塊等。也就是說,如果你要開發自己的用戶空間match的話,那么你必須實例化上面這樣一個結構體對象,然后實現它相應的方法,諸如init、help、parse等等。
? ? ? ?真正用在我們所配置的iptables規則里的匹配條件,是由下列類型表示:
| struct xtables_rule_match??????? #defineiptables_rule_match xtables_rule_match { ???????? struct xtables_rule_match *next; ???????? struct xtables_match *match; ???????? unsigned int completed; }; |
可以看到,xtables_rule_match是將xtables_match組織成了一個鏈表而已。這也正和我們的意愿,因為一條規則里有可能會有多個match條件,而在解析的時候我們只要將我們規則里所用的match通過一個指針指向iptables目前所支持的那個模塊,在后面的使用過程中就可以直接調用那個match模塊里的所有函數了。這樣即提高的訪問效率,又節約了系統內存空間。
? ? ? ?同樣的,用戶空間的target也類似,留給大家自己去研究。
? ? ? ?iptables最常用的命令格式無非就是顯示幫助信息,或者操作規則,例如:
【幫助信息格式】
iptables [-[m|j|p] name ] -h 顯示名為name的match模塊(m)、target模塊(j)或協議(p)的詳細幫助信息。
OK,我們以下面的規則為例,和大家探討一下iptables對其的解析流程。
? ? ? ?iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8–d 10.1.28.184 –j ACCEPT
? ? ? ?在博文三中,我們知道內核中用于表示一條規則的數據結構是struct ipt_entry{}類型,那么iptables對于輸入給它的所有參數最終也要變成這樣的格式。而我們在閱讀iptables源碼時發現,它確實在do_command()函數開始部分定義了一個struct ipt_entry fw;后面當iptables解析傳遞給它的輸入參數時,主要做的事情,就是對該結構體相關成員變量的初始化填充。閑話不多說,let's rock。
(1)、命令控制解析:-A INPUT
? ? ? iptables –A INPUT –ieth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –jACCEPT
? ? ? ?對于“ADRILFZNXEP”這些控制命令來說,其核心處理函數為add_command()函數。
? ? ? ?該函數主要將命令行的控制參數解析出來,然后賦值給一個位圖變量command,該變量的每一位bit表示一種操作。add_command()的函數原型定義如下(iptables.c):
staticvoidadd_command(unsignedint *cmd, const int newcmd, const int othercmds, int invert)
參數說明:
? ? ? ?cmd:用于保存控制參數解析結果的位圖標志變量;
? ? ? ?newcmd:用戶所輸入的控制變量,是一些預定義的宏,定義在iptables.c文件中,如下:
| #define CMD_NONE????????????????? ??????? 0x0000U #define CMD_INSERT?????????????? ??????? 0x0001U #define CMD_DELETE?????????????? ??????? 0x0002U #define CMD_DELETE_NUM??????????? 0x0004U #define CMD_REPLACE??????????? ??? 0x0008U #define CMD_APPEND???????????? ??????? 0x0010U #define CMD_LIST??????????? ??????? 0x0020U #define CMD_FLUSH???????????????? ??????? 0x0040U #define?CMD_ZERO?????????????????? ??????? 0x0080U #define CMD_NEW_CHAIN????????????? ??? 0x0100U #define CMD_DELETE_CHAIN???????? ??? 0x0200U #define CMD_SET_POLICY??????????????? ??? 0x0400U #define CMD_RENAME_CHAIN????? ??? 0x0800U |
? ? ? ?othercmd:在上面這11個控制參數中,只有CMD_ZERO需要輔助額外參數,因為從iptables -Z chainname的執行結果來看,它最后還會輸出清空后的鏈的實際情況。因此,當用戶的iptables命令中有-Z參數時,cmd默認的會被附加一個CMD_LIST特性。其他10個控制參數時,othercmd參數均為CMD_NONE。
? ? ? ?invert:表示命令中是否有取反標志“!”。因為這11個控制參數是沒有取反操作的,因此,這個值均為FALSE(即0)。
????當解析完iptables -A INPUT … 后,command=0x0010U,chain=“INPUT”。然后將invert=FALSE,重新進入while循環,解析剩下的參數。
(2)、解析接口:-i eth0
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8–d 10.1.28.184 –j ACCEPT
? ? ??注意前面講解的關于getopt_long()函數在執行過程中兩個關鍵參數的值及其變化情況。當解析接口的時候optarg=“eth0”,optind=indexof(-p)。
check_inverse(optarg, &invert, &optind, argc);函數用于判斷接口是否有取反標志,如果有取反標志,則將invert=TRUE,同時optind++,然后它指向后面的接口名,并返回TRUE;如果沒有,則直接返回FALSE。
在接下來執行set_option(&options,OPT_VIANAMEIN, &fw.ip.invflags,invert);同樣的,options也是一個位圖標志變量,其取值分別如下(定義在iptables.c文件中):
| #define OPT_NONE ???????????????????? 0x00000U #define OPT_NUMERIC?? ?????? ??????????0x00001U #define OPT_SOURCE????? ???????????????????? 0x00002U #define OPT_DESTINATION???? ???????????? 0x00004U #define OPT_PROTOCOL ???????????????? 0x00008U #define OPT_JUMP ???????????????????? 0x00010U #define OPT_VERBOSE??? ???????????????? 0x00020U #define OPT_EXPANDED ???????????????? 0x00040U #define OPT_VIANAMEIN??????? ???????????????? 0x00080U #define OPT_VIANAMEOUT??? ???????????? 0x00100U #define OPT_FRAGMENT??? ???????????????0x00200U #define OPT_LINENUMBERS ???????????????0x00400U #define OPT_COUNTERS ????????????????0x00800U #define NUMBER_OF_OPT???? ???????????????? 12 |
? ? ? ?然后根據check_inverse()函數解析出來的invert的值來設置fw.ip.invflags相應的標志位,該值也是個位圖標志變量,其可取的值由全局數組inverse_for_options[]來限定(iptables.c):
| static int inverse_for_options[NUMBER_OF_OPT] = { /* -n */ 0, /* -s */ IPT_INV_SRCIP,???????? #這六個宏均定義在ip_tables.h文件中 /* -d */ IPT_INV_DSTIP, /* -p */ IPT_INV_PROTO, /* -j */ 0, /* -v */ 0, /* -x */ 0, /* -i */ IPT_INV_VIA_IN, /* -o */ IPT_INV_VIA_OUT, /* -f */ IPT_INV_FRAG, /*--line*/ 0, /* -c */ 0, }; |
執行parse_interface(argv[optind-1],fw.ip.iniface,fw.ip.iniface_mask);將接口名稱賦值給fw.ip.iniface,然后再設置該接口的mask。如果接口中沒有正則匹配表達式(即“+”),則mask=0xFFFFFFFF。細心的朋友到這里可能就有疑問了:接口名不是保存在optarg中么,為什么要通過argv[optind-1]來獲取呢?我們簡單分析對比一下:
如果是“-i eth0”,那么optarg和argv[optind-1]的值相同,大家可以通過前面我給的那個demo例子去驗證一下;
如果是“-i ! eth0”,情況就不一樣了。注意看代碼,此時optarg=“!”,而arg[optind-1]才是真正的接口名“eth0”。
(3)、解析協議字段:-p tcp
iptables –A INPUT–i eth0–p tcp --syn –s 10.0.0.0/8–d 10.1.28.184 –j ACCEPT
check_inverse(optarg,&invert,&optind, argc); 檢查協議字段是否有取反標志
? ? ? ?set_option(&options,OPT_PROTOCOL,&fw.ip.invflags,invert); 根據invert的值來設置options和fw.ip.invflags。這和前面的接口解析是類似的。
? ? ? ?然后,將協議名稱解析成對應的協議號,例如ICMP=1,TCP=6,UDP=17等等。
fw.ip.proto = parse_protocol(protocol);
? ? ? ?因為iptables在-p參數后面支持數字格式的協議描述,因此parse_protocol()函數首先嘗試去解析數字字符串,將其轉換成一個0-255之間的整數。如果轉換成功,則將轉換結果賦值給fw.ip.proto。如果轉換失敗,首先檢查-p后面的參數是不是“all”。如果是則直接返回,否則調用getprotobyname()函數從/etc/protocols中去解析。這里getprotobyname函數主要根據傳遞給它的協議名返回一個struct protoent{}結構體的對象(詳見man手冊)。解析成功則返回;否則,在用戶自定義的結構體數組chain_protos[]中去解析,其定義如下:
| static const struct pprot chain_protos[] = { ???????? { "tcp", IPPROTO_TCP }, ???????? { "udp", IPPROTO_UDP }, ???????? { "udplite", IPPROTO_UDPLITE }, ???????? { "icmp", IPPROTO_ICMP }, ???????? { "esp", IPPROTO_ESP }, ???????? { "ah", IPPROTO_AH }, ???????? { "sctp", IPPROTO_SCTP }, ???????? { "all", 0 }, }; |
if(fw.ip.proto == 0&& (fw.ip.invflags & IPT_INV_PROTO))
???????? exit_error(PARAMETER_PROBLEM,"rulewould never match protocol");
如果協議類型為“all”并且協議字段-p后面還有取反標志,即-p! all,表示不匹配任何協議。這樣的規則是沒有任何意義的,iptables也不允許這樣的規則存在,因此會給出錯誤提示信息并退出。
(4)、解析tcp協議模塊的具體控制參數:--syn
iptables –A INPUT–i eth0 –p tcp--syn –s 10.0.0.0/8–d 10.1.28.184 –j ACCEPT
針對于--syn符號,會跳轉到switch語句的default處執行。因為目前還沒有解析到target,因此target=NULL。命令行中沒有-m,因此matches=NULL,matchp=NULL,m=NULL。
if (m == NULL&&protocol&&
(!find_proto(protocol,DONT_LOAD,options&OPT_NUMERIC, NULL)
?????????????????? ? || (find_proto(protocol,DONT_LOAD,options&OPT_NUMERIC, NULL)
?????????????????? ???????? ?????????? && (proto_used == 0))
?????????)
???????? && (m = find_proto(protocol, TRY_LOAD,options&OPT_NUMERIC,&matches))) {
? ? ? ?這個邏輯條件判斷已經很清晰了:
? ? ? ?如果命令行中沒有-m,但是有-p,并且find_proto執行失敗或者執行成功且協議本身還沒有被用過proto_used=0,最后我們試圖去加載so庫之后再去執行find_proto。當第三執行find_proto函數時,會運行如下的代碼部分,因為我們這次是以TRY_LOAD方式執行的:
| #ifndef NO_SHARED_LIBS ???????? if (!ptr && tryload != DONT_LOAD && tryload != DURING_LOAD) { ?????????????????? char path[strlen(lib_dir) + sizeof("/.so") ??????????????????????????? ? + strlen(afinfo.libprefix) + strlen(name)]; ?????????????????? sprintf(path, "%s/libxt_%s.so", lib_dir, name); ?????????????????? if (dlopen(path, RTLD_NOW) != NULL) ??????????????????????????? /* Found library.? If it didn't register itself, ??????????????????????????? ?? maybe they specified target as match. */ ??????????????????????????? ptr = find_match(name, DONT_LOAD, NULL); |
以上代碼會將我們的…/libxt_tcp.so庫加載到當前進程的運行空間中,并導出相關環境變量,此時tcp的模塊在執行dlopen時就已經被掛到xtables_matches鏈表中了。最后再在find_match()函數(find_proto()函數的內部其實就是調的find_match()而已)里遞歸的調用一次自己。
第二次遞歸調用自己時,首先會申請一塊大小為structxtables_match{}的內存空間由變量clone來指向,并將tcp.so模塊中的信息保存其中,并設置clone->mflags = 0。然后再申請一塊大小為structxtables_rule_match{}大小的內存空間,由變量newentry來保存,將tcp的so模塊的信息賦給結構體的相關成員變量。
| for (i = matches; *i; i = &(*i)->next) {? #不會執行這個for循環 ???????? printf("i=%s\n",(i==NULL?"NULL":i)); ?????????????????? if (strcmp(name, (*i)->match->name) == 0) ??????????????????????????? (*i)->completed = 1; } newentry->match = ptr; ????//就是前面的clone所指向的地址空間。 newentry->completed = 0; newentry->next = NULL; *i = newentry;??????? #因為matches是個二級指針,因此這里的*i即*matches=newentry return ptr;????????? #ptr目前就保存了和tcp模塊所有相關的內容,ptr最后返回去會賦給 下面的變量m |
然后回到do_command()中繼續執行:
| /* Try loading protocol */ size_t size; ?????????????????????????????????????????????? proto_used = 1; ? printf("Ready to load %s's match\n",protocol); ? size = IPT_ALIGN(sizeof(struct ipt_entry_match))+m->size; ? m->m = fw_calloc(1, size);????????#為內核態的xt_entry_match結構分配存儲空間 m->m->u.match_size = size;???????#整個tcp_match的大小 strcpy(m->m->u.user.name,m->name); set_revision(m->m->u.user.name,m->revision); if (m->init != NULL) ???????? m->init(m->m);#調用tcp_init函數初始化內核中的match結構,主要是將xt_entry_match尾部的data數組進行初始化。對TCP來說就是將源、目的端口置為0xFFFF。這并不是重點。 ? opts = merge_options(opts,m->extra_opts, &m->option_offset); #重點是merge_options操作,將tcp_opts中的數據合并到全局變量opts中去 optind--; continue; #前面說過optind指向當前參數下一個緊挨著的參數的下標。目前只是完成了解析--syn的初始化工作,還并沒有對--syn進行解析,因此需要optind--,然后開始解析--syn |
然后程序繼續執行while循環,這次依然進入default段進行處理,并進入if(!target||…
只不過這次matches已經不為NULL,因此matchp就可以取到值,matchep即指向了tcp模塊。將解析的結果賦給fw結構體的相應成員,并將代表tcp模塊的iptables_match賦給m。
| if (!target|| !(target->parse(c - target->option_offset,argv, invert,&target->tflags,&fw, &target->t))) { ????for (matchp = matches; matchp; matchp = matchp->next) { ? ? ? ? ?if (matchp->completed) ??????????????continue; ??????? #調用tcp模塊的parse函數,即tcp_parse ????????if (matchp->match->parse(c - matchp->match->option_offset,argv, invert, ????????????????????????????????????&matchp->match->mflags,&fw, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &matchp->match->m)) ??????????????break; ?} ? m = matchp ? matchp->match : NULL; ? if(m==NULL &&…) ?#就不會再執行這里了 ?… … |
至此,對--syn的解析就已經完成了。
(5)、解析源、目的地址:-s10.0.0.0/8 -d 10.1.28.184
iptables –A INPUT–i eth0 –p tcp--syn –s10.0.0.0/8 –d10.1.28.184 –j ACCEPT
| 解析源地址: check_inverse(optarg, &invert, &optind, argc); set_option(&options,OPT_SOURCE, &fw.ip.invflags,invert); shostnetworkmask = argv[optind-1]; #暫存源地址,后面要做進一步分析x.x.x.x/xx ? 解析目的地址: check_inverse(optarg, &invert, &optind, argc); set_option(&options,OPT_DESTINATION, &fw.ip.invflags,invert); dhostnetworkmask = argv[optind-1]; #暫存目的地址,后面要做進一步分析x.x.x.x/xx |
?
(6)、解析target:-j ACCEPT
iptables –A INPUT–i eth0 –p tcp --syn–s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
首先判斷target字串是否合法,jumpto= parse_target(optarg);
然后在xtables_targets全局鏈表里查找相應的target。因為目前只有標準target,因此最后加載libxt_standard.so庫,對應的文件為libxt_standard.c。
| static struct xtables_targetstandard_target = { ???????? .family?????????????? = AF_INET, ???????? .name??????????????? = "standard", ???????? .version??? = IPTABLES_VERSION, ???????? .size????????? = XT_ALIGN(sizeof(int)), ???????? .userspacesize???????? = XT_ALIGN(sizeof(int)), ???????? .help?????????????????? = standard_help, ???????? .parse??????????????? = standard_parse, }; |
我們可以看到標準target(諸如ACCEPT、DROP、RETURN、QUEUE等)是沒有init函數和extra_opts變量的。因此,要做的操作只有下面幾個:
| if (target) { size_t size; ? size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ target->size; ? target->t = fw_calloc(1, size); ???????#為內核中的xt_entry_target分配存儲空間 target->t->u.target_size = size; strcpy(target->t->u.user.name, jumpto); set_revision(target->t->u.user.name,target->revision); ? #以下操作均不執行。因為target->init和target->extra_ops都為NULL???????????????????????????? if (target->init != NULL) ?????????????????? target->init(target->t); opts = merge_options(opts, target->extra_opts, &target->option_offset); } |
? ? 至此,對用戶的命令行輸入的參數就算全部解析完成了,其中:
- 控制參數的解析結果保存在位圖變量command中;
- ?規則參數的解析結果保存在位圖變量options中;
- ?源地址保存在臨時變量shostnetworkmask中;
- ?目的地址保存在臨時變量dhostnetworkmask中;
? ? 并完成了對struct ipt_entry{}中struct ipt_ip{}結構體成員的初始化,即對fw.ip的初始化。
?
(7)、參數和合法性檢查
如果是“ADRI”操作但是沒有指定源目的地址,默認將其置為全網段0.0.0.0/0。然后,設置源目的掩碼fw.ip.smsk和fw.ip.dmsk。
檢查command和options的匹配性generic_opt_check(command, options)。它們的相關性由一個二維數組commands_v_options[][]來限定:至此,所有的解析、校驗工作都已完成。接下來我們將要探究,iptables如何與內核交互的問題。
(十二)洞悉linux下的Netfilter&iptables:iptables命令行工具源碼解析【下】
iptables用戶空間和內核空間的交互
iptables目前已經支持IPv4和IPv6兩個版本了,因此它在實現上也需要同時兼容這兩個版本。iptables-1.4.0在這方面做了很好的設計,主要是由libiptc庫來實現。libiptc是iptables control library的簡稱,是Netfilter的一個編程接口,通常被用來顯示、操作(查詢、修改、添加和刪除)netfilter的規則和策略等。使用libipq庫和ip_queue模塊,幾乎可以實現任何在內核中所實現的功能。
? ? ? ? libiptc庫位于iptables源碼包里的libiptc目錄下,共六個文件還是比較容易理解。我們都知道,運行在用戶上下文環境中的代碼是可以阻塞的,這樣,便可以使用消息隊列和?UNIX?域套接字來實現內核態與用戶態的通信。但這些方法的數據傳輸效率較低,Linux?內核提供?copy_from_user()/copy_to_user()?函數來實現內核態與用戶態數據的拷貝,但這兩個函數會引發阻塞,所以不能用在硬、軟中斷中。一般將這兩個特殊拷貝函數用在類似于系統調用一類的函數中,此類函數在使用中往往"穿梭"于內核態與用戶態。此類方法的工作原理為:
?
? ? ? ??其中相關的系統調用是需要用戶自行編寫并載入內核。一般情況都是,內核模塊注冊一組設置套接字選項的函數使得用戶空間進程可以調用此組函數對內核態數據進行讀寫。我們的libiptc庫正是基于這種方式實現了用戶空間和內核空間數據的交換。
????為了后面便于理解,這里我們簡單了解一下在socket編程中經常要接觸的兩個函數:
int setsockopt(intsockfd, int proto, intcmd, void *data, intdatalen)
intgetsockopt(intsockfd, intproto, int cmd, void *data, intdatalen)
這個兩個函數用來控制相關socket文件描述符的一些選項值,如設置(獲取)接受或發送緩沖區的大小、設置(獲取)接受或發送超時值、允許(禁止)重用本地端口和地址等等。
參數說明:
sockfd:為socket的文件描述符;
proto:sock協議,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高層的socket是都可以使用低層socket的命令字?的;
cmd:操作命令字,由自己定義,一般用于擴充;
data:數據緩沖區起始位置指針,set操作時是將緩沖區數據寫入內核,get的時候是將內核中的數據讀入該緩沖區;
datalen:參數data中的數據長度。
?
我們可以通過擴充新的命令字(即前面的cmd字段)來實現特殊應用程序的內核與用戶空間的數據交換,內核實現新的sockopt命令字有兩類:一類是添加完整的新的協議后引入;一類是在原有協議命令集的基礎上增加新的命令字。以netfilter為例,它就是在原有的基礎上擴展命令字,實現了內核與用戶空間的數據交換。Netfilter新定義的命令字如下:
setsockopt新增命令字:
#define IPT_SO_SET_REPLACE //設置規則
#defineIPT_SO_SET_ADD_COUNTERS???//加入計數器
getsockopt新增命令字;
#defineIPT_SO_GET_INFO???????????????//獲取ipt_info
#defineIPT_SO_GET_ENTRIES?????????//獲取規則
#define IPT_SO_GET_REVISION_MATCH?//獲取match
#defineIPT_SO_GET_REVISION_TARGET??????//獲取target
???????一個標準的setsockopt()操作的調用流程如下:
?
在ip_setsockopt調用時,如果發現是一個沒有定義的協議,并且判斷現在這個optname是否為netfilter所設置,如果是則調用netfilter所設置的特殊處理函數,于是加入netfilter對sockopt特殊處理后,新的流程如下:
?
? ? ? ? netfitler對于會實例化一些struct nf_sockopt_ops{}對象,然后通過nf_register_sockopt()將其注冊到全局鏈表nf_sockopts里。
| struct?nf_sockopt_ops { ?????????struct?list_head?list; ?????????int pf; ? ?????????/* Non-inclusive ranges: use 0/0/NULL to never get called. */ ?????????int set_optmin; ?????????int set_optmax; ?????????int (*set)(struct sock *sk, int optval, void __user *user, unsigned int len); ?????????int (*compat_set)(struct sock *sk, int optval,void __user *user, unsigned int len); ? ?????????int get_optmin; ?????????int get_optmax; ?????????int (*get)(struct sock *sk, int optval, void __user *user, int *len); ?????????int (*compat_get)(struct sock *sk, int optval,void __user *user, int *len); ? ?????????/* Number of users inside set() or get(). */ ?????????unsigned int use; ?????????struct task_struct *cleanup_task; }; |
?
? ? ? ? ?繼續回到libiptc中。libiptc庫中的所有函數均以“iptc_”開頭,主要有下面一些接口(節選自libiptc.h):
| typedef?struct iptc_handle?*iptc_handle_t; ? /* Does this chain exist? */ int?iptc_is_chain(const char *chain, const?iptc_handle_t?handle); ? /* Take a snapshot of the rules.??Returns NULL on error. */ iptc_handle_t?iptc_init(const char *tablename); ? /* Cleanup after iptc_init(). */ void?iptc_free(iptc_handle_t?*h); ? /* Iterator functions to run through the chains.??Returns NULL at end. */ const char *iptc_first_chain(iptc_handle_t?*handle); const char *iptc_next_chain(iptc_handle_t?*handle); ? /* Get first rule in the given chain: NULL for empty chain. */ const struct ipt_entry *iptc_first_rule(const char *chain,iptc_handle_t?*handle); /* Returns NULL when rules run out. */ const struct ipt_entry *iptc_next_rule(const struct ipt_entry *prev,iptc_handle_t?*handle); ? /* Returns a pointer to the target name of this entry. */ const char *iptc_get_target(const struct ipt_entry *e,iptc_handle_t?*handle); ? /* Is this a built-in chain? */ int?iptc_builtin(const char *chain, const?iptc_handle_t?handle); ? int?iptc_append_entry(const ipt_chainlabel chain, ?????????????????????????const struct ipt_entry *e, ?????????????????????????iptc_handle_t?*handle); ? /* Zeroes the counters in a chain. */ int?iptc_zero_entries(const ipt_chainlabel chain,iptc_handle_t?*handle); ? /* Creates a new chain. */ int?iptc_create_chain(const ipt_chainlabel chain,iptc_handle_t?*handle); ? /* Makes the actual changes. */ int?iptc_commit(iptc_handle_t?*handle); ? |
? ? ? ? 上面這些接口都是為IPv4定義了,同樣的IPv6的接口均定義在libip6tc.h頭文件中,都以“ip6tc_”開頭(如此說來,IPv4的頭文件應該叫libip4tc.h才比較合適)。然后在libip4tc.c和libip6tc.c文件中分別通過宏定義的形式將IPv4和IPv6對外的接口均統一成“TC_”開頭的宏,并在libiptc.c中實現這些宏即可。如下圖所示:
?
? ? ? ? 這里我們看到iptables-v4和iptables-v6都和諧地統一到了libiptc.c中,后面我們分析的時候只要分析這些相關的宏定義的實現即可。
? ? ? 在繼續往下分析之前我們先看一下STRUCT_TC_HANDLE這個比較拉風的結構體,它用于存儲我們和內核所需要交換的數據。說的通俗一些,就是從內核中取出的表的信息會存儲到該結構體類型的變量中;當我們向內核提交iptables變更時,也需要一個該結構體類型的變量用于存儲我們所要提交的數據。(定義在ip_tables.h頭文件中)
| 適用于當getsockopt的參數為IPT_SO_GET_INFO,用于從內核讀取表信息 struct ipt_getinfo???????????????#define?STRUCT_GETINFO?struct ipt_getinfo { ?????????/* Which table: caller fills this in. */????#從內核取出的表信息會存儲在該結構體中 ?????????char name[IPT_TABLE_MAXNAMELEN]; ?????????/* Kernel fills these in. */ ?????????unsigned int?valid_hooks; /* Which hook entry points are valid: bitmask */ ?????????unsigned int?hook_entry[NF_IP_NUMHOOKS]; // Hook entry points: one per netfilter hook. ?????????unsigned int?underflow[NF_IP_NUMHOOKS]; /* Underflow points. */ ?????????unsigned int?num_entries; /* Number of entries */ ?????????unsigned int?size; /* Size of entries. */ }; |
還有一個成員entries用保存表中的所有規則信息,每條規則都是一個ipt_entry的實例:
| /* The argument to IPT_SO_GET_ENTRIES. */ struct ipt_get_entries { ?????????/* Which table: user fills this in. */ ?????????char?name[IPT_TABLE_MAXNAMELEN]; ?????????unsigned int?size; /* User fills this in: total entry size. */ ? ?????????struct?ipt_entry?entrytable[0]; /*內核里表示規則的結構,參見博文三. */ }; |
????????
(一)、從內核獲取數據:iptc_init()
都說“磨刀不誤砍柴工”,接下來我們繼續上一篇中do_command()函數里剩下的部分。*handle?=iptc_init(*table);?這里即根據表名table去從內核中獲取該表的自身信息和表中的所有規則。關于表自身的一些信息存儲在handle->info成員里;表中所有規則的信息保存在handle->entries成員里。
? ? ? 如果handle獲取失敗,則嘗試加載完內核中相應的ko模塊后再次執行iptc_init()函數。
? ? ? 然后,針對“ADRI”操作需要做一些合法性檢查,諸如-o選項不能用在PREROUTING和INPUT鏈中、-i選項不能用在POSTROUTING和OUTPUT鏈中。
| if (target &&?iptc_is_chain(jumpto, *handle)) { ???????????????????fprintf(stderr,"Warning: using chain %s, not extension\n",jumpto); ?????????????if (target->t) ????????????????????????????free(target->t); ??????????????????????????? ???????????????????printf("Target is a chain,but we have gotten a target,then free it!\n"); ? ???????????????????target = NULL; } |
如果-j XXX?后面的XXX是一條用戶自定義規則鏈,但是之前卻解析出了標準target,那么需要將target的空間釋放掉。很明顯,目前我們的-j ACCEPT不會執行到這里。
| if (!target??#如果沒有指定target。同樣,我們的規則也不會執行到這里 && (strlen(jumpto) == 0|| iptc_is_chain(jumpto, *handle)) #或者target是一條鏈或為空 ) { ?????????size_t size; ????… …???????? } |
? ? ? ? 因為我們的target為ACCEPT,已經被完全正確解析,即target!=NULL。后面我們會執行else條件分子如下的代碼:
e =generate_entry(&fw, matches, target->t);
用于生成一條iptables的規則,它首先會為e去申請一塊大小n*match+target的空間,其中n為用戶輸入的命令行中的match個數,target為最后的動作。這里很明顯,我們的命令只有一個tcp的match,target是標準target,即ACCEPT。將已經解析的fw賦給e,并對結構體e中其他的成員進行初始化,然后將相應的match和target的數據拷貝到e中對應的成員中。
| size = sizeof(struct ipt_entry); for (matchp = matches; matchp; matchp = matchp->next) ?????????size += matchp->match->m->u.match_size; ? e = fw_malloc(size + target->u.target_size); *e = *fw; e->target_offset = size; e->next_offset = size + target->u.target_size; ? size = 0; for (matchp = matches; matchp; matchp = matchp->next) { ?????????memcpy(e->elems + size, matchp->match->m, matchp->match->m->u.match_size); ?????????size += matchp->match->m->u.match_size; } memcpy(e->elems + size, target, target->u.target_size); |
? ? ? ? 最后所生成的規則e,其內存結構如下圖所示:
?
? ? ? ? 這里再聯系我們對內核中netfilter的分析就很容易理解了,一旦我們獲取一條規則ipt_entry的首地址,那么我們能通過target_offset很快獲得這條規則的target地址,同時也可以通過next_offset獲得下一條ipt_entry規則的起始地址,很方便我們到時候做數據包匹配的操作。
?緊接著就是對解析出來的command命令進行具體操作,這里我們是-A命令,因此最后command命令就是CMD_APPEND,這里則執行append_entry()函數。
ret =?append_entry(chain,??#鏈名,這里為INPUT
e,??????#將用戶的命令解析出來的最終的規則對象
nsaddrs,??#-s?后面源地址的個數
saddrs,???#用于保存源地址的數組
ndaddrs,??#-d?后面的目的地址的個數
daddrs,???#用于保存目的地址的數組
options&OPT_VERBOSE,#iptables命令是否有-v參數
handle???#從內核中取出來的規則表信息
);
?在append_entry內部調用了iptc_append_entry(chain, fw, handle),其實就是由宏即TC_APPEND_ENTRY所表示的那個函數。該函數內部有兩個值得注意的結構體類型struct chain_head{}和struct rule_head{},分別用于保存我們所要操作的鏈以及鏈中的規則:
| struct chain_head { ?????????struct list_head list; ?????????char name[TABLE_MAXNAMELEN]; ?????????unsigned int hooknum;?????????????/* hook number+1 if builtin */ ?????????unsigned int references;?/*?有多少-j?指定了我們的名字?*/ ?????????int verdict;??????????????????????????/* verdict if builtin */ ?????????STRUCT_COUNTERS counters;????????/* per-chain counters */ ?????????struct counter_map counter_map; ?????????unsigned int num_rules;???????????/*?本鏈中的規則數*/ ?????????struct list_head rules;?????????????/*?本鏈中所有規則的入口點?*/ ? ?????????unsigned int index;???????????/* index (needed for jump resolval) */ ?????????unsigned int head_offset;????????/* offset in rule blob */ ?????????unsigned int foot_index;?/* index (needed for counter_map) */ ?????????unsigned int foot_offset;??????????/* offset in rule blob */ }; ? struct rule_head { ?????????struct list_head list; ?????????struct chain_head *chain; ?????????struct counter_map counter_map; ?????????unsigned int index;???????????/* index (needed for counter_map) */ ?????????unsigned int offset;??????????/* offset in rule blob */ ? ?????????enum iptcc_rule_type type; ?????????struct chain_head?*jump;??????/* jump target, if IPTCC_R_JUMP */ ? ?????????unsigned int size;??????????????/* size of entry data */ ?????????STRUCT_ENTRY entry[0];???#真正的規則入口點?sizeof計算時不會包含這個字段 }; |
TC_APPEND_ENTRY的函數實現:
| int TC_APPEND_ENTRY(const?IPT_CHAINLABEL?chain, ???????????????????const?STRUCT_ENTRY?*e, ???????????????????TC_HANDLE_T?*handle)????#注意:這里的handle是個二級指針 { ?????????struct chain_head?*c; ?????????struct rule_head?*r; ? ?????????iptc_fn = TC_APPEND_ENTRY; ?????????if (!(c =?iptcc_find_label(chain, *handle))) {?????? #根據鏈名查找真正的鏈地址賦給c,此時c就指向了INPUT鏈的內存, #包括INPUT中的所有規則和它的policy等 ???????????????????DEBUGP("unable to find chain `%s'\n", chain); ???????????????????errno = ENOENT; ???????????????????return 0; ?????????} ? ?????????if (!(r =?iptcc_alloc_rule(c,?e->next_offset))) { #ipt_entry的next_offset即指明了下一條規則的起始地址,同時這個值也說明了本條規則所占了存儲空間的大小。這里所申請的空間大小=sizeof(rule_head)+當前規則所占的空間大小。 ???????????????????DEBUGP("unable to allocate rule for chain `%s'\n", chain); ???????????????????errno = ENOMEM; ???????????????????return 0; ?????????} ? ?????????memcpy(r->entry, e, e->next_offset);???????#把規則拷貝到柔性數組entry中去 ?????????r->counter_map.maptype = COUNTER_MAP_SET; ? ?????????if (!iptcc_map_target(*handle, r)) {???#主要是設置規則r的target,后面分析。 ???????????????????DEBUGP("unable to map target of rule for chain `%s'\n", chain); ???????????????????free(r); ???????????????????return 0; ?????????} ? ?????????list_add_tail(&r->list, &c->rules); #將新規則r添加在鏈c的末尾 ?????????c->num_rules++;??????????????#同時將鏈中的規則計數增加 ? ?????????set_changed(*handle);????#因為INPUT鏈中的規則已經被改變,則handle->changed=1; ?????????return 1; } |
? ? ? ? ?接下來分析一下設置target時其函數內部流程:
| static int iptcc_map_target(const TC_HANDLE_T handle, ????????????struct rule_head *r) { ?????????STRUCT_ENTRY?*e = r->entry;?????????????????#取規則的起始地址 ?????????STRUCT_ENTRY_TARGET?*t = GET_TARGET(e);????#取規則的target ? ?????????/* Maybe it's empty (=> fall through) */ ?????????if (strcmp(t->u.user.name, "") == 0) { #如果沒有指定target,則將規則類型設為“全放行” ???????????????????r->type = IPTCC_R_FALLTHROUGH; ???????????????????return 1; ?????????} ? ?????????/* Maybe it's a standard target name... */ #因為都是標準target,因此將target中用戶空間的user.name都置為空,設置verdict, #并將rule_head中的type字段為IPTCC_R_STANDARD ?????????else if (strcmp(t->u.user.name, LABEL_ACCEPT) == 0) ???????????????????return?iptcc_standard_map(r, -NF_ACCEPT - 1); ?????????else if (strcmp(t->u.user.name, LABEL_DROP) == 0) ???????????????????return?iptcc_standard_map(r, -NF_DROP - 1); ?????????else if (strcmp(t->u.user.name, LABEL_QUEUE) == 0) ???????????????????return?iptcc_standard_map(r, -NF_QUEUE - 1); ?????????else if (strcmp(t->u.user.name, LABEL_RETURN) == 0) ???????????????????return?iptcc_standard_map(r, RETURN); ?????????else if (TC_BUILTIN(t->u.user.name, handle)) { ???????????????????/* Can't jump to builtins. */ ???????????????????errno = EINVAL; ???????????????????return 0; ?????????} else { ???????????????????/*?如果跳轉的目標是一條用戶自定義鏈,則執行下列操作*/ ???????????????????struct chain_head *c; ???????????????????DEBUGP("trying to find chain `%s': ", t->u.user.name); ???????????????????c =?iptcc_find_label(t->u.user.name, handle); #找到要跳轉的目的鏈的入口地址 ???????????????????if (c) { ????????????????????????????DEBUGP_C("found!\n"); ????????????????????????????r->type =?IPTCC_R_JUMP;??#將rule_head結構的type字段置為“跳轉” ????????????????????????????r->jump = c;?????????????#跳轉的目標為t->u.user.name所指示的鏈 ????????????????????????????c->references++;?????????#跳轉到的目的鏈因此而被引用了一次,則計數器++ ????????????????????????????return 1; ???????????????????} ???????????????????DEBUGP_C("not found :(\n"); ?????????} ? ?????????/*?如果不是用戶自定義鏈,它一定一個用戶自定義開發的target模塊,比如SNAT、LOG等。If not, kernel will reject... */ ?????????/* memset to all 0 for your memcmp convenience: don't clear version */ ?????????memset(t->u.user.name + strlen(t->u.user.name), ????????????????0, ????????????????FUNCTION_MAXNAMELEN - 1 - strlen(t->u.user.name)); ?????????r->type =?IPTCC_R_MODULE;??#比如SNAT,LOG等會執行到這里 ?????????set_changed(handle); ?????????return 1; } |
?在append_entry()函數最后,將執行的執行結果返回給ret,1表示成功;0表示失敗。然后在做一下善后清理工作,如果命令行中有-v則將內核中表的快照dump一份詳細信息出來顯示給用戶看:
if (verbose >1)
?????dump_entries(*handle);
clear_rule_matches(&matches);?//釋放matches所占的存儲空間
?由struct ipt_entry e;所存儲的規則信息已經被提交給了handle對象對應的成員,因此將e所占的存儲空間也釋放:
if (e != NULL) {
??????????????free(e);
??????????????e= NULL;
}
?將全局變量opts復位,初始化時opts=original_opts。因為在解析--syn時tcp的解析參數被加進來了:
static structoption?original_opts[] = {
??????????????????{"append", 1, NULL, 'A' },
??????????????????{"delete", 1, NULL,??'D' },
??????????????????……
}
至此,do_command()函數的執行就算全部完成了。
?
(二)、向內核提交變更:iptc_commit()
執行完do_command()解析完命令行參數后,用戶所作的變更僅被提交給了handle這個結構體變量,這個變量里的所有數據在執行iptc_commit()函數前都駐留在內存里。因此,在iptables-standalone.c里有如下的代碼語句:
| ret =?do_command(argc, argv, &table, &handle); if (ret) ?????????ret =?iptc_commit(&handle); |
? 當do_command()執行成功后才會去執行iptc_commit()函數,將handle里的數據提交給Netfilter內核。
? ? ? ? iptc_commit()的實現函數為intTC_COMMIT(TC_HANDLE_T *handle),我們只分析IPv4的情形,因此專注于libiptc.c文件中該函數的實現。
? ? ? ? 在TC_COMMIT()函數中,又出現了我們在分析Netfilter中filter表時所見到的一些重要結構體STRUCT_REPLACE?*repl;STRUCT_COUNTERS_INFO?*newcounters;還有前面出現的struct chain_head?*c;結構體。
new_number=?iptcc_compile_table_prep(*handle,&new_size);
iptcc_compile_table_prep()該函數主要做的工作包含幾個方面:
a.初始化handle里每個struct chain_head{}結構體成員中的head_offset、foot_index和foot_offset。
b.對每個鏈(struct chain_head{})中的每條規則,再分別計算它們的offset和index。
c.計算handle所指示的表中所有規則所占的存儲空間的大小new_size,以及規則的總條數new_number。
?接下來,為指針repl;申請存儲空間,所申請的大小為sizeof(struct ipt_replace)+new_size。因為struct ipt_replace{}結構的末尾有一個柔性數組struct ipt_entry entries[0];?它是不計入sizeof的計算結果的。因此,iptables的所有規則實際上是存儲在struct ipt_entry entries[0]柔性數組中的,這里所有規則所占大小已經得到:new_size。
? 因為,每條規則entry都一個計數器,用來記錄該規則處理了多少數據包,注意結構體STRUCT_COUNTERS_INFO{}的末尾也有一個柔性數組struct xt_counters?counters[0];其中struct xt_counters{}才是真正的用于統計數據包的計數器。
然后開始初始化repl結構:
| strcpy(repl->name, (*handle)->info.name); repl->num_entries = new_number; repl->size = new_size; ? repl->num_counters = (*handle)->info.num_entries; repl->valid_hooks = (*handle)->info.valid_hooks; |
? ? ? ? 緊接著對repl結構體中剩下的成員進行初始化,hook_entry[]、underflow[]等。對于用戶自定義鏈,其末尾的target.verdict=RETURN。
setsockopt(sockfd,TC_IPPROTO,?SO_SET_REPLACE,?repl,sizeof(*repl) +repl->size);
會觸發內核去執行前面我們看到的do_ipt_set_ctl()函數,如下:
| static struct nf_sockopt_ops ipt_sockopts = { ?????????.pf??????????????= PF_INET, ?????????.set_optmin?????= IPT_BASE_CTL,???? ? .set_optmax????= IPT_SO_SET_MAX+1, ?????????.set???????????=?do_ipt_set_ctl, ?????????.get_optmin?????= IPT_BASE_CTL, ?????????.get_optmax????= IPT_SO_GET_MAX+1, ?????????.get???????????=?do_ipt_get_ctl, }; |
? ? ? ? 在do_ipt_set_ctl()中其核心還是執行do_replace()函數:
| static int do_replace(void __user *user, unsigned int len) { ?????????int ret; ?????????struct ipt_replace tmp; ?????????struct xt_table_info *newinfo; ?????????void *loc_cpu_entry; ? ?????????if (copy_from_user(&tmp, user, sizeof(tmp)) != 0) ???????????????????return -EFAULT; ? ?????????/* Hack: Causes ipchains to give correct error msg --RR */ ?????????if (len != sizeof(tmp) + tmp.size) ???????????????????return -ENOPROTOOPT; … … } |
? ? ? ?其中copy_from_user()負責將用戶空間的repl變量中的內容拷貝到內核中的tmp中去。然后設置規則計數器newcounters,通過setsockopt系統調用將newcounters設置到內核:
setsockopt(sockfd,TC_IPPROTO,?SO_SET_ADD_COUNTERS,?newcounters, counterlen);
?此時,在do_ipt_set_ctl()中執行的是do_add_counters()函數。至此,iptables用戶空間的所有代碼流程就算分析完了。命令:
iptables –A INPUT –i eth0 –p tcp --syn–s?10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
即被設置到內核的Netfilter規則中去了。
(十三)洞悉linux下的Netfilter&iptables:為防火墻增添功能模塊【實戰】
為netfilter/iptables增添新功能模塊:ipp2p
? ??一個防火墻功能模塊包含兩部分:內核空間的ko模塊和用戶空間的so模塊。如下:
? ? 而且文件的命令都非常有講究。例如我們有個模塊名叫AAA,那么內核中該模塊的文件名一般為ipt_AAA.c和ipt_AAA.h;對應的用戶空間模塊叫libipt_AAA.c。今天我通過簡單的向防火墻添加ipp2p擴展功能模塊的例子,向大家展示一下相關操作和注意事項。
?
下載ipp2p源碼:http://www.ipp2p.org/
????最新版的ipp2p-0.8.2.tar.gz支持的內核2.6.17,iptables支持1.3.1。不過這個不影響,我們稍對其源文件進行修改就可以適應我們2.6.21平臺了。修改后的源碼從“?ipp2p-0.8.2.zip”下載。
? ?下載后對其解壓,然后進到ipp2p目錄下。接下來有兩種方式來編譯模塊:
1、? 直接在該目錄下執行make。(備注:該目錄下的Makefile我已經改過了)
執行完make后ipt_ipp2p.ko和libipt_ipp2p.so就都生成了。
chmod xipt_ipp2p.ko
2、? 依次按下列步驟來操作:
仍然是在ipp2p-0.8.2解壓目錄里,我系統中如下:
然后將每個文件拷貝到對應的目錄下:
在我機器上,內核源碼在/usr/src/linux-2.6.21,iptables源碼在/usr/src/iptables-1.4.0目錄下。
接下來修改/usr/src/linux-2.6.21/net/ipv4/netfilter/ipt_ipp2p.c文件:
同樣修改/usr/src/iptables-1.4.0/extensions/libipt_ipp2p.c文件如下:
? ? 接換到內核源碼目錄/usr/src/linux-2.6.21目錄下
? ? 先備份net/ipv4/netfilter/目錄下原來的Makefile文件,然后照著下面自己寫個新的Makefile。如下所示:
? ? ipt_ipp2p.ko就已經編譯出來了,加上執行權限后將其拷貝到/lib/modules/2.6.21目錄里。
? ? 最后編譯用戶空間的so模塊,進入到/usr/src/iptables-1.4.0:
? ? 因為我系統中有兩個版本的iptables 1.3.5和1.4.0,其中1.3.5的庫文件位于/lib/iptables目錄;1.4.0我將其放置在/lib/iptables-1.4.0/iptables目錄。
最后我們來使用一下ipp2p模塊,執行iptables -m ipp2p -h應該可以看到下列提示:? ? 加載ipp2p內核模塊:
? ? insmod/lib/modules/2.6.21/kernel/net/ipv4/netfilter/ipt_ipp2p.ko
? ? 總結,雖然方法一干凈利落,犀利快捷,可是不利于我們理解netfilter/iptables代碼的組織架構。方法二雖然麻煩,但學的知識更多些。大家各取所需吧。
?
? ? 網上還流傳另外一種通過補丁包的方式來為iptables擴展功能模塊,這里我簡單提一下,因為那玩意兒更easy。一般是到ftp://ftp.netfilter.org/pub/patch-o-matic-ng/下載對應的補丁文件,在本地將其解壓后進入補丁文件夾,一般執行:./runme ipp2p,然后根據提示輸入一些配置路徑就OK了。它會自動幫你完成文件的拷貝,Makefile的修改,很方便。感興趣的朋友可以去試一下。
? (十四)洞悉linux下的Netfilter&iptables:開發一個match模塊【實戰】
自己開發一個match模塊
? ? ? ?今天我們來寫一個很簡單的match來和大家分享如何為iptables開發擴展功能模塊。這個模塊是根據IP報文中有效載荷字段的長度來對其進行匹配,支持固定包大小,也支持一個區間范圍的的數據包,在用戶空間的用法是:
???????? iptables-A FORWARD -m pktsize --size XX[:YY] -j DROP
? ? ? ?這條規則在FORWARD鏈上對于大小為XX(或大小介于XX和YY之間)的數據包進行匹配,數據包的長度不包括IP頭部的長度。為了簡單起見,這個模塊沒有處理“!”情況,因為只是闡述開發過程。
? ? ? ?OK,下面我們開始動手吧。我們這個模塊名字叫pktsize,所以內核中該模塊對應的文件是ipt_pktsize.h和ipt_pktsize.c;用戶空間的文件名為libipt_pktsize.c。
? ? ? ?我們先來定義頭文件,因為這個匹配模塊功能很單一,所以設計它的數據結構主要包含兩部分,如下:
| #ifndef __IPT_PKTSIZE_H #define __IPT_PKTSIZE_H ? #define PKTSIZE_VERSION "0.1" //我們自己定義的用戶保存規則中指定的數據包大小的結構體 struct ipt_pktsize_info { ??? u_int32_t min_pktsize,max_pktsize; //數據包的最小和最大字節數(不包括IP頭) }; ? #endif //__IPT_EXLENGTH_H |
?
一、用戶空間的開發
? 我們知道用戶空間的match是用structiptables_match{}結構表示的,所以我們需要去實例化一個該對象,然后對其關鍵成員進行初始化賦值。一般情況我們需要實現parse函數、help函數、final_check函數、print和save函數就已經可以滿足基本要求了。我們先把整體代碼框架搭起來:
| #include <stdio.h> #include <netdb.h> #include <string.h> #include <stdlib.h> #include <getopt.h> #include <ctype.h> #include <iptables.h> #include <linux/netfilter_ipv4/ipt_pktsize.h> ? static void help(void) { ??? //Todo: ?your code } ? /*用于解析命令行參數的回調函數; 如果成功則返回true */ static int parse(int c, char **argv, int invert, unsigned int *flags, ??????? const void *entry, ??????? struct ipt_entry_match **match) { ??????? return 1; } ? static void final_check(unsigned int flags) { ???? //Todo: ?your code } ? static void print(const void *ip, const struct ipt_entry_match *match, int numeric) { ??? //Todo: ?your code } ? static void save(const void *ip, const struct ipt_entry_match *match) { ??? //Todo: ?your code } ? static struct iptables_match pktsize= { ??? .next?????????? = NULL, ??? .name?????????? = "pktsize", ??? .version??????? = IPTABLES_VERSION, ??? .size?????????? = IPT_ALIGN(sizeof(struct ipt_pktsize_info)), ??? .userspacesize? = IPT_ALIGN(sizeof(struct ipt_pktsize_info)), ??? .help????? ?????= &help, ??? .parse????????? = &parse, ??? .final_check??? = &final_check, ??? .print????????? = &print, ??? .save?????????? = &save }; ? void _init(void) { ??? register_match(&pktsize); } |
? ? ? ??下面我們分別來實現這些回調函數,并對其做簡單解釋:
?
? help()函數:當我們在命令輸入iptables-m pktsize -h時用于顯示該模塊用法的幫助信息,所以很簡單,你想怎么提示用戶都可以:
| static void help(void) { ??? printf( ??? "pktsize v%s options:\n" ??? " --size size[:size]??????? Match packet size against value or range\n" ??? "\nExamples:\n" ??? " iptables -A FORWARD -m pktsize --size 65 -j DROP\n" ??? " iptables -A FORWARD -m pktsize --size 80:120 -j DROP\n" ??? , PKTSIZE_VERSION); } |
????????
? print()函數:該函數是用于打印用戶的輸入參數的,因為其他地方也有可能會輸出規則參數,所以我們將其封裝成一個子函數__print()供其他人來調用,如下:
| static void __print(struct ipt_pktsize_info * info){ ??? if (info->max_pktsize == info->min_pktsize) ??????? printf("%u ", info->min_pktsize); ??? else ? ??????printf("%u:%u ", info->min_pktsize, info->max_pktsize); } ? static void print(const void *ip, const struct ipt_entry_match *match, int numeric) { ??? printf("size "); ??? __print((struct ipt_pktsize_info *)match->data); } |
? ? ? ??從命令行終端輸入的數據包大小的規則參數“XX:YY”其實最終是在ipt_entry_match結構體的data成員里保存著的,關于該結構體參見博文三的圖解。
? ? ? ??save()函數:該函數和print類似:
| static void save(const void *ip, const struct ipt_entry_match *match) { ??????? printf("--size "); ??????? __print((struct ipt_pktsize_info *)match->data); } |
????????
? ? ? ?final_check()函數:如果你的模塊有些長參數格式是必須的,那么當用戶調用了你的模塊但又沒進一步制定必須參數時,一般在這個函數里做校驗限制。如,我的模塊帶了一個必須按參數--size ,而且后面必須跟數值,所以該函數內容如下:
| static void final_check(unsigned int flags) { ??? if (!flags) ?????? exit_error(PARAMETER_PROBLEM, ??????? "\npktsize-parameter problem: for pktsize usage type: iptables -m pktsize --help\n"); } |
?
? ? ? ?parse()函數:該函數是我們的核心,參數的解析最終是在該函數中完成的。因為我們用到長參數格式,所以必須引入一個結構體struct option{},我們在博文十三中已經見過,不清楚原理和用法的童鞋可以回頭復習一下。
? ? ? ?這里我們的模塊只有一個擴展參數,所以該結構非常簡單,如果你有多個,則必須一一處理:
| static struct option opts[] = { ??????? { "size", 1, NULL, '1' }, ??????? {0} }; //并且還要將該結構體對象賦給:pktsize.extra_opts= opts; //解析參數的具體函數單獨出來,會使得parse()函數的結構很優美 /* 我們的輸入參數的可能格式如下: ???????? xx???????? 指定數據包大小 XX ???????? :XX?????? 范圍是0~XX??? ???????? YY:?????? 范圍是YY~65535 ???????? xx:YY??? 范圍是XX~YY */ static void parse_pkts(const char* s,struct ipt_pktsize_info *info){ ?? char* buff,*cp; ?? buff = strdup(s); ? ?? if(NULL == (cp=strchr(buff,':'))){ ????? info->min_pktsize = info->max_pktsize = strtol(buff,NULL,0); ?? }else{ ????? *cp = '\0'; ????? cp++; ? ????? info->min_pktsize = strtol(buff,NULL,0); ??? ??info->max_pktsize = (cp[0]? strtol(cp,NULL,0):0xFFFF); ?? } ? ? ?free(buff); ? ?? if (info->min_pktsize > info->max_pktsize) ?????? exit_error(PARAMETER_PROBLEM, ??????????????? "pktsize min. range value `%u' greater than max. " ? ??????????????"range value `%u'", info->min_pktsize, info->max_pktsize); } ? static int parse(int c, char **argv, int invert, unsigned int *flags, ??????? const void *entry, ??????? struct ipt_entry_match **match) { ?? struct ipt_pktsize_info *info = (struct ipt_pktsize_info *)(*match)->data; ?? switch(c){ ????? case '1': ??????? if (*flags) ?????????? exit_error(PARAMETER_PROBLEM, ???????????????????? ?"size: `--size' may only be " ????????????????????? ?"specified once"); ????? ??parse_pkts(argv[optind-1], info); ??????? *flags = 1; ????? break; ????? default: ??????? return 0; ?? } ?? return 1; } |
? ? ??該文件的最終版本從“?libipt_pktsize.zip ??”下載。
?用戶空間要用的libipt_pktsize.so的源代碼我們就算編寫完成了,迫不及待的去試一下吧。當前,我的iptables確實不認識pktsize模塊。
?我將libipt_pktsize.c拷貝到/usr/src/iptables-1.4.0/ extensions目錄下,并修改該目錄下的Makefile文:
?然后在/usr/src/iptables-1.4.0/目錄下單獨執行一次make命令,最后將extensions/目錄下編譯出來的libipt_pktsize.so拷貝到iptables的庫目錄里,例如/lib/iptables-1.4.0/iptables。
此時,當我們再在命令行執行一次iptables -m pktsize -h時,在末尾處可以看到如下的信息:? ? ? 就證明我們的模塊已經被iptables正確識別并成功加載了。
?
一、內核空間的開發
同樣的,開發內核的Netfilter模塊時,我們還是先搭其框架:
| #include <linux/module.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/version.h> #include <linux/netfilter_ipv4/ip_tables.h> #include <linux/netfilter_ipv4/ipt_pktsize.h> ? MODULE_AUTHOR("Koorey Wung <wjlkoorey@gmail.com>"); MODULE_DESCRIPTION("iptables pkt size range match module."); MODULE_LICENSE("GPL"); ? static int match(const struct sk_buff *skb, ????? const struct net_device *in, ????? const struct net_device *out, ????? const struct xt_match *match, ????? const void *matchinfo, ????? int offset, ????? unsigned int protoff, ????? int *hotdrop) { ??? return 1; } ? ? static struct ipt_match pktsize_match = { ??????? .name?????????? = "test", ??????? .family????????? = AF_INET, ??????? .match????????? = match, ??????? .matchsize??? ???= sizeof(struct ipt_pktsize_info), ??????? .destroy??????? ?= NULL, ??????? .me??????????? ?= THIS_MODULE, }; ? static int |
總結
以上是生活随笔為你收集整理的洞悉linux下的Netfilteriptables的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数组名加取地址符的理解
- 下一篇: signal.h 中的宏定义 SIG_D