IPv6的TSO/GRO/GSO及其Linux實現的不妥

很明確的一件事是,IPv6不允許中間設備對報文分片。具體爲什麼這麼設計,就是爲了簡單高效。因此,IPv6報頭簡潔了不少。

但TSO貌似並未違背取消IPv6分片的初衷,硬件把一些都處理的妥妥的,在路由軟件層看來,一切好像沒有發生過一樣。

我先簡單解釋一下TSO和IP分片的區別:
在這裏插入圖片描述

我們來看一個簡單的實驗,用IPv6從服務端拉一個大文件,服務端和客戶端的抓包如下:
在這裏插入圖片描述
在客戶端看來,沒有IP分片,因此它不需要做分片重組的動作,它實實在在就是收到了一個完整的7140字節的報文,就好像這個報文就是從服務端真實發出的一樣,而實際上,服務器顯然沒有發送過這麼大的報文,顯然,這是客戶端聚合了幾個小包的結果。

因此,下面的結論是合理的:

  • 如果轉發設備覺得一個IPv6數據包大於出口的MTU,並且如果它的載荷是TCP段的話,啓用TSO將其分段而不是直接丟棄併發送ICMP too big,這並無不妥。

IPv6只是規定了中間設備不能分片,但是卻沒有規定必須保持原樣傳輸啊。

然而,Linux內核是這麼實現的嗎?嚴格來講,Linux對於上面的描述,僅僅實現了一半。

Linux內核做到了:

  • 即便是轉發設備,只要使能了LRO/GRO,就會進行收包聚合,將多個小包儘可能聚合成一個大包,並且修改對應的IPv6頭。
  • 即便是轉發設備,在轉發IPv6超過MTU大小的報文時,只要該報文元數據gso_size不大於MTU,就能成功轉發,依靠GSO/TSO在發送時對其進行分段發送,重新分割成獨立的小報文。

然而,這是不妥的。

gso_size可以認爲是聚合前小報文的大小,在我看來,即便它大於出口MTU,也是可以利用TSO/GSO機制將其分段的啊!只是Linux沒有實現而已。

具體看這段代碼:

int ip6_forward(struct sk_buff *skb)
{
   
   
	...
	if (ip6_pkt_too_big(skb, mtu)) {
   
   
		/* Again, force OUTPUT device used as source address */
		skb->dev = dst->dev;
		icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
		__IP6_INC_STATS(net, idev, IPSTATS_MIB_INTOOBIGERRORS);
		__IP6_INC_STATS(net, ip6_dst_idev(dst),
				IPSTATS_MIB_FRAGFAILS);
		kfree_skb(skb);
		return -EMSGSIZE;
	}
	...
}

當你看ip6_pkt_too_big的代碼時,裏面會找到這個不妥的邏輯。

這裏沒有必要擡槓說什麼GSO的實現之類就是如此,我的意思是,只要指定一個出口的MTU,無論什麼樣的TCP包,底層TSO/GSO都會按照該MTU的大小將其分成小段,每一個小段都是一個獨立完整的IPv6報文。這個和入口GRO設置的gso_size有個毛線關係啊!

但事實上,Linux內核對此的實現就是這麼殘!我用systemtap和bpftrace實地探測過的結論,後來我纔看的代碼。


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章