Linux内核分析 - 网络[十七]:NetFilter之连接跟踪

内核版本:2.6.34
转载请注明 博客:
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,具体如下:

[cpp]  view plain copy
  1. static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = {  
  2.  {  
  3.   .hook  = ipv4_conntrack_in,  
  4.   .owner  = THIS_MODULE,  
  5.   .pf  = NFPROTO_IPV4,  
  6.   .hooknum = NF_INET_PRE_ROUTING,  
  7.   .priority = NF_IP_PRI_CONNTRACK,  
  8.  },  
  9.  {  
  10.   .hook  = ipv4_conntrack_local,  
  11.   .owner  = THIS_MODULE,  
  12.   .pf  = NFPROTO_IPV4,  
  13.   .hooknum = NF_INET_LOCAL_OUT,  
  14.   .priority = NF_IP_PRI_CONNTRACK,  
  15.  },  
  16.  {  
  17.   .hook  = ipv4_confirm,  
  18.   .owner  = THIS_MODULE,  
  19.   .pf  = NFPROTO_IPV4,  
  20.   .hooknum = NF_INET_POST_ROUTING,  
  21.   .priority = NF_IP_PRI_CONNTRACK_CONFIRM,  
  22.  },  
  23.  {  
  24.   .hook  = ipv4_confirm,  
  25.   .owner  = THIS_MODULE,  
  26.   .pf  = NFPROTO_IPV4,  
  27.   .hooknum = NF_INET_LOCAL_IN,  
  28.   .priority = NF_IP_PRI_CONNTRACK_CONFIRM,  
  29.  },  
  30. };  

      从下面的表格中可以看得更清楚:

      开头说过,连接跟踪的目的是记录一条连接的信息,对应的数据结构就是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进行其它必要的设置。

[cpp]  view plain copy
  1. old_state = ct->proto.tcp.state;  
  2. dir = CTINFO2DIR(ctinfo);  
  3. index = get_conntrack_index(th);  
  4. new_state = tcp_conntracks[dir][index][old_state];  
  5. switch (new_state) {  
  6. case TCP_CONNTRACK_SYN_SENT:  
  7.  if (old_state < TCP_CONNTRACK_TIME_WAIT)  
  8.   break;  
  9. ……  
  10. }  

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勾子函数是确认接收或发送一个报文确实已完成,而不是在中途被丢弃,对完成这样过程的连接都会进行记录即确认,而已确认的连接就没必要再次进行确认了。

[cpp]  view plain copy
  1. static inline int nf_conntrack_confirm(struct sk_buff *skb)  
  2. {  
  3.  struct nf_conn *ct = (struct nf_conn *)skb->nfct;  
  4.  int ret = NF_ACCEPT;  
  5.  if (ct && ct != &nf_conntrack_untracked) {  
  6.   if (!nf_ct_is_confirmed(ct) && !nf_ct_is_dying(ct))  
  7.    ret = __nf_conntrack_confirm(skb);  
  8.   if (likely(ret == NF_ACCEPT))  
  9.    nf_ct_deliver_cached_events(ct);  
  10.  }  
  11.  return ret;  
  12. }  

      至此,发送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。

[cpp]  view plain copy
  1. ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum);  
  2. ......  

 

      勾子点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连接记录的建立过程中尤为明显。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值