linux核心協議棧 IPv4分片重組Ⅱ之 ip_defrag() 介面

老王不讓用發表於2020-10-23

Table of Contents

1 IP片段接收入口 ip_local_deliver()

2 IP片段重組 ip_defrag()

2.1 查詢IP分片佇列 ip_find()

2.1.1 雜湊查詢分片佇列 inet_frag_find()

2.1.2 新建IP分片佇列 inet_frag_create()

2.2 重組IP報文 ip_frag_queue()

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()將分片重組邏輯分割的非常清晰:

  1. ip_find() 查詢全域性雜湊表,找到(或者新建)該IP分片所屬的 IP分片佇列struct ipq
  2. 新收到了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_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;
}

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_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);
}

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;
}

相關文章