前面章節介紹過Netfilter的框架,地址見:http://blog.csdn.net/qy532846454/article/details/6605592,本章節介紹的連接跟蹤就是在Netfilter的框架上實現的,連接跟蹤是實現DNAT,SNAT還有有狀態的防火牆的基礎。它的本質就是記錄一條連接,具體來說只要滿足一來一回兩個過程的都可以算作連接,因此TCP是,UDP是,部分IGMP/ICMP也是,記錄連接的作用需要結合它的相關應用(NAT等)來理解,不是本文的重點,本文主要分析連接跟蹤是如何實現的。
回想Netfilter框架中的hook點(下文稱爲勾子),這些勾子相當於報文進出協議棧的關口,報文會在這裏被攔截,然後執行勾子結點的函數,連接跟蹤利用了其中幾個勾子,分別對應於報文在接收、發送和轉發中,如下圖所示:
連接跟蹤正是在上述勾子上註冊了相應函數(在nf_conntrack_l3proto_ipv4_init中被註冊),勾子爲ipv4_conntrack_ops,具體如下:
從下面的表格中可以看得更清楚:
開頭說過,連接跟蹤的目的是記錄一條連接的信息,對應的數據結構就是tuple,它分爲正向(tuple)和反向(repl_tuple),無論TCP還是UDP都是連接跟蹤的目標,當A向B發送一個報文,A收到B的報文時,我們稱一個連接建立,在連接跟蹤中爲ESTABLISHED狀態。特別要注意的是一條連接的信息對雙方是相同的,無論誰是發起方,兩邊的連接信息都保持一致,以方向爲例,A發送報文給B,對A來說,它先發送報文,因此A->B是正向,B->A是反向;對B來說,它先收到報文,但同樣A->B是正向,B->A是反向。
弄清楚這一點後,每條連接都會有下面的信息相對應
tuple [sip sport tip tport proto]
UDP的過程
UDP的連接跟蹤的建立實際是TCP的簡化版本,沒有了三次握手過程,只要收到+發送完成,連接跟蹤也隨之完成。
TCP的過程
TCP涉及到三次握手才能建立連接,因此相對於UDP要更爲複雜,下面以一個TCP建立連接跟蹤的例子來詳細分析其過程。
場景:主機A與主機B,主機A向主機B發起TCP連接
站在B的角度,分析連接跟蹤在TCP三次握手中的過程。
1. 收到SYN報文 [pre_routing -> local_in]
勾子點PRE_ROUTEING [ipv4_conntrack_in]
ipv4_conntrack_in() -> nf_conntrack_in()
nf_ct_l3protos和nf_ct_protos分別存儲註冊其中的3層和4層協議的連接跟蹤操作,對ipv4而言,它們在__init_nf_conntrack_l3proto_ipv4_init()中被註冊(包括tcp/udp/icmp/ipv4),其中ipv4是在nf_ct_l3protos中的,其餘是在nf_ct_protos中的。下面函數__nf_ct_l3proto_find()根據協議簇(AF_INET)找到ipv4(即nf_conntrack_l3proto_ipv4)並賦給l3proto;下面函數__nf_ct_l4proto_find()根據協議號(TCP)找到tcp(即nf_conntrack_l4proto_tcp4)並賦給l4proto。
l3proto = __nf_ct_l3proto_find(pf);
ret = l3proto->get_l4proto(skb, skb_network_offset(skb), &dataoff, &protonum);
......
l4proto = __nf_ct_l4proto_find(pf, protonum);
然後調用resolve_normal_ct()返回對應的連接跟蹤ct(由於是第一次,它會創建ct),下面會詳細分析這個函數。l4proto->packet()等價於tcp_packet(),作用是得到新的TCP狀態,這裏只要知道ct->proto.tcp.state被設置爲TCP_CONNTRACK_SYN_SENT,下面也會具體分析這個函數。
resolve_normal_ct()
先調用nf_ct_get_tuple()從當前報文skb中得到相應的tuple,然後調用nf_conntrack_find_get()來判斷連接跟蹤是否已存在,已記錄連接的tuple都會存儲在net->ct.hash中。如果已存在,則直接返回;如果不存在,則調用init_conntrack()創建新的,最後設置相關的連接信息。
就本例中收到SYN報文而言,是第一次收到報文,顯然在hash表中是沒有的,進而調用init_conntrack()創建新的連接跟蹤,下面會具體分析該函數;最後根據報文的方向及所處的狀態,設置ctinfo和set_reply,此時方向是IP_CT_DIR_ORIGIN,ct->status未置值,因此最終*ctinfo=IP_CT_NEW; *set_reply=0。ctinfo是很重要的,它表示連接跟蹤所處的狀態,如同TCP建立連接,連接跟蹤建立也要經歷一系列的狀態變更,skb->nfctinfo=*ctinfo記錄了此時的狀態(注意與TCP的狀態相區別,兩者沒有必然聯繫)。
其中,連接的表示是用數據結構nf_conn,而存儲tuple是用nf_conntrack_tuple_hash,兩者的關係是:
init_conntrack()
該函數創建一個連接跟蹤,由觸發的報文得到了tuple,然後調用nf_ct_invert_tuple()將其反轉,得到反向的repl_tuple,nf_conntrack_alloc()爲新的連接跟蹤ct分配空間,並設置了
ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = tuple;
ct->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
l4_proto是根據報文中協議號來查找到的,這裏是TCP連接因此l4_proto對應於nf_conntrack_l4proto_tcp4;l4_proto->new()的作用在於設置TCP的狀態,即ct->proto.tcp.state,這個是TCP協議所特有的(TCP有11種狀態的遷移圖),這裏只要知道剛創建時ct->proto.tcp.state會被設置爲TCP_CONNTRACK_NONE,最後將ct->tuplehash加入到了net->ct.unconfirmed,因爲這個連接還是沒有被確認的,所以加入的是uncorfirmed鏈表。
這樣,init_conntrack()創建後的連接跟蹤情況如下(列出了關鍵的元素):
tuple A_ip A_port B_ip B_port ORIG
repl_tuple B_ip B_port A_ip A_port REPLY
tcp.state NONE
tcp_packet()
函數的作用在於通過連接當前的狀態,到達的新報文,得到連接新的狀態並進行更新,其實就是一次查詢,輸入是方向+報文信息+舊狀態,輸出是新狀態,因此可以用查詢表來簡單實現,tcp_conntracks[2][6][TCP_CONNTRACK_MAX]就是這張查詢表,它在nf_conntrack_proto_tcp.c中定義。第一維[2]代表連接的方向,第二維[6]代表6種當前報文所帶的信息(根椐TCP報頭中的標誌位),第三維[TCP_CONNTRACK_MAX]代表舊狀態,而每個元素存儲的是新狀態。
下面代碼完成了表查詢,old_state是舊狀態,dir是當前報文的方向(它在resolve_normal_ct中賦值,簡單來說是最初的發起方向作爲正向),index是當前報文的信息,get_conntrack_index()函數代碼也貼在下面,函數很簡單,通過TCP報頭的標誌位得到報文信息。在此例中,收到SYN,old_state是NONE,dir是ORIG,index是TCP_SYN_SET,最終的結果new_state通過查看tcp_conntracks就可以得到了,它在nf_conntrack_proto_tcp.c中定義,結果可以自行對照查看,本例中查詢的結果應爲TCP_CONNTRACK_SYN_SENT。
然後switch-case語句根據新狀態new_state進行其它必要的設置。
勾子點LOCAL_IN [ipv4_confirm]
ipv4_confirm() -> nf_conntrack_confirm() -> __nf_conntrack_confirm()
這裏的ct是之前在PRE_ROUTING中創建的連接跟蹤,然後調用hash_conntrack()取得連接跟蹤ct的正向和反向tuple的哈希值hash和repl_hash;報文到達這裏表示被接收,即可以被確認,將它從net->ct.unconfirmed鏈中刪除(PRE_ROUTEING時插入的,那時還是未確認的),然後置ct->status位IPS_CONFIRMED_BIT,表示它已被確認,同時將tuple和repl_tuple加入net->ct.hash,這一步是由__nf_conntrack_hash_insert()完成的,net->ct.hash中存儲所有的連接跟蹤。
至此,接收SYN報文完成,生成了一條新的連接記錄ct,狀態爲TCP_CONNTRACK_SYN_SENT,status設置了IPS_CONFIRMED_BIT位。
2. 發送SYN+ACK報文 [local_out -> post_routing]
勾子點LOCAL_OUT [ipv4_conntrack_local]
ipv4_conntrack_local() -> nf_conntrack_in()
這裏可以看到PRE_ROUTEING和LOCAL_OUT的連接跟蹤的勾子函數最終都進入了nf_conntrack_in()。但不同的是,這次由於在收到SYN報文時已經創建了連接跟蹤,並且已添加到了net.ct->hash中,因此這次resolve_normal_ct()會查找到之前插入的ct而不會調用init_conntrack()創建,並且會設置*ctinfo=IP_CT_ESTABLISHED+IP_CT_IS_REPLY,set_reply=1(參見resolve_normal_ct函數)。
取得ct後,同樣調用tcp_packet()更新連接跟蹤狀態,注意此時ct已處於TCP_CONNTRACK_SYN_SENT,在此例中,發送SYN+ACK,old_state是TCP_CONNTRACK_SYN_SENT,dir是REPLY,index是TCP_SYNACK_SET,最終的結果還是查看tcp_conntracks就可以得到了,爲TCP_CONNTRACK_SYN_RECV。最後會設置ct->status的IPS_SEEN_REPLY位,因爲這次已經收到了連接的反向報文。
勾子點POST_ROUTING [ipv4_confirm]
ipv4_confirm() -> nf_conntrack_confirm()
這裏可以看到POST_ROUTEING和LOCAL_IN的勾子函數是相同的。但在進入到nf_conntrack_confirm()後會調用nf_ct_is_confirmed(),它檢查ct->status的IPS_CONFIRMED_BIT,如果沒有被確認,纔會進入__nf_conntrack_confirm()進行確認,而在收到SYN過程的LOCAL_IN節點設置了IPS_CONFIRMED_BIT,所以此處的ipv4_confirm()不做任何動作。實際上,LOCAL_IN和POST_ROUTING勾子函數是確認接收或發送一個報文確實已完成,而不是在中途被丟棄,對完成這樣過程的連接都會進行記錄即確認,而已確認的連接就沒必要再次進行確認了。
至此,發送SYN+ACK報文完成,沒有生成新的連接記錄ct,狀態變更爲TCP_CONNTRACK_SYN_RECV,status設置了IPS_CONFIRMED_BIT+IPS_SEEN_REPLY位。
3. 收到ACK報文 [pre_routing -> local_in]
勾子點PRE_ROUTEING [ipv4_conntrack_in]
ipv4_conntrack_in() -> nf_conntrack_in()
由於之前已經詳細分析了收到SYN報文的連接跟蹤處理的過程,這裏收到ACK報文的過程與收到SYN報文是相同的,只要注意幾個不同點就行了:連接跟蹤已存在,連接跟蹤狀態不同,標識位status不同。
resolve_normal_ct()會返回之前插入的ct,並且會設置*ctinfo=IP_CT_ESTABLISHED,set_reply=0(參見resolve_normal_ct函數)。
取得ct後,同樣調用tcp_packet()更新連接跟蹤狀態,注意此時ct已處於TCP_CONNTRACK_SYN_RECV,在此例中,接收ACK,old_state是TCP_CONNTRACK_SYN_RECV,dir是ORIG,index是TCP_ACK_SET,最終的結果查看tcp_conntracks得到爲TCP_CONNTRACK_ESTABLISHED。
ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum);
......
勾子點LOCAL_IN [ipv4_confirm]
ipv4_confirm() -> nf_conntrack_confirm()
同發送SYN+ACK報文時POST_ROUTING相同,由於連接是已被確認的,所以在nf_conntrack_confirm()函數中會退出,不會再次確認。
至此,接收ACK報文完成,沒有生成新的連接記錄ct,狀態變更爲TCP_CONNTRACK_ESTABLISHED,status設置了IPS_CONFIRMED_BIT+IPS_SEEN_REPLY位。
簡單總結下,以B的角度,在TCP三次握手建立連接的過程中,連接跟蹤的過程如下:
本文開頭提到連接跟蹤對於連接雙方是完全相同的,即以A的角度,在TCP三次握手建立連接的過程中,連接跟蹤的過程也是一樣的,在此不再一一分析,最終的流程如下:
連接記錄的建立只要一來一回兩個報文就足夠了,如B在收到SYN報文併發送SYN+ACK報文後,連接記錄的status=IPS_CONFIRMED+IPS_SEEN_REPLY,表示連接已建立,最後收到的ACK報文並沒有對status再進行更新,它更新的是tcp自身的狀態,所以,連接記錄建立需要的只是兩個方向上的報文,在UDP連接記錄的建立過程中尤爲明顯。