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

电脑资料 时间:2019-01-01 我要投稿
【www.unjs.com - 电脑资料】

内核版本:2.6.34

前面章节介绍过Netfilter的框架,地址见: http://blog.csdn.net/qy532846454/article/details/6605592,本章节介绍的连接跟踪就是在Netfilter的框架上实现的,连 接跟踪是实现DNAT,SNAT还有有状态的防火墙的基础,

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

。它的本质就是记录一条连接,具体来说只要满足一来一回两个过程的都可 以算作连接,因此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中定义,

电脑资料

Linux内核分析 网络[十七]:NetFilter之连接跟踪》(https://www.unjs.com)。第一维[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

最新文章