IP碎片重组(2)

网络数据接收过程分析(四)---IP碎片重组(2)

(2008-09-19 14:31:39)

ip_defrag函数

 

 

struct sk_buff *ip_defrag(struct sk_buff *skb, u32 user)

{

      struct ipq *qp;

      struct net_device *dev;

 

      IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);

 

      //查看已经为碎片分配的内存是否已经超过了事先定义的上限,

      // sysctl_ipfrag_high_thresh初始化如下:

      // int sysctl_ipfrag_high_thresh __read_mostly =256*1024

      //所以,装载碎片的内存上限是256K

      if (atomic_read(&ip_frag_mem) >sysctl_ipfrag_high_thresh)

      

      //释放当前缓冲区中未能重组的数据包

      ip_evictor();

      

      dev = skb->dev;

 

      //在ipq_hash哈希表中查找相对应的ipq项

      if ((qp = ip_find(ip_hdr(skb), user)) != NULL) {

             struct sk_buff *ret = NULL;

 

             spin_lock(&qp->lock);

 

             //将skb碎片包插入ipq中的碎片包链表

             ip_frag_queue(qp, skb);

             

             //如果是第一个碎片包或者最后一个碎片包,并且

             //偏移长度总和等于碎片长度总和,进入重组函数

             if (qp->last_in == (FIRST_IN|LAST_IN)&&

                qp->meat == qp->len)

                    ret = ip_frag_reasm(qp, dev);

 

             spin_unlock(&qp->lock);

             ipq_put(qp, NULL);

             return ret;

      }

 

      IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);

      kfree_skb(skb);

      return NULL;

}

 

 

ip_find函数

 

static inline struct ipq *ip_find(struct iphdr *iph, u32user)

{

      __be16 id = iph->id;

      __be32 saddr = iph->saddr;

      __be32 daddr = iph->daddr;

      __u8 protocol = iph->protocol;

      unsigned int hash;

      struct ipq *qp;

      struct hlist_node *n;

 

      read_lock(&ipfrag_lock);

 

      //根据源目的IP,id,协议号算出哈希值

      hash = ipqhashfn(id, saddr, daddr, protocol);

      

      //根据哈希值,在ipq_hash表中找到相应的链表头,进行遍历

      hlist_for_each_entry(qp, n, &ipq_hash[hash], list){

 

             //这里可以看出匹配的条件是源目的ip,id,协议号以及user相等,

             //其中比较有意思的是user项,原来2.4.x内核中是没有这个项的,

             //user实际上是个枚举值,指明了重组发生的位置,枚举类型定义如下:

              /--------------------------------------------------------

             enum ip_defrag_users

             {

            IP_DEFRAG_LOCAL_DELIVER,

            IP_DEFRAG_CALL_RA_CHAIN,

            IP_DEFRAG_CONNTRACK_IN,

            IP_DEFRAG_CONNTRACK_OUT,

            IP_DEFRAG_VS_IN,

            IP_DEFRAG_VS_OUT,

            IP_DEFRAG_VS_FWD

             };

 

             目前的user值应该是IP_DEFRAG_LOCAL_DELIVER,还可能是在

             CONNTRACK或者其他处理过程中调用的重组函数

             -----------------------------------------------------/

             if (qp->id ==id           &&

                qp->saddr ==saddr      &&

                qp->daddr ==daddr     &&

                qp->protocol == protocol&&

                qp->user == user) {

                    atomic_inc(&qp->refcnt);

                    read_unlock(&ipfrag_lock);

                    return qp;

             }

      }

      read_unlock(&ipfrag_lock);

 

      return ip_frag_create(iph, user);

}


IP分片重组的分析

分类: linux 应用基础知识 1745人阅读 评论(0) 收藏 举报

http://hi.baidu.com/liulife/blog/item/4f0564ef9cbfd413fcfa3c09.html

 

 

1. 概述

在linux源代码中,ip分片重组的全部程序几乎都在都在/net/ipv4/ip_fragment.c
文件中。其对外提供一个函数接口ip_defrag()。其函数原型如下:

struct sk_buff *ip_defrag(struct sk_buff *skb)

众所周知,网络数据报在linux的网络堆栈中是以sk_buff的结构传送的,ip_defrag()的
功能就是接受分片的数据包(sk_buff),并试图进行组合,当完整的包组合好时,将新的
sk_buff返还,否则返回一个空指针。

此函数在其他文件中的调用如下:

ip层接收主函数为ip_rcv()(/net/ipv4/ip_input.c),任何IP包都需经过此函数处理。
如果此包是发往本机,则调用ip_local_deliver()函数(/net/ipv4/ip_input.c)进行
处理,一般的系统碎片只有在到达最终目的的时候才进行重组(尽管在传输过程中可
能被进一步分成更小的片)。在ip_local_deliver()中我们可发现如下代码:

if (sysctl_ip_always_defrag == 0 &&           /*编译时未设置提前组装*/
(iph->frag_off & htons(IP_MF|IP_OFFSET))) {   /*判断是否是分片包*/
skb = ip_defrag(skb);         /*条件满足,进行组装*/ 
if (!skb)                     /*若组装好则进行下一步处理,出错
return 0;             或仍未组装完返回*/
iph = skb->nh.iph;            /*重新定位ip头的指针*/
}

iph->frag_off只有在设置MF(more fragment)或offset!=0才意味着是分片包,因此
此处的检验理所当然,但为什么判断sysctl_ip_always_defrag == 0呢?
在看ip_rcv()时我们应该已经注意到在刚进行了版本号,长度,校验和等判断后,有如下
一段代码:

if (sysctl_ip_always_defrag != 0 &&
            iph->frag_off & htons(IP_MF|IP_OFFSET)) {
skb = ip_defrag(skb);
if (!skb)
return 0;
iph = skb->nh.iph;
ip_send_check(iph);
}

即如果sysctl_ip_always_defrag==1的话,ip_defrag()的调用位置将有变化,对任何
进来的IP分片都要进行重组,可以想像,如果此机器作路由器的话,将对所有的分片
组装好后,才会进行转发。此举一般是没有必要的。这个值可以通过sysctl命令动态
设置,用sysctl -a可以看到在一般的系统中,此值被设为0:

#sysctl -a
......
net.ipv4.ip_always_defrag = 0
......

2. 关键数据结构(2.2系列)

每一个分片用ipfrag结构表示:

/* Describe an IP fragment. */
struct ipfrag {
int offset; /* offset of fragment in IP datagram */
int end; /* last byte of data in datagram */
int len; /* length of this fragment */
struct sk_buff *skb; /* complete received fragment */
unsigned char *ptr; /* pointer into real fragment data */
struct ipfrag *next; /* linked list pointers */
struct ipfrag *prev;
};

这些分片形成一个双向链表(在linux内核中,若需要使用链表,除非有特殊需要,否则推荐
双向链表,见document/CodingStyle),表示一个未组装完的分片队列(属于一个ip包)。
这个链表的头指针要放在ipq结构中:

/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq {
struct iphdr *iph; /* pointer to IP header */
struct ipq *next; /* linked list pointers */
struct ipfrag *fragments; /* linked list of received fragments */
int len; /* total length of original datagram */
short ihlen; /* length of the IP header */ 
struct timer_list timer; /* when will this queue expire? */
struct ipq **pprev;
struct device *dev; /* Device - for icmp replies */
};

注意每个ipq保留了一个定时器(即struct timer_list timer;)。

ipq也会形成一个链表,它们是内核当前未组装完的所有IP包。为了便于查找,保留了一个
hash表:

#define IPQ_HASHSZ 64
struct ipq *ipq_hash[IPQ_HASHSZ];
#define ipqhashfn(id, saddr, daddr, prot) /
 ((((id) >> 1) ^ (saddr) ^ (daddr) ^ (prot)) & (IPQ_HASHSZ - 1))
Hash表 
 --------_____________ 
 |   1  |            | 
 --------        -----------      ------------     ------------
 |   2  |       | ipq1      |---->|  ipfrag1  |----->| ipfrag2  |------>....... 
 --------        ------------     -------------    ------------
  ......            |
 --------           //
 |  63  |      ------------      -------------     -----------
 --------      |  ipq2    |---->|  ipfrag1  |----->| ipfrag2 |------>....... 
               ------------      -------------      -----------
                 |
                 //
            ------------          -------------     -----------
            |  ipq3    |---->    | ipfrag1    |---->| ipfrag2 |------>....... 
            ------------          -------------     -----------
                |
               //
            ........

每个IP包用如下四元组表示:(id,saddr,daddr,protocol),四个值都相同的碎片
保留在一个IPQ中,即可组装成一个完整的IP包。

3. 重要函数说明(2.2系列)

3.1 ip_defrag()
ip_defrag()是整个流程的入口,下面我们首先对ip_defrag()作一定的说明。

(1)为了防止因保留分片而造成内存消耗过大,linux设置了界限来防止这种情况,如果超过了
内存使用的上限,则清空内存中最老的队列(ipq).所用内存的大小保存在变量ip_frag_mem中,
当然,对它的读写都应是“原子”操作(atomic_sub,atomic_add,atomic_read,etc)。
其定义在文件ip_fragment.c前部:

atomic_t ip_frag_mem = ATOMIC_INIT(0);  /* Memory used for fragments */

if (atomic_read(&ip_frag_mem) > sysctl_ipfrag_high_thresh)
  ip_evictor();
  
ip_evicator的具体操作将在下文中描述。  

(2)以id, saddr, daddr, protocol为标志检索是否已经建立了相应的ipq,若发现,则返回
ipq的指针,并重置定时器。

qp = ip_find(iph, skb->dst);

(3)此时有一个if/else对,其作用是:
如果ipq已经存在,则证明已经有同一个包的其他分片到达。检查此片是不是第一个分片(因为
分片到达顺序可能错乱),若是,将ip头信息和头长度保留在ipq结构中();
if (offset == 0) {
/* Fragmented frame replaced by unfragmented copy? */
if ((flags & IP_MF) == 0)
 goto out_freequeue;
qp->ihlen = ihl;
memcpy(qp->iph, iph, (ihl + 8));
}

如果不存在,当然要建立一个了:
qp = ip_create(skb, iph);
if (!qp)
 goto out_freeskb;
 
ip_create便是分配出一块内存,初始化这个ipq,并在hash表中登记。

到此为止ipq已经肯定存在了,不管是已经存在的,还是我们刚才生成的。

(4)对包的长度进行检测,如果超过了ip包的最大范围,则报警,并丢弃此包。jolt2
便是利用这点将window系统打瘫的。由于linux做了这种检查,所以基本免受其害。

(5)调节end值(数据的结尾位置),如果是最后一个包,则最终整个ip包的长度便可以知
道了,为了组装时方便,将其记录到ipq中。
/* Determine the position of this fragment. */
end = offset + ntohs(iph->tot_len) - ihl;

/* Is this the final fragment? */
if ((flags & IP_MF) == 0)
  qp->len = end;
  
(6)接下来很长一段代码(line481-line586)便是定位这份分片在整个数据包中的位置。
如果分片之间有重合(恶意攻击和其他异常),则能归并便归并。这个问题我们将在后面
(常见碎片攻击中)详谈。

(7)此时我们已经知道这个分片的具体位置了。我们要生成一份新的ipfrag结构,并将其放到
我们刚才找到的正确位置上去。
tfp = ip_frag_create(offset, end, skb, ptr);
if (!tfp)
 goto out_freeskb;
 
/* Insert this fragment in the chain of fragments. */
tfp->prev = prev;
tfp->next = next;
if (prev != NULL)
 prev->next = tfp;
else
 qp->fragments = tfp;

if (next != NULL)
 next->prev = tfp;

(8)ip_done函数检查是否所有的分片已经到齐,如果到齐,则将其组装成一个新的sk_buff
(调用ip_glue),并最终返回到调用ip_defrag的地方。

if (ip_done(qp)) {       /*全部到齐了么?*/
 /* Glue together the fragments. */
  skb = ip_glue(qp);
 /* Free the queue entry. */
out_freequeue:
 ip_free(qp);    /*原有的ipq结构已经不需要了,释放。*/
out_skb:
 return skb;     /*组装完成,可以返回了*/
}

如果没有到齐,则返回NULL.

至此全部组装过程结束。

3.2 ip_evictor()
当分片所用的内存超过一定的上限时(sysctl_ipfrag_high_thresh)会调用ip_evicator以释放内存。
ip_evicator会找寻可清空的IPQ,并将其清空,直到到达到可用的下限(sysctl_ipfrag_low_thresh)
。

这个值在ip_fragment.c中按如下定义:
int sysctl_ipfrag_high_thresh = 256*1024;
int sysctl_ipfrag_low_thresh = 192*1024;

同样,用sysctl -a可可看到这两参数,同时可以动态修改。
#sysctl -a
......
net.ipv4.ipfrag_low_thresh = 196608
net.ipv4.ipfrag_high_thresh = 262144
......

理论上ip_evicator应该采用LRU算法,将最古老的IPQ清除。但目前linux(包括2.4.0)没有实现此功能
,只是将hash表按次序清空,这样的好处是简单易行。

3.3 ip_glue()
ip_glue()函数将负责将一个所有分片已经到齐的的IP包组合好。当这一步进行时,所有的分片已经
按顺序排好,并解决了所有的重叠问题。因此其流程相应很简单。
首先生成一个足够大的(足以容纳所有的分片包长度的总和)新的skbuff:
skb = dev_alloc_skb(len);
if (!skb)
 goto out_nomem;
调整一些必要的指针后,就在一个while循环中依次将原有分片的内容用memcoy拷贝到新的skbuff中。
再进行一些指针调整后,过程结束,将新的skbuff返回。

3.4 ip_expire()
前面已经提到,每个ipq保留了一个定时器,当一定时间以后组装还没有完成,将清空此队列。
定时器的值保留在sysctl_ipfrag_time中:
int sysctl_ipfrag_time = IP_FRAG_TIME;
(在/include/net/ip.h中有#define IP_FRAG_TIME (30 * HZ) )
此值也可以用sysctl设置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值