Netfilter之链接跟踪实现机制初步分析php
原文:html
http://blog.chinaunix.net/uid-22227409-id-2656910.htmllinux
什么是链接跟踪算法
链接跟踪(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=1app
总体框架框架
链接跟踪机制是基于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等,经过这样的方式来完成链接跟踪机制的实现。
因为刚开始接触,不少细节还不了解,还没能分析明天,打算明天仿照上面画一个链接跟踪的流程图出来。。。。