基于linux2.6.21
上一节分析了连接跟踪模块相关的初始化代码,本节分析连接中hook函数。在分析之前,我们再次回顾一下数据包在连接跟踪模块中的走向。
发往本地的数据:
对于连接跟踪来说,只在hook点NF_IP_PRE_ROUTING、NF_IP_LOCAL_IN两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm,其中ipv4_conntrack_defrag主要是对分段数据包进行重组操作;接着调用ipv4_conntrack_in为数据包的nfct指针进行赋值以及更新连接跟踪项的状态等;然后调用ipv4_conntrack_help执行数据包对应的连接跟踪项的helper指针,实现创建期望连接或者ALG等功能;最后调用ipv4_confirm将一个未确认的连接跟踪项进行确认操作,并加入确认链表中。
本地转发的数据:
对于连接跟踪来说,只在hook点NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm,我们发现调用的函数与发往本机的数据包调用的函数及顺序是一样的。
本机发出的数据:
对于连接跟踪来说,只在hook点NF_IP_LOCAL、NF_IP_POST_ROUTING两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_local->ipv4_conntrack_help->ipv4_confirm,我们发现调用的函数与发往本机的数据包调用的函数及顺序相比,只有ipv4_conntrack_local与ipv4_conntrack_in不一样,相比于ipv4_conntrack_in,ipv4_conntrack_local增加了对数据包大小的判断。
下面我们就对这些函数进行分析。
功能:对分段数据包实现数据重组操作。
主要是调用函数 nf_ct_ipv4_gather_frags实现数据包重组。
static unsigned int ipv4_conntrack_defrag(unsigned int hooknum, struct sk_buff **pskb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { #if !defined(CONFIG_IP_NF_NAT) && !defined(CONFIG_IP_NF_NAT_MODULE) /* Previously seen (loopback)? Ignore. Do this before fragment check. */ if ((*pskb)->nfct) return NF_ACCEPT; #endif /* Gather fragments. */ if ((*pskb)->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) { /*对数据包进行分段重组*/ *pskb = nf_ct_ipv4_gather_frags(*pskb, hooknum == NF_IP_PRE_ROUTING ? IP_DEFRAG_CONNTRACK_IN : IP_DEFRAG_CONNTRACK_OUT); if (!*pskb) return NF_STOLEN; } return NF_ACCEPT; }
1.1 nf_ct_ipv4_gather_frags
而nf_ct_ipv4_gather_frags主要是通过函数ip_defrag实现分段重组的。
static struct sk_buff * nf_ct_ipv4_gather_frags(struct sk_buff *skb, u_int32_t user) { skb_orphan(skb); local_bh_disable(); skb = ip_defrag(skb, user); local_bh_enable(); if (skb) ip_send_check(skb->nh.iph); return skb; }
这个数据主要实现以下功能:
a)当是一个新的数据包时,则为该数据包创建对应的连接跟踪项,创建成果后进入c
b)当已经为传递过来的数据包创建了连接跟踪项,则进入c
c)更新连接跟踪项的状态
d)为数据包的nfct指针赋值
该函数只是简单的调用nf_conntrack_in,看来实现上述功能的函数即是nf_conntrack_in。
static unsigned int ipv4_conntrack_in(unsigned int hooknum, struct sk_buff **pskb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { return nf_conntrack_in(PF_INET, hooknum, pskb); }
该函数主要被PREROUTING、与LOCAL_OUT两个hook点时,才会被调用
功能: 实现对数据的连接跟踪,更新连接跟踪项的连接状态
目前就是根据五元组识别一条数据流
这个函数可以说是连接跟踪模块的重中之重,
首先它将数据包的类型按如下
几个类型处理:
1.当这是一个新的数据流的第一个数据包时,则会根据该数据包的五元组信息
创建一个新的连接跟踪项,并初始化该连接跟踪项的tuple_hash、helper的值,最后
将该连接跟踪项的原始方向的tuple_hash添加到unconfirmed链表中,且该连接不是期望 连接
2.当这是一个新的数据流的第一个数据包时,则会根据该数据包的五元组信息
创建一个新的连接跟踪项,并初始化该连接跟踪项的tuple_hash、helper的值,最后
将该连接跟踪项的原始方向的tuple_hash添加到unconfirmed链表中,且该连接是期望连 接
3. 当该数据包是原始方向的非第一个数据包,但当该数据包进入连接跟踪模块时,连接跟踪模块还没有收到reply方向的数据包
4.当该数据包是原始方向的非第一个数据包, 且到改数据包进入连接跟踪模块时,连接跟踪 模块已经接收到reply方向的数据包
5.当该数据包是reply方向的数据包。
主要也就对数据包进行上面五种分类
当是第一类数据包时,则创建一个新的连接跟踪项,并作相应的初始化工作,且
将数据包的nfctinfo设置为IP_CT_NEW;
当是第二类数据包时,则创建一个新的连接跟踪项,并作相应的初始化工作,且
将数据包的nfctinfo设置为IP_CT_RELATED;
当时第三类数据包时,将数据包的nfctinfo设置为IP_CT_NEW;
当是第四种数据包时,将数据包的nfctinfo设置为IP_CT_ESTABLISHED;
当时第五种状态时,将数据包的的nfctinfo设置为IP_CT_ESTABLISHED+IP_CT_IS_REPLY;
然后再调用四层协议相关的packet函数,根据数据包的四层内容值,更新四层协议相关的
状态值(比如tcp协议,就会在其nf_conntrack_protocol->packet设置四层相关的状态改变)。
当数据包是reply方向,且置位连接跟踪项的status的IPS_SEEN_REPLY_BIT后,发现IPS_SEEN_REPLY_BIT位原来的值为0,则会调用nf_conntrack_event_cache设置事件通知为IPCT_STATUS,在ipv4_confirm函数里,会调用函数nf_ct_deliver_cached_events
通过通知链的回调函数,将需要更新的status通过netlink机制发送给nfnetlink模块,由其再执行更新状态操作。
最后,为skb的nfct指针赋值,即将skb的nfct指向该数据包对应的连接跟踪项,当数据包再进入NAT模式时,即可以根据这个指针获取到数据包对应的连接跟踪项,从而实现NAT相关的操作。
unsigned int nf_conntrack_in(int pf, unsigned int hooknum, struct sk_buff **pskb) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; struct nf_conntrack_l3proto *l3proto; struct nf_conntrack_protocol *proto; unsigned int dataoff; u_int8_t protonum; int set_reply = 0; int ret; /* Previously seen (loopback or untracked)? Ignore. */ /*如果数据包的nfct项不为空,则说明该数据包已经关联一个nf_conn项了 有两种情况: 1.数据在PRE_ROUTING的raw模块相关的hook函数时,将该数据包与un_conntrack关联了, 因此就不再进行连接模块的跟踪了 2.如果发送数据包的设备为loopback设备,说明数据包已经被跟踪一次了,此处也 不再进行跟踪了。*/ if ((*pskb)->nfct) { NF_CT_STAT_INC(ignore); return NF_ACCEPT; } /*根据三层协议类型,在nf_ct_l3protos数组中,获取三层协议的nf_conntrack_l3proto结构的实例*/ l3proto = __nf_ct_l3proto_find((u_int16_t)pf); /* 调用这个函数,主要有两个目的 a)获取skb buff中三层协议的数据部分相对于skb->data的偏移量 */ if ((ret = l3proto->prepare(pskb, hooknum, &dataoff, &protonum)) <= 0) { DEBUGP("not prepared to track yet or error occured\n"); return -ret; } /*根据三层协议类型与四层协议号,在全局数组nf_ct_protos中,获取四层协议的相应的nf_conntrack_protocol结构*/ proto = __nf_ct_proto_find((u_int16_t)pf, protonum); /* It may be an special packet, error, unclean... * inverse of the return code tells to the netfilter * core what to do with the packet. */ /*根据四层协议数据结构nf_conntrack_protocol提供的error函数,对数据包进行四层格式检查等*/ if (proto->error != NULL && (ret = proto->error(*pskb, dataoff, &ctinfo, pf, hooknum)) <= 0) { NF_CT_STAT_INC(error); NF_CT_STAT_INC(invalid); return -ret; }
/* 功能: 1、若存在一个符合条件的nf_conn,则返回该变量的地址 2、若不存在,则根据已知条件创建一个nf_conn,并放入unconfirm表中 */ ct = resolve_normal_ct(*pskb, dataoff, pf, protonum, l3proto, proto, &set_reply, &ctinfo); if (!ct) { /* Not valid part of a connection */ NF_CT_STAT_INC(invalid); return NF_ACCEPT; } if (IS_ERR(ct)) { /* Too stressed to deal. */ NF_CT_STAT_INC(drop); return NF_DROP; } NF_CT_ASSERT((*pskb)->nfct); /* 创建连接跟踪项后,再调用四层协议的packet函数进行处理。 */ ret = proto->packet(ct, *pskb, dataoff, ctinfo, pf, hooknum); if (ret < 0) { /* Invalid: inverse of the return code tells * the netfilter core what to do */ DEBUGP("nf_conntrack_in: Can't track with proto module\n"); nf_conntrack_put((*pskb)->nfct); (*pskb)->nfct = NULL; NF_CT_STAT_INC(invalid); return -ret; } /*对于一个新创建的连接跟踪项后,当我们第一次收取到reply方向的数据包后, 则会设置nf_conn->status的IPS_SEEN_REPLY_BIT位为1,当设置成功且IPS_SEEN_REPLY_BIT位的原来值为0时,则调用nf_conntrack_event_cache ,由nfnetlink模块处理状态改变的事件。 */ if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status)) nf_conntrack_event_cache(IPCT_STATUS, *pskb); return ret; }
该函数的流程图如下,其中__nf_ct_l3proto_find就是发现该数据包对应的三层协议数据结构,对于ipv4就是我们上一节所说的nf_conntrack_l3proto_ipv4。
而l3proto->prepare就是nf_conntrack_l3proto_ipv4的 ipv4_prepare函数;
__nf_ct_proto_find就是找到该数据包对应的四层协议对应的数据结构,对于tcp就是变量nf_conntrack_protocol_tcp4;
resolve_normal_ct函数实现连接跟踪项的创建以及连接跟踪项状态改变等操作;
proto->packet函数用于更新该连接跟踪项对应的四层协议的状态,对于tcp则是函数 tcp_packet。
由此,我们知道resolve_normal_ct是连接跟踪模块中协议无关处理函数,且对连接跟踪模块来说非常的重要,我们分析一下。
该函数的功能是
a)若连接跟踪项不存在时,则根据已知条件创建一个nf_conn,并放入unconfirm表中,执行c)
b)若连接跟踪项已存在,则执行c)
c)更新连接跟踪项的状态
d)设置传入数据包的nfct指针
static inline struct nf_conn * resolve_normal_ct(struct sk_buff *skb, unsigned int dataoff, u_int16_t l3num, u_int8_t protonum, struct nf_conntrack_l3proto *l3proto, struct nf_conntrack_protocol *proto, int *set_reply, enum ip_conntrack_info *ctinfo) { struct nf_conntrack_tuple tuple; struct nf_conntrack_tuple_hash *h; struct nf_conn *ct; /*根据三、四层协议的nf_conntrack结构变量及skb,为该数据包计算相应的 nf_conntrack_tuple值*/ if (!nf_ct_get_tuple(skb, (unsigned int)(skb->nh.raw - skb->data), dataoff, l3num, protonum, &tuple, l3proto, proto)) { DEBUGP("resolve_normal_ct: Can't get tuple\n"); return NULL; } /* look for tuple match */ /*根据上面获取的nf_conntrack_tuple变量,在hash数组nf_conntrack_hash中相应的hash链表中 查找是否有满足条件的nf_conntrack_tuple_hash*/ h = nf_conntrack_find_get(&tuple, NULL); if (!h) { /*没有找到符合条件的nf_conntrack_tuple_hash,则调用init_conntrack创建一个新的 nf_conntrack_tuple_hash,并对其helper指针进行赋值*/ h = init_conntrack(&tuple, l3proto, proto, skb, dataoff); if (!h) return NULL; if (IS_ERR(h)) return (void *)h; } /*根据tuple值,获取nf_conn*/ ct = nf_ct_tuplehash_to_ctrack(h); /* 根据tuple的dst.dir,确定当前进来的数据包对应于连接跟踪项的哪一个方向, 然后设置set_reply的值, */ /* It exists; we have (non-exclusive) reference. */ if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) { /*当是reply方向的数据包时,则skb->nfctinfo可以设置为IP_CT_ESTABLISHED + IP_CT_IS_REPLY, 同时需要将连接跟踪项的status的IPS_SEEN_REPLY_BIT位置为1。 */ *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY; /* Please set reply bit if this packet OK */ *set_reply = 1; } else { /* Once we've had two way comms, always ESTABLISHED. */ /*当是连接跟踪项原始方向的数据包时,则有以下几种情况 1.当发送的数据包到达连接跟踪模块时,其reply方向没有收到对应的数据包之前 a)连接跟踪项不是期望连接,此时将skb->nfctinfo设置为IP_CT_NEW b)连接跟踪项是期望连接,此时将skb->nfctinfo设置为IP_CT_RELATED 2.当原始方向发送的数据包到达连接跟踪模块时,其reply方向已经收到过对应的数据包 即连接跟踪项的状态的IPS_SEEN_REPLY_BIT位已经置位了。 此时将skb->nfctinfo设置为IP_CT_ESTABLISHED */ if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) { DEBUGP("nf_conntrack_in: normal packet for %p\n", ct); *ctinfo = IP_CT_ESTABLISHED; } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) { DEBUGP("nf_conntrack_in: related packet for %p\n", ct); *ctinfo = IP_CT_RELATED; } else { DEBUGP("nf_conntrack_in: new packet for %p\n", ct); *ctinfo = IP_CT_NEW; } *set_reply = 0; } skb->nfct = &ct->ct_general; skb->nfctinfo = *ctinfo; return ct; }
其中nf_ct_get_tuple主要是根据数据包的三层、四层协议获取相应的五元组值;
nf_conntrack_find_get主要是根据传入的tuple变量,在hash数组nf_conntrack_hash中相应的hash链表中查找是否有满足条件的nf_conntrack_tuple_hash,对于一个已确认的连接跟踪项,是能根据其tuple值在hash数组nf_conntrack_hash中相应的hash链表中查找到满足条件的nf_conntrack_tuple_hash变量的,若没有查找到,则说明还没有创建该tuple变量对应的连接跟踪项。
当连接跟踪项没有创建时,则会调用函数init_conntrack创建连接跟踪项;
接着调用nf_ct_tuplehash_to_ctrack获取相应的连接跟踪项;
然后根据数据包对应于连接跟踪项的方向,设置连接跟踪项的状态;
最后设置skb->nfctinfo、skb->nfct。
下面一一分析上面使用到的函数。
int nf_ct_get_tuple(const struct sk_buff *skb, unsigned int nhoff, unsigned int dataoff, u_int16_t l3num, u_int8_t protonum, struct nf_conntrack_tuple *tuple, const struct nf_conntrack_l3proto *l3proto, const struct nf_conntrack_protocol *protocol) { NF_CT_TUPLE_U_BLANK(tuple); tuple->src.l3num = l3num; /* 调用函数ipv4_pkt_to_tuple,设置tuple结构的源、目的ip地址,对于ipv4,则是调用函数ipv4_pkt_to_tuple */ if (l3proto->pkt_to_tuple(skb, nhoff, tuple) == 0) return 0; /* 调用四层函数l4_pkt_to_tuple,设置tuple结构中的四层相关的源、目的值。 */ tuple->dst.protonum = protonum; tuple->dst.dir = IP_CT_DIR_ORIGINAL; /* 调用四层函数l4_pkt_to_tuple,设置tuple结构中的四层相关的源、目的值。对于tcp协议来说,则是调用函数tcp_pkt_to_tuple */ return protocol->pkt_to_tuple(skb, dataoff, tuple); }
该函数的作用是主要是根据传递的nf_conntrack_tuple类型的变量,在nf_conntrack_hash[]中相关的链表中,查找是否有满足要求的nf_conntrack_tuple_hash类型的变量。
nf_conntrack_tuple_hash查找函数
该函数主要是__nf_conntrack_find的封装函数,相比于__nf_conntrack_find,增加了锁机制。
struct nf_conntrack_tuple_hash * nf_conntrack_find_get(const struct nf_conntrack_tuple *tuple, const struct nf_conn *ignored_conntrack) { struct nf_conntrack_tuple_hash *h; read_lock_bh(&nf_conntrack_lock); h = __nf_conntrack_find(tuple, ignored_conntrack); if (h) atomic_inc(&nf_ct_tuplehash_to_ctrack(h)->ct_general.use); read_unlock_bh(&nf_conntrack_lock); return h; }
那我们就分析下__nf_conntrack_find。
nf_conntrack_tuple_hash查找函数
1、根据nf_conntrack_tuple计算hash值
2、根据hash值,从数组nf_conntrack_hash中取出相应的hash链表
3、遍历hash链表,查找是否存在nf_conntrack_tuple变量相关的nf_conntrack_tuple_hash
若存在则返回相应的值;若不存在,则返回NULL
struct nf_conntrack_tuple_hash * __nf_conntrack_find(const struct nf_conntrack_tuple *tuple, const struct nf_conn *ignored_conntrack) { struct nf_conntrack_tuple_hash *h; unsigned int hash = hash_conntrack(tuple); ASSERT_READ_LOCK(&nf_conntrack_lock); list_for_each_entry(h, &nf_conntrack_hash[hash], list) { if (conntrack_tuple_cmp(h, tuple, ignored_conntrack)) { NF_CT_STAT_INC(found); return h; } NF_CT_STAT_INC(searched); } return NULL; }
如果找到相应的tuple_hash,也就是找到相应的数据连接跟踪项。因为我们可以根据
nf_ct_tuplehash_to_ctrack函数,找到tuple_hash所属的数据连接跟踪项,其实就是通过contain_of实现的。
2.1.3 init_conntrack
这一次分析init_conntrack函数,这个函数包含的信息量也是蛮大的。这个函数主要实现以下功能:
A)调用函数__nf_conntrack_alloc创建一个数据连接跟踪项
B)根据刚创建的连接跟踪项的原始方向的nf_conntrack_tuple变量,查找期望连接链表,看能否找到符合条件的期望连接跟踪。
若查找到,则更新新创建连接跟踪项的master指针,以及调用exp->expectfn;
若没有查找到:
则遍历helpers链表,找到符合条件的nf_conntrack_helper变量后,则更新连接跟踪项的helper指针。
C)将新创建的连接跟踪项插入到unconfirmed链表中。
对于连接跟踪项,是使用定时器超时机制来实现异步垃圾回收的。所以也要对连接跟踪项的垃圾回收机制进行分析下。
那我们先分析下这个函数,具体的代码分析如下:
功能:根据nf_conntrack_tuple结构变量、nf_conntrack_l3proto、nf_conntrack_protocol结构变量创建一个新的nf_conntrack_tuple_hash变量
static struct nf_conntrack_tuple_hash * init_conntrack(const struct nf_conntrack_tuple *tuple, struct nf_conntrack_l3proto *l3proto, struct nf_conntrack_protocol *protocol, struct sk_buff *skb, unsigned int dataoff) { struct nf_conn *conntrack; struct nf_conntrack_tuple repl_tuple; struct nf_conntrack_expect *exp; /*根据入口nf_conntrack_tuple变量和三层、四层nf_conntrack_proto变量的invert函数构造出口nf_conntrack_tuple变量,对于ipv4来说,就是ipv4_invert_tuple,对于tcp协议来说即是 tcp_invert_tuple*/ if (!nf_ct_invert_tuple(&repl_tuple, tuple, l3proto, protocol)) { DEBUGP("Can't invert tuple.\n"); return NULL; } /*为新的nf_conn申请内存,并根据入口、出口tuple以及三层nf_conntrack_prot进行初始*/ conntrack = __nf_conntrack_alloc(tuple, &repl_tuple, l3proto); if (conntrack == NULL || IS_ERR(conntrack)) { DEBUGP("Can't allocate conntrack.\n"); return (struct nf_conntrack_tuple_hash *)conntrack; } /*调用四层nf_conntrack_prot的new函数为新的nf_conn进行四层初始化*/ if (!protocol->new(conntrack, skb, dataoff)) { nf_conntrack_free(conntrack); DEBUGP("init conntrack: can't track with proto module\n"); return NULL; } write_lock_bh(&nf_conntrack_lock); /*在nf_conntrack_expect_list链表中,查找新建的nf_conn是否为一个已建立的nf_conn的期望连接*/ exp = find_expectation(tuple); if (exp) { DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n", conntrack, exp); /* Welcome, Mr. Bond. We've been expecting you... */ /*若是期望链接,则更新链接的状态,并对master指针进行赋值。并 增加对主数据连接跟踪项的引用计数*/ __set_bit(IPS_EXPECTED_BIT, &conntrack->status); conntrack->master = exp->master; #ifdef CONFIG_NF_CONNTRACK_MARK conntrack->mark = exp->master->mark; #endif nf_conntrack_get(&conntrack->master->ct_general); NF_CT_STAT_INC(expect_new); } else { /*若不是期望链接,则调用__nf_ct_helper_find,从链表helpers中查找符合条件的helper函数*/ conntrack->helper = __nf_ct_helper_find(&repl_tuple); NF_CT_STAT_INC(new); } /*conntrack->tuplehash添加到unconfirmed链表中*/ /* Overload tuple linked list to put us in unconfirmed list. */ list_add(&conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list, &unconfirmed); write_unlock_bh(&nf_conntrack_lock); if (exp) { if (exp->expectfn) exp->expectfn(conntrack, exp); nf_conntrack_expect_put(exp); } return &conntrack->tuplehash[IP_CT_DIR_ORIGINAL]; }
2.1.2.1__nf_conntrack_alloc
接下来分析这个函数,其功能如下:
1.调用kmem_cache_alloc,创建一个新的连接跟踪项
2.设置连接跟踪项的origin、reply方向的tuple值
3.初始化连接跟踪项的定时器,设置超时处理函数为death_by_timeout
4.设置连接跟踪项的销毁函数为destroy_conntrack
5.全局连接跟踪项统计值nf_conntrack_count加1
static struct nf_conn * __nf_conntrack_alloc(const struct nf_conntrack_tuple *orig, const struct nf_conntrack_tuple *repl, const struct nf_conntrack_l3proto *l3proto) { struct nf_conn *conntrack = NULL; u_int32_t features = 0; if (!nf_conntrack_hash_rnd_initted) { get_random_bytes(&nf_conntrack_hash_rnd, 4); nf_conntrack_hash_rnd_initted = 1; } /*若当前以创建的连接跟踪项已经超过了最大值了, 则根据要创建的连接的origin方向的tuple变量计算hash值为h,然后 调用early_drop,遍历链表nf_conntrack_hash[h],对于连接跟踪项的status的IPS_ASSURED_BIT位没有被置位的连接跟踪项,则强制删除。 最后若early_drop返回0,则没有找到可删除的项,程序返回创建连接跟踪项失败; 若early_drop 返回1,则说明已经删除了一个连接跟踪项,则可以继续创建新的 连接跟踪项。 */ if (nf_conntrack_max && atomic_read(&nf_conntrack_count) >= nf_conntrack_max) { unsigned int hash = hash_conntrack(orig); /* Try dropping from this hash chain. */ if (!early_drop(&nf_conntrack_hash[hash])) { if (net_ratelimit()) printk(KERN_WARNING "nf_conntrack: table full, dropping" " packet.\n"); return ERR_PTR(-ENOMEM); } } /* find features needed by this conntrack. */ features = l3proto->get_features(orig); read_lock_bh(&nf_conntrack_lock); if (__nf_ct_helper_find(repl) != NULL) features |= NF_CT_F_HELP; read_unlock_bh(&nf_conntrack_lock); DEBUGP("nf_conntrack_alloc: features=0x%x\n", features); read_lock_bh(&nf_ct_cache_lock); if (!nf_ct_cache[features].use) { DEBUGP("nf_conntrack_alloc: not supported features = 0x%x\n", features); goto out; } conntrack = kmem_cache_alloc(nf_ct_cache[features].cachep, GFP_ATOMIC); if (conntrack == NULL) { DEBUGP("nf_conntrack_alloc: Can't alloc conntrack from cache\n"); goto out; } memset(conntrack, 0, nf_ct_cache[features].size); conntrack->features = features; if (nf_ct_cache[features].init_conntrack && nf_ct_cache[features].init_conntrack(conntrack, features) < 0) { DEBUGP("nf_conntrack_alloc: failed to init\n"); kmem_cache_free(nf_ct_cache[features].cachep, conntrack); conntrack = NULL; goto out; } atomic_set(&conntrack->ct_general.use, 1); conntrack->ct_general.destroy = destroy_conntrack; conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig; conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = *repl; /* Don't set timer yet: wait for confirmation */ init_timer(&conntrack->timeout); conntrack->timeout.data = (unsigned long)conntrack; conntrack->timeout.function = death_by_timeout; atomic_inc(&nf_conntrack_count); out: read_unlock_bh(&nf_ct_cache_lock); return conntrack; }
2.1.2.2 __nf_ct_helper_find
功能:根据传入的nf_conntrack_tuple类型的参数,查找符合条件的helper
static struct nf_conntrack_helper *
__nf_ct_helper_find(const struct nf_conntrack_tuple *tuple)
{
return LIST_FIND(&helpers, helper_cmp,
struct nf_conntrack_helper *,
tuple);
}
2.1.2.3 连接跟踪项的异步垃圾处理机制
在ipv4_confirm中,对新的连接跟踪项进行确认时,会启动连接跟踪项的超时定时器,以后再收到数据包时,会在nf_conntrack_in中调用四层协议的packet函数里调用函数nf_ct_refresh_acct更新定时器的超时处理时间,这样的话,只要一个连接跟踪项对应的数据流一直有数据发收发,则连接跟踪项就不会超时,若在一定时间内没有再收到数据包,则会超时,从而调用超时处理函数,用于释放该数据连接跟踪项占用的内存。
所以我们就跟着超时处理函数,看下是如何释放连接跟踪项的内存的。
2.1.2.3.1death_by_timeout
连接跟踪项的超时处理函数
1. 调用clean_from_lists将该连接跟踪项的两个方向的tuple变量从ip_conntrack_hash[x]链表中删除
2. 调用nf_ct_put,自减nf_conn的引用计数,若自减后的引用计数为0,则调用
nf_conn->ct_general->destroy销毁nf_conn变量
static void death_by_timeout(unsigned long ul_conntrack) { struct nf_conn *ct = (void *)ul_conntrack; write_lock_bh(&nf_conntrack_lock); /* Inside lock so preempt is disabled on module removal path. * Otherwise we can get spurious warnings. */ NF_CT_STAT_INC(delete_list); clean_from_lists(ct); write_unlock_bh(&nf_conntrack_lock); nf_ct_put(ct); }
可以看到是在nf_ct_put中触发的对连接跟踪项所占内存的释放
2.1.2.3.2nf_ct_put&nf_conntrack_put
/*
nf_conn的引用计数减一
*/
static inline void nf_ct_put(struct nf_conn *ct) { NF_CT_ASSERT(ct); nf_conntrack_put(&ct->ct_general); }
/*
1.nfct的引用计数减一
2.若减一后的引用计数值为0,则调用nfct->destroy销毁nf_conn
*/
static inline void nf_conntrack_put(struct nf_conntrack *nfct) { if (nfct && atomic_dec_and_test(&nfct->use)) nfct->destroy(nfct); }
我们在函数__nf_conntrack_alloc知道nfct->destroy对应的函数为destroy_conntrack。
继续分析
2.1.2.3.3 destroy_conntrack
/*
功能:销毁一个连接跟踪项
1.发送destory消息给nfnetlink模块,让nfnetlink模块执行该连接跟踪相关联的内容
2.调用连接跟踪项中三、四层协议相关的销毁函数
3.因为未确认的连接跟踪项是放在unconntrack链表中的,因此对于未确认的连接跟踪
项,还需要将该连接跟踪项origin方向的tuple从unconntrack链表上删除
4.若该连接为期望连接,调用nf_ct_put,减去对主连接的引用
5.调用nf_conntrack_free释放连接跟踪项占用的内存。
*/
static void destroy_conntrack(struct nf_conntrack *nfct) { struct nf_conn *ct = (struct nf_conn *)nfct; struct nf_conntrack_l3proto *l3proto; struct nf_conntrack_protocol *proto; DEBUGP("destroy_conntrack(%p)\n", ct); NF_CT_ASSERT(atomic_read(&nfct->use) == 0); NF_CT_ASSERT(!timer_pending(&ct->timeout)); nf_conntrack_event(IPCT_DESTROY, ct); set_bit(IPS_DYING_BIT, &ct->status); /* To make sure we don't get any weird locking issues here: * destroy_conntrack() MUST NOT be called with a write lock * to nf_conntrack_lock!!! -HW */ l3proto = __nf_ct_l3proto_find(ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.l3num); if (l3proto && l3proto->destroy) l3proto->destroy(ct); proto = __nf_ct_proto_find(ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.l3num, ct->tuplehash[IP_CT_DIR_REPLY].tuple.dst.protonum); if (proto && proto->destroy) proto->destroy(ct); if (nf_conntrack_destroyed) nf_conntrack_destroyed(ct); write_lock_bh(&nf_conntrack_lock); /* Expectations will have been removed in clean_from_lists, * except TFTP can create an expectation on the first packet, * before connection is in the list, so we need to clean here, * too. */ nf_ct_remove_expectations(ct); /* We overload first tuple to link into unconfirmed list. */ /*若该连接还未被确认,则该连接跟踪项只有origin tuple添加到了unconntrack的 链表上了,所以只删除origin方向的tuple连接*/ if (!nf_ct_is_confirmed(ct)) { BUG_ON(list_empty(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list)); list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list); } NF_CT_STAT_INC(delete); write_unlock_bh(&nf_conntrack_lock); if (ct->master) nf_ct_put(ct->master); /*调用nf_conntrack_free释放nf_conn变量占用的内存*/ DEBUGP("destroy_conntrack: returning ct=%p to slab\n", ct); nf_conntrack_free(ct); }
至此,即为异步清理的整个过程。
我们在__nf_conntrack_alloc里有看到一个通过清理机制,那也分析下。
2.1.2.4 连接跟踪项的同步垃圾处理机制
同步垃圾处理是通过函数early_drop实现的。
/*
1.调用LIST_FIND_B遍历链表chain中的每一个tuple_hash,查找符合
删除条件的tuple_hahs
2.若找到符合条件的tuple_hash,则获取该tuple_hash所属的连接跟踪项
3.调用death_by_timeout,强制删除该连接跟踪项
*/
static int early_drop(struct list_head *chain) { /* Traverse backwards: gives us oldest, which is roughly LRU */ struct nf_conntrack_tuple_hash *h; struct nf_conn *ct = NULL; int dropped = 0; read_lock_bh(&nf_conntrack_lock); /*遍历链表chain,对每一个tuple_hash都调用函数unreplied,若unreplied返回1, 则表明可以删除该tuple_hash对应的连接跟踪项*/ h = LIST_FIND_B(chain, unreplied, struct nf_conntrack_tuple_hash *); if (h) { ct = nf_ct_tuplehash_to_ctrack(h); atomic_inc(&ct->ct_general.use); } read_unlock_bh(&nf_conntrack_lock); if (!ct) return dropped; if (del_timer(&ct->timeout)) { death_by_timeout((unsigned long)ct); dropped = 1; NF_CT_STAT_INC(early_drop); } nf_ct_put(ct); return dropped; }
接着就是由nf_ct_put接管同步垃圾回收了,剩下的流程就会异步垃圾处理机制是一样的了。
至此,函数ipv4_conntrack_in的分析告一段落。
接下来我们分析ipv4_conntrack_help。
这个函数还是比较简单的,首先根据传入的数据包;然后找到该数据包对应的数据连接跟踪项;接着若该数据连接跟踪项的helper指针不为空,则调用该helper结构对应的help函数,实现创建期望连接或者相关协议的ALG功能(即对应用层协议中的数据部分如果有ip地址,且该数据连接跟踪项对应的数据流有开启NAT机制时,则将应用层协议中的数据部分的ip地址也进行NAT操作)。
/*
1.从skb->nfct中获取该数据包对应的连接跟踪项
2.如果该连接管跟踪项的helper指针不为0,则调用
ct->helper->help执行该连接跟踪项对应的helper函数,用来创建
期望连接或者对数据包的应用层携带的ip地址信息进行nat操作
*/
static unsigned int ipv4_conntrack_help(unsigned int hooknum, struct sk_buff **pskb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; /* This is where we call the helper: as the packet goes out. */ ct = nf_ct_get(*pskb, &ctinfo); if (ct && ct->helper) { unsigned int ret; ret = ct->helper->help(pskb, (*pskb)->nh.raw - (*pskb)->data + (*pskb)->nh.iph->ihl*4, ct, ctinfo); if (ret != NF_ACCEPT) return ret; } return NF_ACCEPT; }
进入到这个函数里的数据包,说明其连接跟踪项均已创建,此时需要做的就是对于还没有进行确认的连接跟踪项,实现确认操作,即是将该连接跟踪项对应的原始方向与应答方向的nf_conntrack_tuple_hash变量插入到nf_conntrack_hash[]数组对应的hash链表中。
/*
1.确认一个数据包对应的连接跟踪项
*/
static unsigned int ipv4_confirm(unsigned int hooknum, struct sk_buff **pskb, const structnet_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { /* We've seen it coming out the other side: confirm it */ return nf_conntrack_confirm(pskb); }
其实就是调用函数nf_conntrack_confirm,下面分析一下:
1.首先判断该数据连接跟踪项是否已经被确认,即是判断该连接跟踪项的status中的IPS_CONFIRMED_BIT位是否为1
2.若连接跟踪项尚未有确认,则调用函数__nf_conntrack_confirm进行确认
3.调用nf_ct_deliver_cached_events触发连接跟踪项对应的消息通知回调,判断是否需要向nfnetlink模块发送相应的消息。
static inline int nf_conntrack_confirm(struct sk_buff **pskb) { struct nf_conn *ct = (struct nf_conn *)(*pskb)->nfct; int ret = NF_ACCEPT; if (ct) { /*若一个连接跟踪项还没有被确认,则调用函数 __nf_conntrack_confirm对一个连接跟踪项执行确认操作。*/ if (!nf_ct_is_confirmed(ct)) ret = __nf_conntrack_confirm(pskb); /*通过调用该函数,由通知block ctnl_notifier的回调函数决定是否将该事件发送出去*/ nf_ct_deliver_cached_events(ct); } return ret; }
该函数主要确认一个连接跟踪项,确认操作只会发生在连接跟踪项的状态不为reply时。
1.调用CTINFO2DIR,判断是否是原始方向发送的数据包(仅对原始方向的数据包对应的
连接跟踪项进行确认操作。)
在
2. 若1中判断通过后,分别计算original、reply方向上的tuple变量对应的hash值,分别
为hash、repl_hash
3. 若连接跟踪项的original、reply方向上的tuple_hash变量均没有对应的nf_conntrack_hash[]链表中
则首先将original方向上的tuple_hash从unconntrack链表中删除,然后将连接跟踪项的original、
reply方向上的tuple_hash变量添加到相对应的nf_conntrack_hash[]链表中
置位连接跟踪项的status中的IPS_CONFIRMED_BIT,并启动超时定时器,用于实现对连接
跟踪项所占内存的超时回收功能。
*/
int __nf_conntrack_confirm(struct sk_buff **pskb) { unsigned int hash, repl_hash; struct nf_conn *ct; enum ip_conntrack_info ctinfo; ct = nf_ct_get(*pskb, &ctinfo); /* ipt_REJECT uses nf_conntrack_attach to attach related ICMP/TCP RST packets in other direction. Actual packet which created connection will be IP_CT_NEW or for an expected connection, IP_CT_RELATED. */ /*该函数只对dir为IP_CT_DIR_ORIGINAL状态时的nf_conn进行confirm操作 对于dir为IP_CT_DIR_REPLY状态的nf_conn,其状态已经设置过confirm,所以 不需要操作*/ if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL) return NF_ACCEPT; /*根据tuple值获取来、去的hash值*/ hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple); repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple); /* We're not in hash table, and we refuse to set up related connections for unconfirmed conns. But packet copies and REJECT will give spurious warnings here. */ /* NF_CT_ASSERT(atomic_read(&ct->ct_general.use) == 1); */ /* No external references means noone else could have confirmed us. */ NF_CT_ASSERT(!nf_ct_is_confirmed(ct)); DEBUGP("Confirming conntrack %p\n", ct); write_lock_bh(&nf_conntrack_lock); /* See if there's one in the list already, including reverse: NAT could have grabbed it without realizing, since we're not in the hash. If there is, we lost race. */ /* 若在nf_conntrack_hash链表中没有发现符合条件的nf_conn 则说明该nf_conn还没有在nf_conntrack_hash表中。 a)则需要将该nf_conn从unconfirmed链表中删除,并加入到nf_conntrack_hash 链表中 b)设置nf_conn超时时间并启动定时器 c)设置该nf_conn的状态为confirm状态 d)若对于该nf_conn,有设置SNAT/DNAT操作,则设置event状态为IPCT_NATINFO, 用于netlink通知链处理 e)若该nf_conn链接为一个期望链接,则设置event状态为IPCT_RELATED,否则 设置为IPCT_NEW 对于上面的d)、e)只是设置了event状态,而并没有将改事件通知给netlink, 主要是在nf_conntrack_confirm的最后通过调用nf_ct_deliver_cached_events,才将事件通知发送给netlink的nf_conntrack_chain,由该通知链的回调函数根据事件类型,设置相应的组并决定是否将事件通知发送出去。并根据连接跟踪项的status中IPCT_HELPER、IPS_SRC_NAT_DONE_BIT、IPS_SRC_NAT_DONE_BIT的值,来决定是否向nfnetlink模块发送消息。 */ if (!LIST_FIND(&nf_conntrack_hash[hash], conntrack_tuple_cmp, struct nf_conntrack_tuple_hash *, &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL) && !LIST_FIND(&nf_conntrack_hash[repl_hash], conntrack_tuple_cmp, struct nf_conntrack_tuple_hash *, &ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) { /* Remove from unconfirmed list */ list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list); __nf_conntrack_hash_insert(ct, hash, repl_hash); /* Timer relative to confirmation time, not original setting time, otherwise we'd get timer wrap in weird delay cases. */ ct->timeout.expires += jiffies; add_timer(&ct->timeout); atomic_inc(&ct->ct_general.use); set_bit(IPS_CONFIRMED_BIT, &ct->status); NF_CT_STAT_INC(insert); write_unlock_bh(&nf_conntrack_lock); if (ct->helper) nf_conntrack_event_cache(IPCT_HELPER, *pskb); #ifdef CONFIG_NF_NAT_NEEDED if (test_bit(IPS_SRC_NAT_DONE_BIT, &ct->status) || test_bit(IPS_DST_NAT_DONE_BIT, &ct->status)) nf_conntrack_event_cache(IPCT_NATINFO, *pskb); #endif nf_conntrack_event_cache(master_ct(ct) ? IPCT_RELATED : IPCT_NEW, *pskb); return NF_ACCEPT; } NF_CT_STAT_INC(insert_failed); write_unlock_bh(&nf_conntrack_lock); return NF_DROP; }
至此将连接跟踪模块相应的hook函数都分析完了,下一节开始分析NAT模块。