Table of Contents
2.1.1 哈希查找分片队列 inet_frag_find()
2.1.2 新建IP分片队列 inet_frag_create()
2.2.1 所有分片均已收到重组ip报文 ip_frag_reasm()
1 IP片段接收入口 ip_local_deliver()
在接收路径上的 ip_local_deliver() 函数中,此时已经确认数据包是给本机的,会首先调用 ip_defrag() 判断是否是一个完整的IP报文,即是否需要进行重组,如果一切 ok,那么继续过防火墙的 LOCAL_IN 点,继续数据包的接收流程。
int ip_local_deliver(struct sk_buff *skb)
{
// 第一个if条件成立,说明收到的skb是一个IP片段;
// ip_defrag()返回非0表示此数据包不完整,需要等更多的数据包才能完成重组,所以现在还不能递交,接收流程结束
if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
}
显然,IP报文重组的核心在ip_defrag()中。
2 IP片段重组 ip_defrag()
ip_defrag()将分片重组逻辑分割的非常清晰:
- ip_find() 查找全局哈希表,找到(或者新建)该IP分片所属的 IP分片队列struct ipq;
- 新收到了IP片段,所以调用 ip_frag_queue() 尝试对该IP分片队列中的片段进行重组。
/* Process an incoming IP datagram fragment. */
int ip_defrag(struct sk_buff *skb, u32 user)
{
struct ipq *qp;
struct net *net;
IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);
net = skb->dev ? skb->dev->nd_net : skb->dst->dev->nd_net;
// 后面很可能需要分配内存来保存新来的分片,所以先检查分片占用内存是否已经超过了最大上限,
// 如果超过了则调用ip_evictor()进行内存清理
if (atomic_read(&net->ipv4.frags.mem) > net->ipv4.frags.high_thresh)
ip_evictor(net);
// 查询该IP片段所属IP分片队列(每一个IP数据报有一个该队列),如果没有则新建一个
if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {
int ret;
spin_lock(&qp->q.lock);
// 尝试进行分片重组,如果能够重组出一个完整的IP报文,则返回0,这样数据包就会传递给L4协议
ret = ip_frag_queue(qp, skb);
spin_unlock(&qp->q.lock);
ipq_put(qp);
return ret;
}
// 新建IP分片队列失败,则丢包
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
kfree_skb(skb);
return -ENOMEM;
}
2.1 查找IP分片队列 ip_find()
// ip_find()内部的一个临时传参用的结构
struct ip4_create_arg {
struct iphdr *iph;
u32 user;
};
/* Find the correct entry in the "incomplete datagrams" queue for
* this IP datagram, and create new one, if nothing is found.
*/
static inline struct ipq *ip_find(struct net *net, struct iphdr *iph, u32 user)
{
struct inet_frag_queue *q;
struct ip4_create_arg arg;
unsigned int hash;
arg.iph = iph;
arg.user = user;
// 根据ipid、源IP、目的IP、L4协议号以及初始化时生成的一个随机数共5个信息计算hash值
hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);
// 查找哈希表,检查是否有该片段所属报文对应的inet_frag_queue队列,如果没有那么函数会新建
q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
if (q == NULL)
goto out_nomem;
return container_of(q, struct ipq, q);
out_nomem:
LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left !\n");
return NULL;
}
2.1.1 哈希查找分片队列 inet_frag_find()
struct inet_frag_queue *inet_frag_find(struct netns_frags *nf, struct inet_frags *f, void *key, unsigned int hash)
{
struct inet_frag_queue *q;
struct hlist_node *n;
// 遍历冲突链,尝试寻找匹配的,如果找到,增加引用计数并返回
read_lock(&f->lock);
hlist_for_each_entry(q, n, &f->hash[hash], list) {
if (q->net == nf && f->match(q, key)) {
// 找到了对应的IP分片队列,说明该分片不是第一个分片
atomic_inc(&q->refcnt);
read_unlock(&f->lock);
return q;
}
}
read_unlock(&f->lock);
// 没找到则新建一个,注意此时并未持有哈希表的读写锁
return inet_frag_create(nf, f, key, hash);
}
2.1.2 新建IP分片队列 inet_frag_create()
inet_frag_create()流程新建一个IP分片队列,并且将该新的IP分片队列插入哈希表、LRU表中。
static struct inet_frag_queue *inet_frag_create(struct netns_frags *nf, struct inet_frags *f,
void *arg, unsigned int hash)
{
struct inet_frag_queue *q;
// 分配IP分片队列并对其进行初始化
q = inet_frag_alloc(nf, f, arg);
if (q == NULL)
return NULL;
// 将新建的IP分片队列放入全局的IP分片重组哈希表中
return inet_frag_intern(nf, q, f, hash, arg);
}
static struct inet_frag_queue *inet_frag_alloc(struct netns_frags *nf, struct inet_frags *f, void *arg)
{
struct inet_frag_queue *q;
// 按照指定大小进行分配,对于IPv4,就是sizeof(struct ipq)
q = kzalloc(f->qsize, GFP_ATOMIC);
if (q == NULL)
return NULL;
// 调用回调进行初始化,对于IPv4,这里是ip4_frag_init()
f->constructor(q, arg);
// 内存消耗记账
atomic_add(f->qsize, &nf->mem);
// 建立定时器,但是并不启动,启动是在inet_frag_intern()中完成的
setup_timer(&q->timer, f->frag_expire, (unsigned long)q);
spin_lock_init(&q->lock);
atomic_set(&q->refcnt, 1);
q->net = nf;
return q;
}
static struct inet_frag_queue *inet_frag_intern(struct netns_frags *nf,
struct inet_frag_queue *qp_in, struct inet_frags *f, unsigned int hash, void *arg)
{
struct inet_frag_queue *qp;
#ifdef CONFIG_SMP
struct hlist_node *n;
#endif
write_lock(&f->lock);
#ifdef CONFIG_SMP
/* With SMP race we have to recheck hash table, because
* such entry could be created on other cpu, while we
* promoted read lock to write lock.
*/
hlist_for_each_entry(qp, n, &f->hash[hash], list) {
if (qp->net == nf && f->match(qp, arg)) {
atomic_inc(&qp->refcnt);
write_unlock(&f->lock);
qp_in->last_in |= COMPLETE;
inet_frag_put(qp_in, f);
return qp;
}
}
#endif
qp = qp_in;
// 启动该IP分片队列的定时器,超时时间来自系统参数(见网络命名空间中的frags)
if (!mod_timer(&qp->timer, jiffies + nf->timeout))
atomic_inc(&qp->refcnt);
// 将新的IP分片队列加入哈希表中,并累加计数器
atomic_inc(&qp->refcnt);
hlist_add_head(&qp->list, &f->hash[hash]);
// 将qp加入LRU链表
list_add_tail(&qp->lru_list, &nf->lru_list);
nf->nqueues++;
write_unlock(&f->lock);
return qp;
}
2.2 重组IP报文 ip_frag_queue()
在上面ip_find()的处理过程中,仅仅是拿到了该IP分片对应的IP分片队列,并没有将新收到的IP分片添加到队列中,是否添加以及它们是否能够重组成一个IP报文是由ip_frag_queue()实现的。
/* Add new segment to existing queue. */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
struct sk_buff *prev, *next;
struct net_device *dev;
int flags, offset;
int ihl, end;
int err = -ENOENT;
// 如果该IP报文已经重组完毕,但是又收到了属于它的片段,那么新收到的IP分片一定是个重复分片,丢弃
if (qp->q.last_in & COMPLETE)
goto err;
if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
unlikely(ip_frag_too_far(qp)) && unlikely(err = ip_frag_reinit(qp)))
{
ipq_kill(qp);
goto err;
}
// offset记录偏移量,字节为单位,flags记录MF和DF标记
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记录的是该IP分片最后一个字节在整个IP报文中的偏移量
end = offset + skb->len - ihl;
err = -EINVAL;
if ((flags & IP_MF) == 0) {
// MF标记为0,说明该IP片段是IP报文的最后一个分片
// q.len记录的是当前收到的该IP报文的最大偏移量,所以下面两种情况分别是:
// cond1成立,表示新收到这个片段虽然说自己是该IP报文的最后一个片段,
// 但是其最后一个字节之前已经收过了,这显然是一种传输错误;
// cond2成立,之前已经收到了最后一个IP分片,现在又收到了一个最后分片(可能是重传),
// 但是其最后一个字节的偏移量不正确,这显然是一种错误
if (end < qp->q.len || ((qp->q.last_in & LAST_IN) && end != qp->q.len))
goto err;
// 标记最后一个片段接收成功,更新q.len
qp->q.last_in |= LAST_IN;
qp->q.len = end;
} else {
// IP分片不是IP报文的最后一个分片,其end必须是8字节对齐的,这是由offset首部格式决定的
if (end&7) {
end &= ~7;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
if (end > qp->q.len) {
// 不是最后一个分片,但是其最后一个字节的偏移超过了报文总长度,显然是错误
if (qp->q.last_in & LAST_IN)
goto err;
// 偏移量ok,更新q.len
qp->q.len = end;
}
}
// 上面调整过end的值,或者该IP分片没有携带数据,那么是一种错误
if (end == offset)
goto err;
err = -ENOMEM;
// 调整skb,删除IP首部,只保留数据部分
if (pskb_pull(skb, ihl) == NULL)
goto err;
// 校验和调整
err = pskb_trim_rcsum(skb, end - offset);
if (err)
goto err;
/* Find out which fragments are in front and at the back of us
* in the chain of fragments so far. We must know where to put
* this fragment, right?
*/
prev = NULL;
// 根据offset,找到该分片的插入点,最后将插入到prev的后面
for (next = qp->q.fragments; next != NULL; next = next->next) {
if (FRAG_CB(next)->offset >= offset)
break; /* bingo! */
prev = next;
}
// 检查新接收的skb和prev是否有重叠
if (prev) {
int i = (FRAG_CB(prev)->offset + prev->len) - offset;
// 因为prev->offset肯定是小于offset的,如果prev报文的最后一个字节的
// 偏移量超过了offset(i > 0),说明二者一定有重叠,需要调整
if (i > 0) {
// 调整策略就是将新收到的skb前面的一部分删除
offset += i;
err = -EINVAL;
// 删除后,新的报文已经没有数据了,这种情况是新收到的skb之前已经完全收过了
if (end <= offset)
goto err;
err = -ENOMEM;
// 将新收到的skb的前i字节数据删除
if (!pskb_pull(skb, i))
goto err;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
}
err = -ENOMEM;
// 检查新接收的skb和next是否有重叠
while (next && FRAG_CB(next)->offset < end) {
int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes */
if (i < next->len) {
// i < next->len说明next末尾还有一部分数据是不重叠的,更新next
/* 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 {
// next中所有字节都是重复的,删除它
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);
}
}
FRAG_CB(skb)->offset = offset;
// 将新收到的skb插入IP分片队列中
skb->next = next;
if (prev)
prev->next = skb;
else
qp->q.fragments = skb;
// 记录输入设备索引
dev = skb->dev;
if (dev) {
qp->iif = dev->ifindex;
skb->dev = NULL;
}
// 更新时间戳和meat
qp->q.stamp = skb->tstamp;
qp->q.meat += skb->len;
// 内存记账
atomic_add(skb->truesize, &qp->q.net->mem);
// 偏移量为0,说明是第一个IP片段,设置FIRST_IN标记
if (offset == 0)
qp->q.last_in |= FIRST_IN;
// 所有片段都已经收到,重组IP报文
if (qp->q.last_in == (FIRST_IN | LAST_IN) && qp->q.meat == qp->q.len)
return ip_frag_reasm(qp, prev, dev);
// 重组条件不满足,更新LRU链表
write_lock(&ip4_frags.lock);
list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);
write_unlock(&ip4_frags.lock);
return -EINPROGRESS;
err:
kfree_skb(skb);
return err;
}
2.2.1 所有分片均已收到重组ip报文 ip_frag_reasm()
当ip_frag_queue()发现一个IP报文的所有片段都已经收到后,就调用ip_frag_reasm()进行重组IP报文。
/* Build a new IP datagram from all its fragments. */
static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev, struct net_device *dev)
{
struct iphdr *iph;
struct sk_buff *fp, *head = qp->q.fragments;
int len;
int ihlen;
int err;
// 将IP分片队列从哈希表中摘下来,递减IP分片队列引用计数,以及停止相关定时器
ipq_kill(qp);
/* Make the one we just received the head. */
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;
}
BUG_TRAP(head != NULL);
BUG_TRAP(FRAG_CB(head)->offset == 0);
// 计算整个IP报文的总长度
ihlen = ip_hdrlen(head);
len = ihlen + qp->q.len;
// 整个IP报文长度不能超过65535
err = -E2BIG;
if (len > 65535)
goto out_oversize;
/* Head of list must not be cloned. */
// 组装时,所有的IP片段会被拷贝到第一个IP片段上,第一个IP片段不能是克隆的
if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC))
goto out_nomem;
/* If the first fragment is fragmented itself, we split
* it to two chunks: the first with data and paged part
* and the second, holding only fragments. */
// 第一个IP片段不能有分片,如果有,把它拆成两部分,第一部分没有片段,片段部分拷贝到新建的skb中
if (skb_shinfo(head)->frag_list) {
struct sk_buff *clone;
int i, plen = 0;
if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL)
goto out_nomem;
clone->next = head->next;
head->next = clone;
skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;
skb_shinfo(head)->frag_list = NULL;
for (i=0; i<skb_shinfo(head)->nr_frags; i++)
plen += skb_shinfo(head)->frags[i].size;
clone->len = clone->data_len = head->data_len - plen;
head->data_len -= clone->len;
head->len -= clone->len;
clone->csum = 0;
clone->ip_summed = head->ip_summed;
atomic_add(clone->truesize, &qp->q.net->mem);
}
// 将所有的IP分片链接到第一个IP片段的frag_list中,并且重新计算校验和以及长度信息
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);
}
head->next = NULL;
head->dev = dev;
head->tstamp = qp->q.stamp;
// 重新设定IP首部的一些字段
iph = ip_hdr(head);
iph->frag_off = 0;
iph->tot_len = htons(len);
IP_INC_STATS_BH(IPSTATS_MIB_REASMOKS);
qp->q.fragments = NULL;
return 0;
out_nomem:
LIMIT_NETDEBUG(KERN_ERR "IP: queue_glue: no memory for gluing queue %p\n", qp);
err = -ENOMEM;
goto out_fail;
out_oversize:
if (net_ratelimit())
printk(KERN_INFO "Oversized IP packet from %d.%d.%d.%d.\n", NIPQUAD(qp->saddr));
out_fail:
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
return err;
}