歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[十七]:NetFilter之連接跟蹤

Linux內核分析 - 網絡[十七]:NetFilter之連接跟蹤

日期:2017/3/3 16:37:58   编辑:Linux內核

內核版本: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

Copyright © Linux教程網 All Rights Reserved