全部博文(668)
分类:
2009-01-06 10:10:29
Linux下IP――分片与重组
原理介绍
为数据包分片和为数据包片再次分片之间的细微差别就在于网关处理MF比特的不同。但一个网关为原来为分片的数据包分片时,除了末尾的数据包片,它将其余所有分片上的MF比特都置为一,最后一片为0。然而,当网关为一个非末尾的数据包片再次分片时,它会把生成的所有子分片中的MF比特全部设置为1,因为所有这些子分片都不可能是整个数据包的末尾的数据包片。
对于分片,需要拷贝IP首部和选项,以及数据。而选项的拷贝要注意:根据协议标准,某些选项只应当出现在的一个数据包片中,而其他一些则必须出现在所有的数据包中。
为了使数据包的重组效率更高,用于保存数据包的数据结构必须能够做到:
重组程序代码使用了一个互斥信号量。Ipfrag_lock
查找方式:链表的线性查找
分片列表空间以全满的情况下:丢弃对应的数据包的所有分片。Ip_evictor
判断IP_MF位是否为0!
为了使丢失数据包片的数据包不再浪费存储资源 ,并防止因为标示符字段的重新使用而给IP带来混乱,但已经不可能再受到剩余数据包片时,IP必须定期检查数据包片列表。
Ipq_unlink
Ipq_put
Ipq_kill
Ipqhashfn
如何提高分片处理的效率
ip_sendà ip_fragment(skb, ip_finish_output);一般从转发来
ip_queue_xmit2à ip_fragment(skb, skb->dst->output)一般从TCP来
因为IP报太大而将其分片以适合于一个帧的传输。
获取外出设备(由skb决定)
dev = rt->u.dst.dev; 出口路由设备
!!!skb->dst=rt=rt->u.dstàdst_entry
取IP包头
raw = skb->nh.raw;
iph = (struct iphdr*)raw; 取IP头
设定开始值
hlen=IP头长
left = ntohs(iph->tot_len) - hlen; 包总长度减去IP头长度――需要分片的数据长度
mtu = rt->u.dst.pmtu - hlen; 物理MTU减去IP头长度――除去IP头的分片长度
ptr = raw + hlen; 取数据区指针
将数据包分片
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
取出偏移位(13位),并乘8算出总字节数――算该包的偏移字节数
not_last_frag = iph->frag_off & htons(IP_MF);
取出MF位(第14位)
循环进行分片:
while(left > 0) {
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
if (len > mtu) 如果剩下的数据left还比MTU大,则以MTU为分片的数据长度;否则,就用left作为数据长度(对于最后一片)
len = mtu;
/* IF: we are not sending upto and including the packet end
then align the next start on an eight byte boundary下一个开始出是八字节的边界对齐 */
if (len < left) { 当len
len &= ~7; 取8字节的整数倍
}
分配缓冲区新的分片包的sk_buff,大小:硬件帧长+分片长+IP头长
填充分片
/*
* Charge the memory for the fragment to any owner
* it might possess
*/
装填新的IP头
如果偏移为0――表明该包第一次被分片
if (offset == 0)
表明该IP包是第一次分片要在第一片中填入一些不允许在其他分片中出现的选项,为了提高效率(选项一般放在分片的第一个包中。
ip_options_fragment(skb);
对于多充分片(not_last_frag=1表示该包就是一个分片包)――对分片包再次分片时,需要保持MF为1
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF); 设置MF位1
移动原IP包的数据指针ptr
移动分片偏移指针offset
ptr += len; 移动IP包数据指针
offset += len; 移动分片指针
如果配置了防火墙,则进行防火墙值的设置。
发送该分片
}循环直到数据分片结束(left=0)
众所周知,网络数据报在linux的网络堆栈中是以sk_buff的结构传送的,ip_defrag()的功能就是接受分片的数据包(sk_buff),并试图进行组合,当完整的包组合好时,将新的sk_buff返还,否则返回一个空指针。
if (iph->frag_off & htons(IP_MF|IP_OFFSET))判断是否是分片
ip_local_deliveràip_defrag(skb);
这些分片形成一个双向链表(在linux内核中,若需要使用链表,除非有特殊需要,否则推荐双向链表,见document\CodingStyle),表示一个未组装完的分片队列(属于一个ip包)。这个链表的头指针要放在ipq结构中:
/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq {
struct ipq *next; /* linked list pointers */
u32 saddr;
u32 daddr;
u16 id;
u8 protocol;
u8 last_in;
#define COMPLETE 4
#define FIRST_IN 2
#define LAST_IN 1
struct sk_buff *fragments; /* linked list of received fragments */
int len; /* total length of original datagram */
int meat; 保留现有的分片长度的累加值
spinlock_t lock;
atomic_t refcnt;
struct timer_list timer; /* when will this queue expire? */
struct ipq **pdivv;
int iif; /* Device index - for icmp replies */
};
注意每个ipq保留了一个定时器(即struct timer_list timer;)。
Ipq利用一个HASH表来构建分片链。
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))
每个IP包用如下四元组表示:(id,saddr,daddr,protocol),四个值都相同的碎片散列到一个IPQ链中,即可组装成一个完整的IP包。
#define FRAG_CB(skb) ((struct ipfrag_skb_cb*)((skb)->cb))
cb是一块控制缓冲区。它提供给每一层存放私有的数据。如果你需要将它们保持到其他层,则必须进行克隆skb_clone。
char cb[48];
struct ipfrag_skb_cb
{
struct inet_skb_parm h;
int offset;
};
struct inet_skb_parm
{
struct ip_options opt; /* Compiled IP options */
unsigned char flags;
#define IPSKB_MASQUERADED 1
#define IPSKB_TRANSLATED 2
#define IPSKB_FORWARDED 4
};
1)ip_defrag分成了ip_defrag和ip_frag_queue两部分。
2)ip_glue换名成ip_frag_reasm,流程基本未动。
3)现在ipq中用meat保留现有的分片长度的累加值(已经解决重叠),如果此值到达总长度,则意味着所有的分片到达,因此取消了ip_done函数,不必每次遍历一次链表,因此在效率上有了较大的提高,抗小碎片攻击的能力得到加强。
ip_findàip_frag_createàip_frag-internà IP_FRAG_TIME
struct sk_buff *ip_defrag(struct sk_buff *skb)
{
如果用于分片处理的内存空间大于系统规定的最大值256k,那么要进行清洗
指定IP包对应设备dev
根据HASH值,定位该包在分片链中的位置:
将该分片插入到对应的分片队列中,
当分片所用的内存超过一定的上限时(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表按次序清空,这样的好处是简单易行。
Memory limiting on fragments. Evictor trashes(丢弃) the oldest * fragment queue until we are back under the low threshold.
Ip_evictor函数遍历分片队列,同时丢弃到目前为止已经收集到的分片,直到所使用的总的内存量小于规定的限制时为止。只要占用的内存大于对他的内存限制值,这个函数就调用Ip_free函数。当分片队列为空并且内存阀值也超过时,Ip_evictor函数可以引起内核的恐慌。
LRU算法,全局变量ipq_hash[64]链表,越靠近链尾越老引用计数越大越不容易被洗掉
两重循环每次洗掉时间最老引用计数最少的分片,直到总占用内存降到sysctl_ipfrag_low_thresh=192*1024
初始化一个IP分片队列,包括了定时器,处理函数为IP_expire。用ip_frag_intern建立链表头和插入时间链。
ip_frag_nqueues++