ip报文有分片就会有组装。在接收方,只有报文的所有分片被重新组合后才会提交到上层协议。内核组装ip报文用到了ipq结构体:(注,这系列源码中的注释都来自:http://blog.csdn.net/justlinux2010)
struct ipq {
struct inet_frag_queue q;
/*
* 标识分片来源,取值为IP_DEFRAG_LOCAL_DELIVER等。
*/
u32 user;
/*
* 下面四个字段的值都来源于IP首部,
* 用来唯一确定分片来自哪个IP数据包。
*/
__be32 saddr;
__be32 daddr;
__be16 id;
u8 protocol;
/*
* 接收最后一个分片的网络设备索引号。
* 当分片组装失败时,用该设备发送分片
* 组装超时ICMP出错报文,即类型为ICMP_TIME_EXCEEDED,
* 代码为ICMP_EXC_FRAGTIME。参见ip_expire()。
*/
int iif;
/*
* 已接收到分片的计数器。可通过对端信息块
* peer中的分片计数器和该分片计数器来防止
* DoS攻击。
*/
unsigned int rid;
/*
* 记录发送方的一些信息。
*/
struct inet_peer *peer;
};
ip报文分片调用的ip_fragment,而组装调用的是ip_defrag:
int ip_defrag(struct sk_buff *skb, u32 user)
{
......
/*
* 如果ipq散列表消耗的内存大于指定值时,
* 需调用ip_evictor()清理分片。
*/
if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)
ip_evictor(net);
if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {
int ret;
spin_lock(&qp->q.lock);
/*
* 将分片插入到ipq分片链表的适当位置。
*/
ret = ip_frag_queue(qp, skb);
spin_unlock(&qp->q.lock);
ipq_put(qp);
return ret;
}
......
}
ip报文的重装会有一个垃圾回收机制,这种机制是为了控制ip组装所占用的内存。当netfs_frags所占用的内存大于netfs_frags的high_thresh就会调用ip_evictor清理内存。这个函数稍后介绍。
接着会调用ip_find从iqp散列表中查找,如果找到相应的ipq,就会调用ip_frag_queue把skb插入到ipq的队列中。先来看下ip_find:
static inline struct ipq *ip_find(struct net *net, struct iphdr *iph, u32 user)
{
......
hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);
q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
return container_of(q, struct ipq, q);
......
}
ipq是通过哈希表组织起来的。首先会根据源和目的ip地址以及协议计算hash,然后从哈希表中查找到inet_frag_queue,如果查找到就会返回,否则就会调用inet_frag_create创建。查找和创建inet_frag_create都有了,现在来看下怎么把分片skb插入到pq指定的ipq分片链表中的。也就是ip_frag_queue:
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
......
/*
* 对last_in标志是COMPLETE的ipq,即分片已全部接收到的
* ipq,则释放该分片后返回。
*/
if (qp->q.last_in & INET_FRAG_COMPLETE)
goto err;
/*
* 若不是有本地生成的分片,则调用ip_frag_too_far()
* 检测该分片是否存在DoS攻击嫌疑。如果受到DoS
* 攻击,则调用ip_frag_reinit()释放ipq所有分片。
*/
if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
unlikely(ip_frag_too_far(qp)) &&
unlikely(err = ip_frag_reinit(qp))) {
ipq_kill(qp);
goto err;
}
/*
* 分别取出IP首部中的标志位、片偏移以及首部长度
* 字段,并计算片偏移值和首部长度值。IP首部中的
* 片偏移字段为13位,表示的是8字节的倍数,而首部
* 长度字段是首部占32位字的数目。
*/
offset = ntohs(ip_hdr(skb)->frag_off);
flags = offset & ~IP_OFFSET;
offset &= IP_OFFSET;
offset <<= 3; /* offset is in 8-byte chunks */
ihl = ip_hdrlen(skb);
/*
* 计算分片末尾处在原始数据包中的位置。
*/
end = offset + skb->len - ihl;
/*
* 如果是最后一个分片,则先对该分片进行检验;
* 如果其末尾小于原始数据包长度,或其ipq已有
* LAST_IN标志并且分片末尾不等于原始数据包长度,
* 则表示出错。通过检验后,对ipq设置LAST_IN标志,
* 并将完整数据包长度存储在ipq的len字段中。
*/
if ((flags & IP_MF) == 0) {
/* If we already have some bits beyond end
* or have different end, the segment is corrrupted.
*/
if (end < qp->q.len ||
((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
goto err;
qp->q.last_in |= INET_FRAG_LAST_IN;
qp->q.len = end;
} else {
/*
* 如果不是最后一个分片,其数据长度又不8字节对齐,
* 则将其截为8字节对齐。如果需要计算校验和,则强制
* 设置由软件来计算校验和。这是因为截断了IP有效负载,
* 改变了长度,需重新计算校验和。
*/
if (end&7) {
end &= ~7;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
/*
* 在最后一个分节没有到达的情况下,如果当前分片的
* 末尾在整个数据包中的位置大于ipq中len字段的值,则
* 更新len字段;若此数据包有异常,则直接丢弃。因为
* ipq结构的len字段始终保持所有已接收到的分片中分片
* 末尾在数据包中位置的最大值,而只有在接收到最后
* 一个分节后,len值才是整个数据包的长度。
*/
if (end > qp->q.len) {
/* Some bits beyond end -> corruption. */
if (qp->q.last_in & INET_FRAG_LAST_IN)
goto err;
qp->q.len = end;
}
}
/*
* 如果分片的数据区长度为零,则该
* 分片有异常,直接丢弃。
*/
if (end == offset)
goto err;
......
/*
* 确定分片在分片链表中的位置。因为各分片
* 很可能不按顺序到达目的端,而ipq分片链表
* 上的分片是按分片偏移值从小到大的顺序
* 链接在一起。
*/
prev = NULL;
for (next = qp->q.fragments; next != NULL; next = next->next) {
if (FRAG_CB(next)->offset >= offset)
break; /* bingo! */
prev = next;
}
/*
* 检测和上一个分片的数据是否有重叠,i是重叠
* 部分数据长度,如果有重叠则调用pskb_pull去掉
* 重叠部分。
*/
if (prev) {
int i = (FRAG_CB(prev)->offset + prev->len) - offset;
if (i > 0) {
offset += i;
err = -EINVAL;
if (end <= offset)
goto err;
err = -ENOMEM;
if (!pskb_pull(skb, i))
goto err;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
}
/*
* 如果和后一个分片的数据有重叠,则还需要
* 判断重叠部分的数据长度是否超过下一个分
* 片的数据长度,没有超过则调整下一个分片,
* 超过则需要释放下一个分片后再检查与后面
* 第二个分片的数据是否有重叠,如此反复,
* 直到完成后面对所有分片的检测。调整分片
* 的片偏移值、已接收分片总长度等。
*/
while (next && FRAG_CB(next)->offset < end) {
int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes */
if (i < next->len) {
/* Eat head of the next overlapped fragment
* and leave the loop. The next ones cannot overlap.
*/
if (!pskb_pull(next, i))
goto err;
FRAG_CB(next)->offset += i;
qp->q.meat -= i;
if (next->ip_summed != CHECKSUM_UNNECESSARY)
next->ip_summed = CHECKSUM_NONE;
break;
} else {
struct sk_buff *free_it = next;
/* Old fragment is completely overridden with
* new one drop it.
*/
next = next->next;
if (prev)
prev->next = next;
else
qp->q.fragments = next;
qp->q.meat -= free_it->len;
frag_kfree_skb(qp->q.net, free_it, NULL);
}
}
......
/*
* 更新ipq的时间戳
*/
qp->q.stamp = skb->tstamp;
/*
* 累计该ipq已收到分片的总长度。
*/
qp->q.meat += skb->len;
/*
* 累计分片组装模块所占的内存。
*/
atomic_add(skb->truesize, &qp->q.net->mem);
/*
* 如果片偏移值为0,则说明当前分片时第一个
* 分片,设置FIRST_IN标志。
*/
if (offset == 0)
qp->q.last_in |= INET_FRAG_FIRST_IN;
if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
qp->q.meat == qp->q.len)
/* 所有的分片都已接收*/
return ip_frag_reasm(qp, prev, dev);
......
}
这个函数比较长,但是大部分都有注释,所以阅读起来也不是很难,主要注意在重装ip报文的时候,首先要注意inet_frag_queue的last_in会标识分片是否分片全部接收到,并且会判断当ip报文分片全部接收到后但是长度无效的处理,后续会把分片插入到链表的正确位置也就是根据分片在ip报文的片偏移(offset)来插入。最终要两点就是:1、插入到链表后,会判断当前分片是否覆盖了前一个分片一部分数据,如果覆盖了就会删除当前分片的覆盖部分。2、如果和后一个分片的数据有重叠,则还需要判断重叠部分的数据长度是否超过下一个分片的数据长度,没有超过则调整下一个分片,超过则需要释放下一个分片后再检查与后面第二个分片的数据是否有重叠,如此反复,直到完成后面对所有分片的检测。调整分片的片偏移值、已接收分片总长度等。这里有个疑问就是为什么判断当前分片与前一个分片没有像第二种情况那样那么细致呢?
更新ipq相关属性后会判断分片是否接收完毕,如果接收完毕就会调用ip_frag_reasm对ip报文进行组装
/*
* 此函数用于组装已到齐的所有分片,当原始
* 数据包的所有分片都已到齐时,会调用此函
* 数组装分片。
*/
static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev,
struct net_device *dev)
{
......
struct sk_buff *fp, *head = qp->q.fragments;
......
/*
* 要开始组装了,因此调用ipq_kill()将此ipq结点从
* ipq散列表和ipq_lru_list链表中断开,并删除定时器。
*/
ipq_kill(qp);
if (prev) {
head = prev->next;
fp = skb_clone(head, GFP_ATOMIC);
if (!fp)
goto out_nomem;
fp->next = head->next;
prev->next = fp;
skb_morph(head, qp->q.fragments);
head->next = qp->q.fragments->next;
kfree_skb(qp->q.fragments);
qp->q.fragments = head;
}
/*
* IP数据包总长过了限值则丢弃。
*/
if (len > 65535)
goto out_oversize;
/*
* 在组装分片时,所有的分片都会组装到第一个分片
* 上,因此第一个分片是不能克隆的,如果是克隆的,
* 则需为分片组装重新分配一个SKB。
*/
if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC))
goto out_nomem;
......
/*
* 分片队列的第一个SKB不能既带有数据,又带有分片,即其
* frag_list上不能有分片skb,如果有则重新分配一个SKB。最终的
* 效果是,head自身不包括数据,其frag_list上链接着所有分片的
* SKB。这也是SKB的一种表现形式,不一定是一个连续的数据块,
* 但最终会调用skb_linearize()将这些数据都复制到一个连续的数据
* 块中。
*/
if (skb_has_frags(head)) {
......
if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL)
goto out_nomem;
skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;
skb_frag_list_init(head);
for (i=0; inr_frags; i++)
plen += skb_shinfo(head)->frags[i].size;
......
/*
* 把所有分片组装起来即将分片链接到第一个
* SKB的frag_list上,同时还需要遍历所有分片,
* 重新计算IP数据包长度以及校验和等。
*/
skb_shinfo(head)->frag_list = head->next;
skb_push(head, head->data - skb_network_header(head));
atomic_sub(head->truesize, &qp->q.net->mem);
for (fp=head->next; fp; fp = fp->next) {
head->data_len += fp->len;
head->len += fp->len;
if (head->ip_summed != fp->ip_summed)
head->ip_summed = CHECKSUM_NONE;
else if (head->ip_summed == CHECKSUM_COMPLETE)
head->csum = csum_add(head->csum, fp->csum);
head->truesize += fp->truesize;
atomic_sub(fp->truesize, &qp->q.net->mem);
}
/*
* 重置首部长度、片偏移、标志位和总长度。
*/
iph = ip_hdr(head);
iph->frag_off = 0;
iph->tot_len = htons(len);
IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS);
/*
* 既然各分片都已处理完,释放ipq的分片队列。
*/
qp->q.fragments = NULL;
......
}
这个函数大部分也有注释,也就不多说了。下面来看下ip分片组装的垃圾回收机制,其实也就是lru队列,释放内存到少于sysctl_ipfrag_low_thresh。
int inet_frag_evictor(struct netns_frags *nf, struct inet_frags *f)
{
/*
* 在清理之前再次对当前消耗的内存做测量,
* 如果少于sysctl_ipfrag_low_thresh,则不进行清理。
*/
work = atomic_read(&nf->mem) - nf->low_thresh;
while (work > 0) {
......
/*
* 如果ipq_lru_list链表为空,则解锁后返回。
*/
if (list_empty(&nf->lru_list)) {
......
/*
* 递增ipq的引用计数。
*/
q = list_first_entry(&nf->lru_list,
struct inet_frag_queue, lru_list);
......
/*
* 如果分片还没到齐,则ipq上从ipq散列表及
* ipq_lru_list链表中删除。inet_frag_kill()只删除不释放
*/
if (!(q->last_in & INET_FRAG_COMPLETE))
inet_frag_kill(q, f);
/*
* 减少引用计数,如果为0,删除ipq及其所有分片。
*/
if (atomic_dec_and_test(&q->refcnt))
inet_frag_destroy(q, f, &work);
}
ip分片组装除了一个垃圾回收机制,还有一个定时器监视ip分片组装超时,这样可以让一个ip报文有的分片在不可能全部到达目的地址的时候占用大量的资源。此外还可以抵御DoS攻击。组装超时定时器例程为ip_expire:
static void ip_expire(unsigned long arg)
{
struct ipq *qp;
struct net *net;
qp = container_of((struct inet_frag_queue *) arg, struct ipq, q);
net = container_of(qp->q.net, struct net, ipv4.frags);
......
/*
* 若ipq当前已是COMPLETE状态,则不作
* 处理,直接跳转到释放ipq及其
* 所有的分片处。
*/
if (qp->q.last_in & INET_FRAG_COMPLETE)
goto out;
/*
* 将ipq从ipq散列表和ipq_lru_list链表中
* 删除。
*/
ipq_kill(qp);
......
/*
* 如果第一个分片已经到达,则发送分片组装
* 超时ICMP出错报文。
*/
if ((qp->q.last_in & INET_FRAG_FIRST_IN) && qp->q.fragments != NULL) {
struct sk_buff *head = qp->q.fragments;
/* Send an ICMP "Fragment Reassembly Timeout" message. */
if ((head->dev = dev_get_by_index(net, qp->iif)) != NULL) {
icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
dev_put(head->dev);
}
}
......
}