NETDEV 協議 十

轉載請註明 博客:http://blog.csdn.net/qy532846454 by yoyo

      前面章節介紹過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連接記錄的建立過程中尤爲明顯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章