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,具体如下:

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值