1. 程式人生 > >linux核心學習筆記------ip報文組裝

linux核心學習筆記------ip報文組裝

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; i<skb_shinfo(head)->nr_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);
		}
	}
......
}
自此ip報文的組裝就完成了,主要是ip_frag_queue這個函式