Netfilter之连接跟踪实现机制初步分析
Netfilter之连接跟踪实现机制初步分析
原文:
http://blog.chinaunix.net/uid-22227409-id-2656910.html
什么是连接跟踪
连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。Linux为每一个经过网络堆栈的数据包,生成一个新的连接记录项(Connection entry)。此后,所有属于此连接的数据包都被唯一地分配给这个连接,并标识连接的状态。连接跟踪是防火墙模块的状态检测的基础,同时也是地址转换中实现SNAT和DNAT的前提。
那么Netfilter又是如何生成连接记录项的呢?每一个数据,都有“来源”与“目的”主机,发起连接的主机称为“来源”,响应“来源”的请求的主机即为目的,所谓生成记录项,就是对每一个这样的连接的产生、传输及终止进行跟踪记录。由所有记录项产生的表,即称为连接跟踪表。
Netfilter中的连接跟踪模块作为地址转换等的基础,在对Netfilter的实现机制有所了解的基础上再深入理解连接跟踪的实现机制,对于充分应用Netfilter框架的功能和扩展其他的模块有重大的作用。
在这里只是对连接跟踪模块整体流程的一个粗略描述,主要参考了cu论坛上的两篇文章:
http://linux.chinaunix.net/bbs/viewthread.php?tid=1057483
http://bbs.chinaunix.net/viewthread.php?tid=815129&extra=&page=1
整体框架
连接跟踪机制是基于Netfilter架构实现的,其在Netfilter的不同钩子点中注册了相应的钩子函数,如下图所示
主要挂载函数如下:
NF_IP_PRE_ROUTING: ip_conntrack_defrag(), ip_conntrack_in();
NF_IP_LOCAL_IN: ip_confirm();
NF_IP_LOCAL_OUT: ip_conntrack_defrag(),ip_conntrack_local();
NF_IP_POST_ROUTING: ip_confirm();
其中ip_conntrack_defrag()用于分片数据包的重组,defrag钩子函数的优先级高于conntrack,所以重组会在连接建立之前执行
ip_conntrack_in()函数根据数据包协议找到其连接跟踪中的对应模块,若找到,则对sk_buf中的nfct字段进行标记,若没有,则新创建一个连接跟踪;ip_conntrack_local()实际也是调用了ip_conntrack_in()函数来实现。
ip_confirm()用于将创建新创建的连接跟踪挂载进系统的连接跟踪表中,因为对应某些数据包可能被过滤函数给丢弃了,所以在最后时候LOCAL_IN及POST_ROUTING处才将新建跟踪挂在入跟踪表中。
重要数据结构
Netfilter使用一张连接跟踪表,来描述整个连接状态,这个表在实现上采用了hash算法。
struct list_head *ip_conntrack_hash;
每一个hash节点,同时又是一条链表的首部,链表的每个节点都是一个struct ip_conntrack_tuple_hash类型;
struct ip_conntrack_tuple_hash
{
struct list_head list;
struct ip_conntrack_tuple tuple;
};
list用于组织链表,多元组tuple则用于描述具体的数据包。
对于每个数据包最基本的要素,就是来源和目的,所以这个数据包就可以表示为“源地址/源端口+目的地址/目的端口”(对于没有端口的协议,如ICMP,可以使用其他办法替代)。
union ip_conntrack_manip_proto
{
/* Add other protocols here. */
u_int16_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int16_t id;
} icmp;
struct {
u_int16_t port;
} sctp;
};
/* The manipulable part of the tuple. */
struct ip_conntrack_manip
{
u_int32_t ip;
union ip_conntrack_manip_proto u;
};
/* This contains the information to distinguish a connection. */
struct ip_conntrack_tuple
{
struct ip_conntrack_manip src;
/* These are the parts of the tuple which are fixed. */
struct {
u_int32_t ip;
union {
/* Add other protocols here. */
u_int16_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int8_t type, code;
} icmp;
struct {
u_int16_t port;
} sctp;
} u;
/* The protocol. */
u_int8_t protonum;
/* The direction (for tuplehash) */
u_int8_t dir;
} dst;
};
对于struct ip_conntrack_tuple实际只包含了src,dst两个成员,包含ip以及各个协议的端口;dst成员中有一个dir成员,用于标识连接的方向。
uple 结构仅仅是一个数据包的转换,并不是描述一条完整的连接状态,内核中,描述一个包的连接状态,使用了struct ip_conntrack 结构,可以在ip_conntrack.h中看到它的定义:
struct ip_conntrack
{
/* 包含了使用计数器和指向删除连接的函数的指针 */
struct nf_conntrack ct_general;
/* 连接状态位,它通常是一个ip_conntrack_status类型的枚举变量,如IPS_SEEN_REPLY_BIT等*/
unsigned long status;
/* 内核的定时器,用于处理连接超时 */
struct timer_list timeout;
#ifdef CONFIG_IP_NF_CT_ACCT
/* Accounting Information (same cache line as other written members) */
struct ip_conntrack_counter counters[IP_CT_DIR_MAX];
#endif
/* If we were expected by an expectation, this will be it */
struct ip_conntrack *master;
/* Current number of expected connections */
unsigned int expecting;
/* Helper, if any. */
struct ip_conntrack_helper *helper;
/* Storage reserved for other modules: */
union ip_conntrack_proto proto;
union ip_conntrack_help help;
#ifdef CONFIG_IP_NF_NAT_NEEDED
struct {
struct ip_nat_info info;
#if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || \
defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)
int masq_index;
#endif
} nat;
#endif /* CONFIG_IP_NF_NAT_NEEDED */
#if defined(CONFIG_IP_NF_CONNTRACK_MARK)
unsigned long mark;
#endif
/* Traversed often, so hopefully in different cacheline to top */
/* These are my tuples; original and reply */
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
};
struct ip_conntrack结构的最后一个成员tuplehash,它是一个struct ip_conntrack_tuple_hash 类型的数组,我们前面说了,该结构描述链表中的节点,这个数组包含“初始”和“应答”两个成员(tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]),所以,当一个数据包进入连接跟踪模块后,先根据这个数据包的套接字对转换成一个“初始的”tuple,赋值给tuplehash[IP_CT_DIR_ORIGINAL],然后对这个数据包“取反”,计算出“应答”的tuple,赋值给tuplehash[IP_CT_DIR_REPLY],这样,一条完整的连接已经跃然纸上了。
对于一些特殊的应用则需要ip_conntrack_helper、ip_conntrack_expect提供功能的扩展,这里只是简单分析,对于这两个结构暂不做了解。
重要函数
ip_conntrack_defrag()
ip_conntrack_defrag()函数对分片的包进行重组,其调用ip_ct_gather_frag()收集已经到达的分片包,然后再调用函数ip_defrag()实现数据分片包的重组。ip_conntrack_defrag()被挂载在钩子点NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT,即从外面进来的数据包或本地主机生成的数据包会首先调用该函数。该函数只操作数据包的内容,对连接跟踪记录没有影响也没有操作,如果不需要进行重组操作则直接返回NF_ACCEPT。函数的定义如下:
static unsigned int ip_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 = ip_ct_gather_frags(*pskb,
hooknum == NF_IP_PRE_ROUTING ?
IP_DEFRAG_CONNTRACK_IN :
IP_DEFRAG_CONNTRACK_OUT);
if (!*pskb)
return NF_STOLEN;
}
return NF_ACCEPT;
}
ip_conntrack_in
函数ip_conntrack_in()被挂载在钩子点NF_IP_PRE_ROUTING,同时该函数也被挂载在钩子点NF_IP_LOCAL_OUT的函数ip_conntrack_local()调用,连接跟踪模块在这两个钩子点挂载的函数对数据包的处理区别仅在于对分片包的重组方式有所不同。
函数ip_conntrack_in()首先调用__ip_conntrack_proto_find(),根据数据包的协议找到其应该使用的传输协议的连接跟踪模块,接下来调用协议模块的error()对数据包进行正确性检查,然后调用函数resolve_normal_ct()选择正确的连接跟踪记录,如果没有,则创建一个新纪录。接着调用协议模块的packet()函数,如果返回失败,则nf_conntrack_put()将释放连接记录。ip_conntrack_in()函数的源码如下,函数resolve_normal_ct()实际操作了数据包和连接跟踪表的内容。
/* Netfilter hook itself. */
unsigned int ip_conntrack_in(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
struct ip_conntrack_protocol *proto;
int set_reply;
int ret;
/* 判断当前数据包是否已被检查过了 */
if ((*pskb)->nfct) {
CONNTRACK_STAT_INC(ignore);
return NF_ACCEPT;
}
/* 分片包当会在前一个Hook中被处理,事实上,并不会触发该条件 */
if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) {
if (net_ratelimit()) {
printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\n",
(*pskb)->nh.iph->protocol, hooknum);
}
return NF_DROP;
}
/* 将当前数据包设置为未修改 */
(*pskb)->nfcache |= NFC_UNKNOWN;
/*根据当前数据包的协议,查找与之相应的struct ip_conntrack_protocol结构*/
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
/* 没有找到对应的协议. */
if (proto->error != NULL
&& (ret = proto->error(*pskb, &ctinfo, hooknum)) <= 0) {
CONNTRACK_STAT_INC(error);
CONNTRACK_STAT_INC(invalid);
return -ret;
}
/*在全局的连接表中,查找与当前包相匹配的连接结构,返回的是struct ip_conntrack *类型指针,它用于描述一个数据包的连接状态*/
if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo))) {
/* Not valid part of a connection */
CONNTRACK_STAT_INC(invalid);
return NF_ACCEPT;
}
if (IS_ERR(ct)) {
/* Too stressed to deal. */
CONNTRACK_STAT_INC(drop);
return NF_DROP;
}
IP_NF_ASSERT((*pskb)->nfct);
/*Packet函数指针,为数据包返回一个判断,如果数据包不是连接中有效的部分,返回-1,否则返回NF_ACCEPT。*/
ret = proto->packet(ct, *pskb, ctinfo);
if (ret < 0) {
/* Invalid: inverse of the return code tells
* the netfilter core what to do*/
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
CONNTRACK_STAT_INC(invalid);
return -ret;
}
/*设置应答状态标志位*/
if (set_reply)
set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
return ret;
}
在函数中首先检查数据包是否已经被检查过,或者是否为分片数据包;
然后根据当前包的协议,查找对应的ip_conntrack_protocol结构,其中包含了连接项tuple等一些数据的生成函数,对于不同的协议都有其不同的数据结构。
对于连接跟踪模块将所有支持的协议,都使用struct ip_conntrack_protocol 结构封装,注册至全局数组ip_ct_protos,这里首先调用函数ip_ct_find_proto根据当前数据包的协议值,找到协议注册对应的模块。然后调用resolve_normal_ct 函数进一步处理。
resolve_normal_ct函数是连接跟踪中最重要的函数之一,它的主要功能就是判断数据包在连接跟踪表是否存在,如果不存在,则为数据包分配相应的连接跟踪节点空间并初始化,然后设置连接状态。
/* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
static inline struct ip_conntrack *
resolve_normal_ct(struct sk_buff *skb,
struct ip_conntrack_protocol *proto,
int *set_reply,
unsigned int hooknum,
enum ip_conntrack_info *ctinfo)
{
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple_hash *h;
struct ip_conntrack *ct;
IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
/*前面提到过,需要将一个数据包转换成tuple,这个转换,就是通过ip_ct_get_tuple函数实现的*/
if (!ip_ct_get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4,
&tuple,proto))
return NULL;
/*查看数据包对应的tuple在连接跟踪表中是否存在 */
h = ip_conntrack_find_get(&tuple, NULL);
if (!h) {
/*如果不存在,初始化之*/
h = init_conntrack(&tuple, proto, skb);
if (!h)
return NULL;
if (IS_ERR(h))
return (void *)h;
}
/*根据hash表节点,取得数据包对应的连接跟踪结构*/
ct = tuplehash_to_ctrack(h);
/* 判断连接的方向 */
if (DIRECTION(h) == IP_CT_DIR_REPLY) {
*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. */
if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
DEBUGP("ip_conntrack_in: normal packet for %p\n",
ct);
*ctinfo = IP_CT_ESTABLISHED;
} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
DEBUGP("ip_conntrack_in: related packet for %p\n",
ct);
*ctinfo = IP_CT_RELATED;
} else {
DEBUGP("ip_conntrack_in: new packet for %p\n",
ct);
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
/*设置skb的对应成员,如使用计数器、数据包状态标记*/
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo;
return ct;
}
对于新建连接,连接跟踪初始化的工作有resolve_normal_ct下的init_conntrack完成,完成struct ip_conntrack数据结构的填充。
在函数的最后完成连接状态的判断,位于tuple中的dst.dir中,对于初始连接,它是IP_CT_DIR_ORIGINAL,对于它的应答包,则为IP_CT_DIR_REPLY;同时,比如对于TCP协议,它是一个面向连接的协议,所以,它的初始或应答包,并不一定就是新建或单纯的应答包,而是一个连接过程中的已建连接包,所以需要对连接状态做额外的判断。
ip_confirm
函数ip_confirm()被挂载在钩子点NF_IP_LOCAL_IN和NF_IP_POST_ROUTING,其对数据包再次进行连接跟踪记录确认,并将新建的连接跟踪记录加到表中。考虑到包可能被过滤掉,之前新建的连接跟踪记录实际上并未真正加到连接跟踪表中,而在最后由函数ip_confirm()确认后真正添加,实际对传来的sk_buff进行确认的函数是__ip_conntrack_confirm()。在该函数中首先调用函数ip_conntrack_get()查找相应的连接跟踪记录,如果数据包不是IP_CT_DIR_ORIGINAL方向的包,则直接ACCEPT,否则接着调用hash_conntrack()计算所找到的连接跟踪记录的ip_conntrack_tuple类型的hash值,且同时计算两个方向的值。然后根据这两个hash值分别查找连接跟踪记录的hash表,如果找到了,则返回NF_DROP,如果未找到,则调用函数__ip_conntrack_hash_insert()将两个方向的连接跟踪记录加到hash表中。
static unsigned int ip_confirm(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* We've seen it coming out the other side: confirm it */
return ip_conntrack_confirm(pskb);
}
函数仅是转向;
/* Confirm a connection: returns NF_DROP if packet must be dropped. */
static inline int ip_conntrack_confirm(struct sk_buff **pskb)
{
if ((*pskb)->nfct
&& !is_confirmed((struct ip_conntrack *)(*pskb)->nfct))
return __ip_conntrack_confirm(pskb);
return NF_ACCEPT;
}
is_comfirmed函数用于判断数据包是否已经被__ip_conntrack_confirm函数处理过了,它是通过IPS_CONFIRMED_BIT 标志位来判断,而这个标志位当然是在__ip_conntrack_confirm函数中来设置的:
/* Confirm a connection given skb; places it in hash table */
int
__ip_conntrack_confirm(struct sk_buff **pskb)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/*取得数据包的连接状态*/
ct = ip_conntrack_get(*pskb, &ctinfo);
/* 如果当前包不是一个初始方向的封包,则直接返回. */
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. */
/* IP_NF_ASSERT(atomic_read(&ct->ct_general.use) == 1); */
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* 在hash表中查找初始及应答的节点*/
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
/* Remove from unconfirmed list */
list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list);
/*将当前连接(初始和应答的tuple)添加进hash表*/
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
/* 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);
CONNTRACK_STAT_INC(insert);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
CONNTRACK_STAT_INC(insert_failed);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
ip_conntrack_local
函数ip_conntrack_local()被挂载在钩子点NF_IP_LOCAL_OUT,该函数会调用ip_conntrack_in(),函数源码如下:
static unsigned int ip_conntrack_local(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* root is playing with raw sockets. */
if ((*pskb)->len < sizeof(struct iphdr)
|| (*pskb)->nh.iph->ihl * 4 < sizeof(struct iphdr)) {
if (net_ratelimit())
printk("ipt_hook: happy cracking.\n");
return NF_ACCEPT;
}
return ip_conntrack_in(hooknum, pskb, in, out, okfn);
}
数据包转发的连接跟踪流程
下面以数据包转发为例描述连接跟踪的流程,其中的函数及结构体为前几节所介绍的一部分,图中主要想体现数据包sk_buff在连接跟踪流程中的相应改变,连接跟踪记录与连接跟踪表的关系,何时查找和修改连接跟踪表,辅助模块以及传输协议如何在连接跟踪中使用等。所有的函数说明以及结构体在之前都有描述。发往本机以及本机发出的数据包的连接跟踪流程在此不再做分析。
总结
以上只是简要分析了Netfilter架构中连接跟踪功能的实现机制,其中很多细节被忽略,主要目的是学习,了解整个实现框架。
后记
对于Linux 2.4版本的内核中,不支持基于ipv6协议的连接跟踪,在2.6以后开始支持,目前我需要学习版本为2.6.30,只看了连接跟踪这一块,感觉变化比较大,很多数据结构及函数名称发生了改变,不过整体思想没变。
比照总结版本来说,对于每一个包的truple结构的内容发生了改变,虽然同样只是保护src及dst,但是对于其中的变量发生了改变,同时兼容于ipv4地址以及ipv6地址。
而对于整个连接跟踪的建立流程来说,对于ipv4及ipv6分别注册了相应的钩子函数,如conntrack_in,defrag等。在这些函数的过程中采用了抽象的方法,首先完成基于自己特殊协议的一些变量的设置,之后从ipv4及ipv6的过程中抽象出共同的函数,如nf_conntrack_in等,通过这样的方式来完成连接跟踪机制的实现。
由于刚开始接触,很多细节还不了解,还没能分析明天,打算明天仿照上面画一个连接跟踪的流程图出来。。。。