Chinaunix首页 | 论坛 | 博客
  • 博客访问: 585372
  • 博文数量: 146
  • 博客积分: 5251
  • 博客等级: 大校
  • 技术积分: 1767
  • 用 户 组: 普通用户
  • 注册时间: 2006-11-10 15:58
文章分类
文章存档

2010年(12)

2008年(129)

2007年(5)

我的朋友

分类: LINUX

2008-02-03 14:13:11

内核版本:2.6.12

本文只是一部份,详细分析了连接跟踪的基本实现,对于ALG部份,还没有写,在整理笔记,欢迎大家提意见,批评指正。

1.什么是连接跟踪
连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。Linux为每一个经过网络堆栈的数据包,生成一个新的连接记录项(Connection entry)。此后,所有属于此连接的数据包都被唯一地分配给这个连接,并标识连接的状态。连接跟踪是防火墙模块的状态检测的基础,同时也是地址转换中实现SNAT和DNAT的前提。
那么Netfilter又是如何生成连接记录项的呢?每一个数据,都有“来源”与“目的”主机,发起连接的主机称为“来源”,响应“来源”的请求的主机即为目的,所谓生成记录项,就是对每一个这样的连接的产生、传输及终止进行跟踪记录。由所有记录项产生的表,即称为连接跟踪表。

2.连接跟踪表
Netfilter使用一张连接跟踪表,来描述整个连接状态,这个表在实现算法上采用了hash算法。我们先来看看这个hash 表的实现。
整个hash表用全局指针ip_conntrack_hash 指针来描述,它定义在ip_conntrack_core.c中:
struct list_head *ip_conntrack_hash;

这个hash表的大小是有限制的,表的大小由ip_conntrack_htable_size 全局变量决定,这个值,用户态可以在模块插入时传递,默认是根据内存大小计算出来的。
        每一个hash节点,同时又是一条链表的首部,所以,连接跟踪表就由ip_conntrack_htable_size 条链表构成,整个连接跟踪表大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size。
链表的每个节点,都是一个struct ip_conntrack_tuple_hash 类型:

CODE:
/* Connections have two entries in the hash table: one for each way */
struct ip_conntrack_tuple_hash
{
        struct list_head list;

        struct ip_conntrack_tuple tuple;
};

这个结构有两个成员,list 成员用于组织链表。多元组(tuple) 则用于描述具体的数据包。
每个数据包最基本的要素,就是“来源”和“目的”,从Socket套接字角度来讲,连接两端用“地址+端口”的形式来唯一标识一个连接(对于没有端口的协议,如ICMP,可以使用其它办法替代),所以,这个数据包就可以表示为“来源地址/来源端口+目的地址/目的端口”,Netfilter用结构struct ip_conntrack_tuple 结构来封装这个“来源”和“目的”,封装好的struct ip_conntrack_tuple结构节点在内核中就称为“tuple”。最终实现“封装”,就是根据来源/目的地址、端口这些要素,来进行一个具体网络封包到tuple的转换。结构定义如下:

CODE:
/* The protocol-specific manipulable parts of the tuple: always in
   network order! */
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;
};



CODE:
/* The manipulable part of the tuple. */
struct ip_conntrack_manip
{
        u_int32_t ip;
        union ip_conntrack_manip_proto u;
};



CODE:
/* 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成员,dir是direction 的缩写,标识一个连接的方向,后面我们会看到它的用法。

tuple 结构仅仅是一个数据包的转换,并不是描述一条完整的连接状态,内核中,描述一个包的连接状态,使用了struct ip_conntrack 结构,可以在ip_conntrack.h中看到它的定义:

CODE:
struct ip_conntrack
{
        ……
        /* These are my tuples; original and reply */
        struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
};

这里仅仅是分析hash表的实现,所以,我们仅需注意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],这样,一条完整的连接已经跃然纸上了。
最后一要注意的问题,就是对于每一条连接,寻找链表在hash表的入口,也就是如计算hash值。我们关心的是一条连接,连接是由“请求”和“应答”的数据包组成,数据包会被转化成tuple,所以,hash值就是根据tuple,通过一定的hash算法实现,这样,整个hash表如下图所示:
         

如图,小结一下:
n        整个hash表用ip_conntrack_hash 指针数组来描述,它包含了ip_conntrack_htable_size个元素,用户态可以在模块插入时传递,默认是根据内存大小计算出来的;
n        整个连接跟踪表的大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size;
n        hash链表的每一个节点是一个struct ip_conntrack_tuple_hash结构,它有两个成员,一个是list,一个是tuple;
n        Netfilter将每一个数据包转换成tuple,再根据tuple计算出hash值,这样,就可以使用ip_conntrack_hash[hash_id]找到hash表中链表的入口,并组织链表;
n        找到hash表中链表入口后,如果链表中不存在此“tuple”,则是一个新连接,就把tuple插入到链表的合适位置;
n        图中两个节点tuple[ORIGINAL]和tuple[REPLY],虽然是分开的,在两个链表当中,但是如前所述,它们同时又被封装在ip_conntrack结构的tuplehash数组中,这在图中,并没有标注出来;
n        链表的组织采用的是双向链表,上图中没有完整表示出来;

        当然,具体的实现要稍微麻烦一点,主要体现在一些复杂的应用层协议上来,例如主动模式下的FTP协议,服务器在连接建立后,会主动打开高端口与客户端进行通讯,这样,由于端口变换了,我们前面说的连接表的实现就会遇到麻烦。Netfilter为这些协议提供了一个巧秒的解决办法,我们在本章中,先分析连接跟踪的基本实现,然后再来分析Netfilter对这些特殊的协议的支持的实现。

3.连接跟踪的初始化

3.1 初始化函数
ip_conntrack_standalone.c 是连接跟踪的主要模块:

CODE:
static int __init init(void)
{
        return init_or_cleanup(1);
}

初始化函数进一步调用init_or_cleanup() 进行模块的初始化,它主要完成hash表的初始化等三个方面的工作:

CODE:
static int init_or_cleanup(int init)
{
        /*初始化连接跟踪的一些变量、数据结构,如初始化连接跟踪表的大小,Hash表的大小等*/
        ret = ip_conntrack_init();
        if (ret < 0)
                goto cleanup_nothing;

/*创建proc 文件系统的对应节点*/
#ifdef CONFIG_PROC_FS
        ……
#endif

/*为连接跟踪注册Hook */
        ret = nf_register_hook(&ip_conntrack_defrag_ops);
        if (ret < 0) {
                printk("ip_conntrack: can't register pre-routing defrag hook.\n");
                goto cleanup_proc_stat;
        }
        ……
}

3.2 ip_conntrack_init

ip_conntrack_init 函数用于初始化连接跟踪的包括hash表相关参数在内一些重要的变量:

CODE:
/*用户态可以在模块插入的时候,可以使用hashsize参数,指明hash 表的大小*/
static int hashsize;
module_param(hashsize, int, 0400);

int __init ip_conntrack_init(void)
{
        unsigned int i;
        int ret;

        /* 如果模块指明了hash表的大小,则使用指定值,否则,根据内存的大小,来计算一个默认值. ,hash表的大小,是使用全局变量ip_conntrack_htable_size 来描述*/
        if (hashsize) {
                ip_conntrack_htable_size = hashsize;
        } else {
                ip_conntrack_htable_size
                        = (((num_physpages << PAGE_SHIFT) / 16384)
                           / sizeof(struct list_head));
                if (num_physpages > (1024 * 1024 * 1024 / PAGE_SIZE))
                        ip_conntrack_htable_size = 8192;
                if (ip_conntrack_htable_size < 16)
                        ip_conntrack_htable_size = 16;
        }

/*根据hash表的大小,计算最大的连接跟踪表数*/
        ip_conntrack_max = 8 * ip_conntrack_htable_size;

        printk("ip_conntrack version %s (%u buckets, %d max)"
               " - %Zd bytes per conntrack\n", IP_CONNTRACK_VERSION,
               ip_conntrack_htable_size, ip_conntrack_max,
               sizeof(struct ip_conntrack));
       
/*注册socket选项*/
        ret = nf_register_sockopt(&so_getorigdst);
        if (ret != 0) {
                printk(KERN_ERR "Unable to register netfilter socket option\n");
                return ret;
        }

        /* 初始化内存分配标识变量 */
        ip_conntrack_vmalloc = 0;

        /*为hash表分配连续内存页*/
        ip_conntrack_hash
                =(void*)__get_free_pages(GFP_KERNEL,
                                         get_order(sizeof(struct list_head)
                                                   *ip_conntrack_htable_size));
        /*分配失败,尝试调用vmalloc重新分配*/
if (!ip_conntrack_hash) {
                ip_conntrack_vmalloc = 1;
                printk(KERN_WARNING "ip_conntrack: falling back to vmalloc.\n");
                ip_conntrack_hash = vmalloc(sizeof(struct list_head)
                                            * ip_conntrack_htable_size);
        }
        /*仍然分配失败*/
        if (!ip_conntrack_hash) {
                printk(KERN_ERR "Unable to create ip_conntrack_hash\n");
                goto err_unreg_sockopt;
        }

        ip_conntrack_cachep = kmem_cache_create("ip_conntrack",
                                                sizeof(struct ip_conntrack), 0,
                                                0, NULL, NULL);
        if (!ip_conntrack_cachep) {
                printk(KERN_ERR "Unable to create ip_conntrack slab cache\n");
                goto err_free_hash;
        }

        ip_conntrack_expect_cachep = kmem_cache_create("ip_conntrack_expect",
                                        sizeof(struct ip_conntrack_expect),
                                        0, 0, NULL, NULL);
        if (!ip_conntrack_expect_cachep) {
                printk(KERN_ERR "Unable to create ip_expect slab cache\n");
                goto err_free_conntrack_slab;
        }

        /* Don't NEED lock here, but good form anyway. */
        WRITE_LOCK(&ip_conntrack_lock);
       
/* 注册协议。对不同协议,连接跟踪记录的参数不同,所以不同的协议定义了不同的 ip_conntrack_protocol结构来处理与协议相关的内容。这些结构被注册到一个全局的链表中,在使用时根据协议去查找,并调用相应的处理函数来完成相应的动作。*/
        for (i = 0; i < MAX_IP_CT_PROTO; i++)
                ip_ct_protos[i] = &ip_conntrack_generic_protocol;
        ip_ct_protos[IPPROTO_TCP] = &ip_conntrack_protocol_tcp;
        ip_ct_protos[IPPROTO_UDP] = &ip_conntrack_protocol_udp;
        ip_ct_protos[IPPROTO_ICMP] = &ip_conntrack_protocol_icmp;
        WRITE_UNLOCK(&ip_conntrack_lock);
       
        /*初始化hash表*/
        for (i = 0; i < ip_conntrack_htable_size; i++)
                INIT_LIST_HEAD(&ip_conntrack_hash[i]);

        /* For use by ipt_REJECT */
        ip_ct_attach = ip_conntrack_attach;

        /* Set up fake conntrack:
            - to never be deleted, not in any hashes */
        atomic_set(&ip_conntrack_untracked.ct_general.use, 1);
        /*  - and look it like as a confirmed connection */
        set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status);

        return ret;

err_free_conntrack_slab:
        kmem_cache_destroy(ip_conntrack_cachep);
err_free_hash:
        free_conntrack_hash();
err_unreg_sockopt:
        nf_unregister_sockopt(&so_getorigdst);

        return -ENOMEM;
}

在这个函数中,有两个重点的地方值得注意,一个是hash表的相关变量的初始化、内存空间的分析等等,另一个是协议的注册。
        连接跟踪由于针对每种协议的处理,都有些细微不同的地方,举个例子,我们前面讲到数据包至tuple的转换,TCP的转换与ICMP的转换肯定不同的,因为ICMP连端口的概念也没有,所以,对于每种协议的一些特殊处理的函数,需要进行封装,struct ip_conntrack_protocol 结构就实现了这一封装,在初始化工作中,针对最常见的TCP、UDP和ICMP协议,定义了ip_conntrack_protocol_tcp、ip_conntrack_protocol_udp和ip_conntrack_protocol_icmp三个该类型的全局变量,初始化函数中,将它们封装至ip_ct_protos 数组,这些,在后面的数据包处理后,就可以根据包中的协议值,使用ip_ct_protos[协议值],找到注册的协议节点,就可以方便地调用协议对应的处理函数了,我们在后面将看到这一调用过程。

3.2        钩子函数的注册
init_or_cleanup 函数在创建/proc文件系统完成后,会调用nf_register_hook 函数注册钩子,进行连接跟踪,按优先级和Hook不同,注册了多个钩子:

CODE:
        ret = nf_register_hook(&ip_conntrack_defrag_ops);
        if (ret < 0) {
                printk("ip_conntrack: can't register pre-routing defrag hook.\n");
                goto cleanup_proc_stat;
        }
        ret = nf_register_hook(&ip_conntrack_defrag_local_out_ops);
        if (ret < 0) {
                printk("ip_conntrack: can't register local_out defrag hook.\n");
                goto cleanup_defragops;
        }
        ……

整个Hook注册好后,如下图所示:


上图中,粗黑体标识函数就是连接跟踪注册的钩子函数,除此之外,用于处理分片包和处理复杂协议的钩子函数在上图中没有标识出来。处理分片包的钩子用于重组分片,用于保证数据在进入连接跟踪模块不会是一个分片数据包。例如,在数据包进入NF_IP_PRE_ROUTING Hook点,主要的连接跟踪函数是ip_conntrack_in,然而,在它之前,还注册了ip_conntrack_defrag,用于处理分片数据包:

CODE:
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 *))
{
        /* 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;
}

对于我们本章的分析而言,主要是以“Linux做为一个网关主机,转发过往数据”为主线,更多关注的是在NF_IP_PRE_ROUTING和NF_IP_POSTROUTING两个Hook点上注册的两个钩子函数ip_conntrack_in和ip_refrag(这个函数主要执行的是ip_confirm函数)。
        钩子的注册的另一个值得注意的小问题,就是钩子函数的优先级,NF_IP_PRE_ROUTING上的优先级是NF_IP_PRI_CONNTRACK ,意味着它的优先级是很高的,这也意味着每个输入数据包首先被传输到连接跟踪模块,才会进入其它优先级较低的模块。同样地,NF_IP_POSTROUTING上的优先级为NF_IP_PRI_CONNTRACK_CONFIRM,优先级是很低的,也就是说,等到其它优先级高的模块处理完成后,才会做最后的处理,然后将数据包送出去。

4.ip_conntrack_in

数据包进入Netfilter后,会调用ip_conntrack_in函数,以进入连接跟踪模块,ip_conntrack_in 主要完成的工作就是判断数据包是否已在连接跟踪表中,如果不在,则为数据包分配ip_conntrack,并初始化它,然后,为这个数据包设置连接状态。

CODE:
/* 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;
}

在初始化的时候,我们就提过,连接跟踪模块将所有支持的协议,都使用struct ip_conntrack_protocol 结构封装,注册至全局数组ip_ct_protos,这里首先调用函数ip_ct_find_proto根据当前数据包的协议值,找到协议注册对应的模块。然后调用resolve_normal_ct 函数进一步处理。
5.resolve_normal_ct
        resolve_normal_ct 函数是连接跟踪中最重要的函数之一,它的主要功能就是判断数据包在连接跟踪表是否存在,如果不存在,则为数据包分配相应的连接跟踪节点空间并初始化,然后设置连接状态:

CODE:
/* 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;
}

这个函数包含了连接跟踪中许多重要的步骤
n        调用ip_ct_get_tuple函数,把数据包转换为tuple;
n        ip_conntrack_find_get函数,根据tuple查找连接跟踪表;
n        init_conntrack函数,初始化一条连接;
n        判断连接方向,设置连接状态;

5.1 数据包的转换
ip_ct_get_tuple 实现数据包至tuple的转换,这个转换,主要是根据数据包的套接字对来进行转换的:

CODE:
int ip_ct_get_tuple(const struct iphdr *iph,
                const struct sk_buff *skb,
                unsigned int dataoff,
                struct ip_conntrack_tuple *tuple,
                const struct ip_conntrack_protocol *protocol)
{
                /* Never happen */
                if (iph->frag_off & htons(IP_OFFSET)) {
                        printk("ip_conntrack_core: Frag of proto %u.\n",
                       iph->protocol);
                        return 0;
        }
/*设置来源、目的地址*/
                tuple->src.ip = iph->saddr;
                tuple->dst.ip = iph->daddr;
        tuple->dst.protonum = iph->protocol;
                tuple->dst.dir = IP_CT_DIR_ORIGINAL;

        return protocol->pkt_to_tuple(skb, dataoff, tuple);
}

回忆一下我们前面分析协议的初始化中协议初始化的部份,pkt_to_tuple 函数指针,以每种协议的不同而不同,以TCP协议为例:

CODE:
static int tcp_pkt_to_tuple(const struct sk_buff *skb,
                            unsigned int dataoff,
                            struct ip_conntrack_tuple *tuple)
{
                struct tcphdr _hdr, *hp;

                /* 获取TCP报头*/
hp = skb_header_pointer(skb, dataoff, 8, &_hdr);
        if (hp == NULL)
                        return 0;
/*根据报头的端口信息,设置tuple对应成员*/
                tuple->src.u.tcp.port = hp->source;
        tuple->dst.u.tcp.port = hp->dest;

        return 1;
}

TCP协议中,根据来源和目的端口设置,其它协议类似,读者可以对比分析。

5.2 Hash 表的搜索
要对Hash表进行遍历,首要需要找到hash表的入口,然后来遍历该入口指向的链表。每个链表的节点是struct ip_conntrack_tuple_hash,它封装了tuple,所谓封装,就是把待查找的tuple与节点中已存的tuple相比较,我们来看这一过程的实现。
计算hash值,是调用hash_conntrack函数,根据数据包对应的tuple实现的:

CODE:
unsigned int hash = hash_conntrack(tuple);

        这样,tuple对应的hash表入口即为ip_conntrack_hash[hash],也就是链表的首节点,然后调用ip_conntrack_find_get函数进行查找:
struct ip_conntrack_tuple_hash *
ip_conntrack_find_get(const struct ip_conntrack_tuple *tuple,
                      const struct ip_conntrack *ignored_conntrack)
{
        struct ip_conntrack_tuple_hash *h;

        READ_LOCK(&ip_conntrack_lock);
        /*搜索链表*/
        h = __ip_conntrack_find(tuple, ignored_conntrack);
        if (h)                /*查找到了,使用计数器累加*/
                atomic_inc(&tuplehash_to_ctrack(h)->ct_general.use);
        READ_UNLOCK(&ip_conntrack_lock);

        return h;
}

链表是内核中一个标准的双向链表,可以调用宏list_for_each_entry 进遍历链表:

CODE:
static struct ip_conntrack_tuple_hash *
__ip_conntrack_find(const struct ip_conntrack_tuple *tuple,
                    const struct ip_conntrack *ignored_conntrack)
{
        struct ip_conntrack_tuple_hash *h;
        unsigned int hash = hash_conntrack(tuple);

        MUST_BE_READ_LOCKED(&ip_conntrack_lock);
        list_for_each_entry(h, &ip_conntrack_hash[hash], list) {
                if (conntrack_tuple_cmp(h, tuple, ignored_conntrack)) {
                        CONNTRACK_STAT_INC(found);
                        return h;
                }
                CONNTRACK_STAT_INC(searched);
        }

        return NULL;
}

list_for_each_entry在以&ip_conntrack_hash[hash]为起始地址的链表中,逐个搜索其成员,比较这个节点中的tuple是否与待查找的tuple是否一致,这个比较过程,是通过conntrack_tuple_cmp 函数实现的:

CODE:
conntrack_tuple_cmp(const struct ip_conntrack_tuple_hash *i,
                    const struct ip_conntrack_tuple *tuple,
                    const struct ip_conntrack *ignored_conntrack)
{
        MUST_BE_READ_LOCKED(&ip_conntrack_lock);
        return tuplehash_to_ctrack(i) != ignored_conntrack
                && ip_ct_tuple_equal(tuple, &i->tuple);
}

tuplehash_to_ctrack 函数主要是取连接跟踪ip_conntrack中的连接方向,判断它是否等于ignored_conntrack,对与这里的比较而言,ignored_conntrack传递过来的为NULL。
主要的比较函数是ip_ct_tuple_equal函数,函数分为“来源”和“目的”进行比较:

CODE:
static inline int ip_ct_tuple_src_equal(const struct ip_conntrack_tuple *t1,
                                        const struct ip_conntrack_tuple *t2)
{
        return t1->src.ip == t2->src.ip
                && t1->src.u.all == t2->src.u.all;
}

static inline int ip_ct_tuple_dst_equal(const struct ip_conntrack_tuple *t1,
                                        const struct ip_conntrack_tuple *t2)
{
        return t1->dst.ip == t2->dst.ip
                && t1->dst.u.all == t2->dst.u.all
                && t1->dst.protonum == t2->dst.protonum;
}

static inline int ip_ct_tuple_equal(const struct ip_conntrack_tuple *t1,
                                    const struct ip_conntrack_tuple *t2)
{
        return ip_ct_tuple_src_equal(t1, t2) && ip_ct_tuple_dst_equal(t1, t2);
}

这里的比较,除了IP地址之外,并没有直接比较“端口”,这是因为像ICMP协议这样的并没有“端口”协议,struct ip_conntrack_tuple 结构中,与协议相关的,如端口等,都定义成union类型,这样,就可以直接使用u.all,而不用再去管TCP,UDP还是ICMP了。

5.3 连接初始化
内核使用ip_conntrack结构来描述一个数据包的连接状态,init_conntrack函数就是在连接状态表中不存在当前数据包时,初始化一个ip_conntrack结构,此结构被Netfilter用来描述一条连接,前面分析hash表时,已经分析了它的tuplehash成员:

CODE:
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

        struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
};


static struct ip_conntrack_tuple_hash *
init_conntrack(const struct ip_conntrack_tuple *tuple,
               struct ip_conntrack_protocol *protocol,
               struct sk_buff *skb)
{
        struct ip_conntrack *conntrack;
        struct ip_conntrack_tuple repl_tuple;
        size_t hash;
        struct ip_conntrack_expect *exp;

        /*如果计算hash值的随机数种子没有被初始化,则初始化之*/
        if (!ip_conntrack_hash_rnd_initted) {
                get_random_bytes(&ip_conntrack_hash_rnd, 4);
                ip_conntrack_hash_rnd_initted = 1;
        }

        /*计算hash值*/
        hash = hash_conntrack(tuple);
       
        /*判断连接跟踪表是否已满*/
        if (ip_conntrack_max
            && atomic_read(&ip_conntrack_count) >= ip_conntrack_max) {
                /* Try dropping from this hash chain. */
                if (!early_drop(&ip_conntrack_hash[hash])) {
                        if (net_ratelimit())
                                printk(KERN_WARNING
                                       "ip_conntrack: table full, dropping"
                                       " packet.\n");
                        return ERR_PTR(-ENOMEM);
                }
        }

        /*根据当前的tuple取反,计算该数据包的“应答”的tuple*/
        if (!ip_ct_invert_tuple(&repl_tuple, tuple, protocol)) {
                DEBUGP("Can't invert tuple.\n");
                return NULL;
        }
        /*为数据包对应的连接分配空间*/
        conntrack = kmem_cache_alloc(ip_conntrack_cachep, GFP_ATOMIC);
        if (!conntrack) {
                DEBUGP("Can't allocate conntrack.\n");
                return ERR_PTR(-ENOMEM);
        }
        /*初始化该结构*/
        memset(conntrack, 0, sizeof(*conntrack));
        /*使用计数器累加*/
        atomic_set(&conntrack->ct_general.use, 1);
        /*设置destroy函数指针*/
        conntrack->ct_general.destroy = destroy_conntrack;
        /*设置正反两个方向的tuple*/
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
        conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
        if (!protocol->new(conntrack, skb)) {
                kmem_cache_free(ip_conntrack_cachep, conntrack);
                return NULL;
        }
        /* 初始化时间计数器,并设置超时初始函数 */
        init_timer(&conntrack->timeout);
        conntrack->timeout.data = (unsigned long)conntrack;
        conntrack->timeout.function = death_by_timeout;

        WRITE_LOCK(&ip_conntrack_lock);
        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... */
                __set_bit(IPS_EXPECTED_BIT, &conntrack->status);
                conntrack->master = exp->master;
#if CONFIG_IP_NF_CONNTRACK_MARK
                conntrack->mark = exp->master->mark;
#endif
                nf_conntrack_get(&conntrack->master->ct_general);
                CONNTRACK_STAT_INC(expect_new);
        } else {
                conntrack->helper = ip_ct_find_helper(&repl_tuple);

                CONNTRACK_STAT_INC(new);
        }

        /* 这里,并没有直接就把该连接加入hash表,而是先加入到unconfirmed链表中. */
        list_add(&conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list, &unconfirmed);

        atomic_inc(&ip_conntrack_count);
        WRITE_UNLOCK(&ip_conntrack_lock);

        if (exp) {
                if (exp->expectfn)
                        exp->expectfn(conntrack, exp);
                destroy_expect(exp);
        }

        /*返回的是初始方向的hash节点*/
        return &conntrack->tuplehash[IP_CT_DIR_ORIGINAL];
}

在前文中提到过,一条完整的连接,采用struct ip_conntrack 结构描述,初始化函数的主要功能,就是分配一个这样的空间,然后初始化它的一些成员。

在这个函数中,有三个重要的地方需要注意,一个是根据当前tuple,计算出应答方向的tuple,它是调用ip_ct_invert_tuple 函数实现的:

CODE:
int
ip_ct_invert_tuple(struct ip_conntrack_tuple *inverse,
                   const struct ip_conntrack_tuple *orig,
                   const struct ip_conntrack_protocol *protocol)
{
        inverse->src.ip = orig->dst.ip;
        inverse->dst.ip = orig->src.ip;
        inverse->dst.protonum = orig->dst.protonum;
        inverse->dst.dir = !orig->dst.dir;

        return protocol->invert_tuple(inverse, orig);
}

这个函数事实上,与前面讲的tuple的转换是一样的,只是来了个乾坤大挪移,把来源和目的,以及方向对调了。

另一个重点的是函数对特殊协议的支持,我们这里暂时跳过了这部份。

第三个地方是调用协议的new函数:
        if (!protocol->new(conntrack, skb)) {
                kmem_cache_free(ip_conntrack_cachep, conntrack);
                return NULL;
        }
new 函数指定在每个封包第一次创建连接时被调用,它根据协议的不同,所处理的过程不同,以ICMP协议为例:

CODE:
/* Called when a new connection for this protocol found. */
static int icmp_new(struct ip_conntrack *conntrack,
                    const struct sk_buff *skb)
{
        static u_int8_t valid_new[]
                = { [ICMP_ECHO] = 1,
                    [ICMP_TIMESTAMP] = 1,
                    [ICMP_INFO_REQUEST] = 1,
                    [ICMP_ADDRESS] = 1 };

        if (conntrack->tuplehash[0].tuple.dst.u.icmp.type >= sizeof(valid_new)
            || !valid_new[conntrack->tuplehash[0].tuple.dst.u.icmp.type]) {
                /* Can't create a new ICMP `conn' with this. */
                DEBUGP("icmp: can't create new conn with type %u\n",
                       conntrack->tuplehash[0].tuple.dst.u.icmp.type);
                DUMP_TUPLE(&conntrack->tuplehash[0].tuple);
                return 0;
        }
        atomic_set(&conntrack->proto.icmp.count, 0);
        return 1;
}

对于ICMP协议而言,仅有ICMP 请求回显、时间戳请求、信息请求(已经很少用了)、地址掩码请求这四个“请求”,可能是一个“新建”的连接,所以,ICMP协议的new函数判断是否是一个全法的ICMP新建连接,如果是非法的,则返回0,否则,初始化协议使用计数器,返回1。

5.4 连接状态的判断
resolve_normal_ct 函数的最后一个重要的工作是对连接状态的判断,tuple中包含一个“方向”成员dst.dir,对于一个初始连接,它是IP_CT_DIR_ORIGINAL:
tuple->dst.dir = IP_CT_DIR_ORIGINAL;
而它的应答包的tuple,则为IP_CT_DIR_REPLY:
inverse->dst.dir = !orig->dst.dir;
IP_CT_DIR_ORIGINAL 和IP_CT_DIR_REPLY都是枚举变量:

CODE:
enum ip_conntrack_dir
{
        IP_CT_DIR_ORIGINAL,
        IP_CT_DIR_REPLY,
        IP_CT_DIR_MAX
};

宏DIRECTION 就根据tuple中对应成员的值,判断数据包的方向,
/* If we're the first tuple, it's the original dir. */
#define DIRECTION(h) ((enum ip_conntrack_dir)(h)->tuple.dst.dir)

但是,还有一些特殊地方,比如TCP协议,它是一个面向连接的协议,所以,它的“初始”或“应答”包,并不一定就是“新建”或单纯的“应答”包,而是在一个连接过程中的“已建连接包”,另一个,如FTP等 复杂协议,它们还存在一些“关联”的连接,当然这两部份目前还没有涉及到,但并不影响我们分析如下这段代码:

CODE:
        /* 如果是一个应答包 ,设置状态为已建+应答*/
        if (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)) {
                        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;
        /*每个sk_buff都将与ip_conntrack的一个状态关联,所以从sk_buff可以得到相应ip_conntrack的状态,即数据包的状态*/
        skb->nfctinfo = *ctinfo;
        return ct;

以上的代表所表示的发送或应答的状态如下图所示:


6.        ip_confirm

以上的工作事实上都很简单,基本思路是:
一个包来了,转换其tuple,看其在连接跟踪表中没有,有的话,更新其状态,以其做一些与协议相关的工作,如果没有,则分配一个新的连接表项,并与skb_buff关连,但是问题是,这个表项,还没有被加入连接表当中来。其实这样做的理由很简单,因为这个时候,这个包是否有机会活命还是个未知数,例如被其它模块给Drop了……所以,要等到一切安全了,再来将这个表项插入至连接跟踪表。
这个“一切安全”当然是Netfilter所有的模块处理完了,最完全了。

当数据包要离开Netfilter时,它会穿过NF_IP_POST_ROUTING Hook点,状态跟踪模块在这里注册了ip_refrag函数(前面谈到过它的优先级是很低的)。这个Hook函数的工作,也可以猜测到了:“判断表项是否已经在连接跟踪表中了,如果没有,就将其插入表中”!

CODE:
static unsigned int ip_refrag(unsigned int hooknum,
                              struct sk_buff **pskb,
                              const struct net_device *in,
                              const struct net_device *out,
                              int (*okfn)(struct sk_buff *))
{
        struct rtable *rt = (struct rtable *)(*pskb)->dst;

        /* ip_confirm函数用于处理将tuple加入hash表等重要的后续处理 */
        if (ip_confirm(hooknum, pskb, in, out, okfn) != NF_ACCEPT)
                return NF_DROP;

        /* 在连接跟踪开始之前,对分片包进行了重组,这里判断数据包是否需要分片,如果要分片,就调用ip_fragment分片函数将数据包分片发送出去,因为数据包已经被发送走了,所以,在它之后的任何Hook函数已经没有意思了 */
        if ((*pskb)->len > dst_mtu(&rt->u.dst) &&
            !skb_shinfo(*pskb)->tso_size) {
                /* No hook can be after us, so this should be OK. */
                ip_fragment(*pskb, okfn);
                return NF_STOLEN;
        }
        return NF_ACCEPT;
}

ip_confirm 函数是状态跟踪的另一个重要的函数:

CODE:
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 *))
{
        return ip_conntrack_confirm(pskb);
}

函数仅是转向,将控制权转交给ip_conntrack_confirm函数:

CODE:
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函数中来设置的:

[code
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);

        /* 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;
}[/code]

这样,一条新建连接就被加入到表项当中了。如果其有后续连接,如应答,进入连接跟踪表,又转换其tuple,然后查到此表项,循环中……
阅读(1263) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~