內核版本:2.6.34
前面章節介紹過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, 具體如下:
static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = { { .hook = ipv4_conntrack_in, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_CONNTRACK, }, { .hook = ipv4_conntrack_local, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_OUT, .priority = NF_IP_PRI_CONNTRACK, }, { .hook = ipv4_confirm, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_POST_ROUTING, .priority = NF_IP_PRI_CONNTRACK_CONFIRM, }, { .hook = ipv4_confirm, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_IN, .priority = NF_IP_PRI_CONNTRACK_CONFIRM, }, };
從下面的表格中可以看得更清楚:
開頭說過,連接跟蹤的目的是記錄一條連接的信息,對應的數據結構就是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,下面也會具體分析這個函數。
ct = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum, l3proto, l4proto, &set_reply, &ctinfo); ...... ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum); ...... if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status)) nf_conntrack_event_cache(IPCT_REPLY, ct);
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的狀態相區別 ,兩者沒有必然聯系)。
if (!nf_ct_get_tuple(skb, skb_network_offset(skb), dataoff, l3num, protonum, &tuple, l3proto, l4proto)) { pr_debug("resolve_normal_ct: Can't get tuple\n"); return NULL; } h = nf_conntrack_find_get(net, zone, &tuple); if (!h) { h = init_conntrack(net, tmpl, &tuple, l3proto, l4proto, skb, dataoff); …… } ct = nf_ct_tuplehash_to_ctrack(h); if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) { *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY; *set_reply = 1; } else { if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) { pr_debug("nf_conntrack_in: normal packet for %p\n", ct); *ctinfo = IP_CT_ESTABLISHED; } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) { pr_debug("nf_conntrack_in: related packet for %p\n", ct); *ctinfo = IP_CT_RELATED; } else { pr_debug("nf_conntrack_in: new packet for %p\n", ct); *ctinfo = IP_CT_NEW; } *set_reply = 0; } skb->nfct = &ct->ct_general; skb->nfctinfo = *ctinfo;
其中,連接的表示是用數據結構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
if (!nf_ct_invert_tuple(&repl_tuple, tuple, l3proto, l4proto)) { pr_debug("Can't invert tuple.\n"); return NULL; } ct = nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC); if (IS_ERR(ct)) { pr_debug("Can't allocate conntrack.\n"); return (struct nf_conntrack_tuple_hash *)ct; } if (!l4proto->new(ct, skb, dataoff)) { nf_conntrack_free(ct); pr_debug("init conntrack: can't track with proto module\n"); return NULL; } ……. /* Overload tuple linked list to put us in unconfirmed list. */ hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode, &net->ct.unconfirmed);
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進行其它必要的設置。
old_state = ct->proto.tcp.state; dir = CTINFO2DIR(ctinfo); index = get_conntrack_index(th); new_state = tcp_conntracks[dir][index][old_state]; switch (new_state) { case TCP_CONNTRACK_SYN_SENT: if (old_state < TCP_CONNTRACK_TIME_WAIT) break; …… }
static unsigned int get_conntrack_index(const struct tcphdr *tcph) { if (tcph->rst) return TCP_RST_SET; else if (tcph->syn) return (tcph->ack ? TCP_SYNACK_SET : TCP_SYN_SET); else if (tcph->fin) return TCP_FIN_SET; else if (tcph->ack) return TCP_ACK_SET; else return TCP_NONE_SET; }
勾子點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中存儲所有的連接跟 蹤。
zone = nf_ct_zone(ct); hash = hash_conntrack(net, zone, &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple); repl_hash = hash_conntrack(net, zone, &ct->tuplehash[IP_CT_DIR_REPLY].tuple); /* Remove from unconfirmed list */ hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode); …… set_bit(IPS_CONFIRMED_BIT, &ct->status); …… __nf_conntrack_hash_insert(ct, hash, repl_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 = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum, l3proto, l4proto, &set_reply, &ctinfo);
取得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位,因為這次已經收到了連接的反向報文。
ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum); ...... if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status)) nf_conntrack_event_cache(IPCT_REPLY, ct);
勾子點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勾子函數是確認接收或發送一個報文確實已完成,而不是在中途被丟棄,對完 成這樣過程的連接都會進行記錄即確認,而已確認的連接就沒必要再次進行確認了。
static inline int nf_conntrack_confirm(struct sk_buff *skb) { struct nf_conn *ct = (struct nf_conn *)skb->nfct; int ret = NF_ACCEPT; if (ct && ct != &nf_conntrack_untracked) { if (!nf_ct_is_confirmed(ct) && !nf_ct_is_dying(ct)) ret = __nf_conntrack_confirm(skb); if (likely(ret == NF_ACCEPT)) nf_ct_deliver_cached_events(ct); } return ret; }
至此,發送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 = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum, l3proto, l4proto, &set_reply, &ctinfo);
取得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連接記錄的建立過程中尤為明顯。
博客: http://blog.csdn.net/qy532846454 by yoyo