从去年9月初以来,我把做实验写博客写代码的时间放在了晚上,但是现在,晚上要早睡觉,白天要被指使着干这干那,感觉还是周六的凌晨更是自己的时间。
本文最后,夹杂着一则关于 “皮鞋为什么比布鞋落后?” 以及 “犹太人为什么成功?” 两个问题的形而上回答!
近日帮一位朋友解决了一个IPv6的分片问题,索性就再来一篇闲谈。
这次就谈谈IP分片。
事情
这位朋友自己写了一个内核模块,大致就是在PREROUTING这个HOOK点将数据包复制,直接调用ip6_forward函数做monitor镜像。但是并不是所有包都能被镜像,总是有丢包,流量并不大,队列也没满,三台机器直连…
问题出在由于数据报文大于1500这个镜像链路的MTU,而被丢弃了。
纳尼?数据报文怎么可能大于1500呢,如果大于1500,IPv6不是一开始就分片了吗?是的。
纳尼?如果一开始就分片,不是到达最终目的地才会被重组吗?是的。
那么为什么数据包在不该重组的中间节点被重组了呢?
Linux的实现中,如果加载了Netfilter并且配置了处理4层/7层信息的规则,那么为了获取完整的4层/7层信息,就要先将分片进行重组,这个和IPv4是一样的。
IPv6的defrag是在PREROUTING的ipv6_defrag这个HOOK函数里被处理的。
问题出在,这位朋友在复制skb的时候,只复制了重组后的skb本身,并没有复制其之前重组前的frag list这些元数据。导致转发逻辑以为这个数据包一开始就是这么大而被丢弃。
实际上,转发逻辑如果发现这是一个 为了某种需要而被临时重组 的数据包之后,它会将其按照原样再分片发送的,然而这位朋友忽略了这个细节。改动比较简单。
在查这个问题的过程中,我搜到了另一个类似的问题:
https://access.redhat.com/solutions/2961581
可以一起review一下。
解决了这个问题,那么我们就来好好讨论一下关于分片的枝枝蔓蔓吧。
IPv4的分片以及问题
熟悉IPv4的肯定知道IP分片这个特性,它在某种意义上让应用程序忘记了数据包还有大小这个属性,也就是说,应用程序可以发送小于IP头规定的最长65535字节的任意大小的数据包。
IPv4严格采纳分层模型,让路径MTU这种事做到对应用程序完全透明而无感知。如果路径MTU太小不足以让大数据报文通过,那么 分片 这种机制便开始起作用。
IP分片是自动进行的,假设有下面的IP报文序列要发送:
P1-P2-P3
而 P2 的大小超过了MTU,假设为2个MTU的大小,那么分片机制会将P2分片拆为2个新的IP报文 sP2/1 和 sP2/2 ,那么,网络上将会有下面新的IP报文序列:
P1-sP2/1-sP2/2-P3
然而,由于IP协议不保证顺序传输,数据报文在网络中可能会乱序,且数据报文可能本身就有长度语义,因此,sP2/1和sP2/2在终点站或者中间任何设备上一定要能重组成原始的P2,否则可能会使得接收者接收到错误的报文而无法处理。所以重组是必须的。需要重组的地方有两个:
- 中间设备需要处理IP层以上层次语义的地方,比如要做NAT的地方;
- 数据报文的终点站,这里要保持数据报文应用层的报文语义。
为了能让重组逻辑顺利进行,需要用一个唯一标识符来标识每一个未分片的IP数据报文(即IP头里的 identifier字段 ,它在协议层面是本机唯一的,比如对于一台主机,TCP协议会单调递增一个identifier序列,而UDP单调递增另一个identifier序列,两个序列可以上相同的序列),分片后的报文继承母报文的该唯一标识符,这样就可以在需要进行分片重组的地方,将携带相同唯一IP报文标识符的报文重组成一个IP报文即可。
这一切信息在IPv4报文头中都能找得到。看看上面的那些过程,想想每一个路由器,中间节点,都可能要check并可能处理这么多的复杂逻辑,这并不适合数据报文的高速转发!
不光是效率问题,IPv4的分片还可能会带来安全问题。
我们知道,所有需要处理IP层以上逻辑的中间设备(比如状态防火墙,无状态防火墙,NAT网关等)或者端设备,都要去进行可能的分片重组。如果攻击者持续发送缺失片段的分片分组,就会导致这些设备重组不成功,而重组不成功的分片在一段时间内会被积累的内存中等待缺失的分片到来而成功重组,这就使得DDoS非常容易发生…
不多说,这方面的资料已经汗牛充栋了。
对于现代互联网,我们需要一个高速且安全的协议,越简单越高速,越直接越安全,IPv6满足需要!
IPv6的分片和实现
网络只管转发,分片这种端到端功能自然需要卸载到通信双方终端主机!
IPv6禁止中间节点设备对IP报文进行分片。分片只能在端到端进行!
我们先来看一下,IPv6禁止了中间设备分片,卸载了多少处理流程。很简单,看协议头即可,IPv4中下面的字段在IPv6中不再需要:
- 16位标识符:相同报文的分片拥有相同的16位标识符
- 3位分片标识:指示是否分片以及分片是否结束
别小看这些信息处理的卸载,这可只是IPv6的N分之一个特性造成的处理卸载!最终目的是让IPv6报头成为固定的长度,且内部字段对齐,便于高效预取或者直接通过固定硬件处理,从而达到提高处理性能的目的。
回到分片的是非本身,既然在路由器等转发设备上去掉分片机制这么好,那么为什么在端主机还允许分片,直接全部禁止了不更好吗?
我们知道,应用层对于数据报文的解释,它代表了一个数据报呢,还是说代表一个流。如果是代表一个流,那么一切OK,只要持续发送数据流字节即可,网络情况好了就一次多发几个字节,网络情况不好了就少发几个甚至发1个字节,都无所谓。但是对于用户数据报,比如UDP报文这种,就不行了。
UDP报文是严格按照报文长度发送和接收的,应用程序之间定义了一个2000字节的应用层协议,那么一个报文就必须是2000字节长,不能说你IPv6为了转发效率而不让人家发长报文吧。
因此,IPv6不能完全放弃分片机制,只是说它用一种完全不同的机制来实现分片:
- 分片和重组只能在端主机进行。
- 分片信息不在IPv6协议标准头里,而单独设计一个扩展头存放。
如此做,IPv6相当于:
- 报文始发站完全可以通过PMTU知晓链路最小MTU,这完全是端到端行为,卸载了跟网络没有毛线关系的额外动作。
- IPv6等效于IPv4永久无条件设置Don’t Fragment标识(但并不绝对,对于1280以下MTU则例外!后面讲)。
卸载了IPv4头的分片相关字段后,这些分片字段便放在了扩展头里面:
/** fragmentation header*/struct frag_hdr {__u8 nexthdr;__u8 reserved;__be16 frag_off;__be32 identification;
};
具体的细节和实现方式,我在这篇文章里不赘述,自行查看RFC以及Linux内核实现源码。我这里只是闲谈,所以说点别的。
如果仅仅是以上的简单规定,那么在实现的时候将会遇到很多无法避开的问题:
- 在端主机只知道自己的链路MTU,如何知道中间路径的最小MTU,即如何确定要不要分片?
- 计算通过MTU发现找到了最小MTU,如何保证实际数据传输时就是按照MTU探测时的路径走的?
- …
PMTU的实现过程,我这里依然不谈,我要说的是,IPv6为了让分片不那么频繁进行,依靠宣传力量 强制建议了一个规则 ,即 所有传输IPv6报文的链路都要提供1280字节以上的MTU容量! 呵呵,强制建议,意思是你必须实现它,如果你不实现,我也没有办法,毕竟为了保持网路的透明性,不能把事情做绝,但是求求你实现它好吗?我这可是IPv6的标准啊!
那么,为什么是1280字节?这是一个权衡的结果。如果限制的最小MTU太小,那么将会有很多链路为了成本或者其它博弈结论考虑,提供小的MTU,如此一来,这种 不得不进行 的补救分片方案就会非常多,相当于抵消了IPv6禁止分片的收益。小分片非常容易助力网路的拥塞,如果拥塞控制队列机制是基于包而不是基于自己的话,这会更加严重,并且小分片,特别是TCP段的小分片,一个丢失则整个重传,这会增加链路的重传率。
而反过来如果MTU限制的非常大,则不利于一些小型设备弱链路(它们可能没有宽阔的道路可用)的默认报文大小的设定。我们看RFC2460里面的一段:
It is strongly recommended that IPv6 nodes implement Path MTU
Discovery [RFC-1981], in order to discover and take advantage of path
MTUs greater than 1280 octets. However, a minimal IPv6
implementation (e.g., in a boot ROM) may simply restrict itself to
sending packets no larger than 1280 octets, and omit implementation
of Path MTU Discovery.
考虑到隧道封装协议的广泛存在以及以太网1500字节MTU的普遍性,1280空出220字节给隧道使用,足够了。
不管大了还是小了都不好,那么什么样的限制最好呢?
关于这点,我倒是有个形而上的想法,即整个互联网上所有链路MTU的方差,均差越小越好!即 大家的MTU都往使用最多的MTU的附近凑! 由于我们需要限制的是最小MTU,那么就是使用最多的1500,其下面不远的位置,照顾到隧道协议,空出足够的220字节,就选1280字节!完美。
那如果就有人有意或者无意,考虑到底层链路的硬件投资,考虑到兼容性,其MTU小于1280字节,怎么办?
没办法,还是要分片的,这就是 不得不进行的分片 …看[RFC2460]
(https://tools.ietf.org/html/rfc2460#section-5):
5. Packet Size Issues
、
IPv6 requires that every link in the internet have an MTU of 1280
octets or greater. On any link that cannot convey a 1280-octet
packet in one piece, link-specific fragmentation and reassembly must
be provided at a layer below IPv6.
关于IPv6的分片这个话题,我们看Linux实现的代码最直接:
static int ip6_finish_output(struct sock *sk, struct sk_buff *skb)
{if ((skb->len > ip6_skb_dst_mtu(skb) && !skb_is_gso(skb)) ||dst_allfrag(skb_dst(skb)) ||(IP6CB(skb)->frag_max_size && skb->len > IP6CB(skb)->frag_max_size))return ip6_fragment(sk, skb, ip6_finish_output2);elsereturn ip6_finish_output2(sk, skb);
}
三个条件满足任何一个,报文就会被分片:
- skb的长度大于PMTU发现的mtu值
- 本地链路mtu小于1280字节
- skb分片中的最大分片长度大于PMTU发现的mtu值
怎么理解呢?有点乱,但别慌,先看看这个函数的调用路径,一切就都明白了。
该函数由两个路径抵达,分别是:
- 本地始发
由 ip6_local_out 路径调用抵达 - 转发
由 ip6_forward 路径调用抵达
对于本地始发的数据报文,显然它之前不会有任何分片,它本来就是一个单独的skb而已。那么就只有前2个条件会触发该本地始发的数据报文被分片, 这正是端到端的分片行为 ,完全合法。
接下来就看 系统是如何禁止非本地始发的转发报文被分片的。 这也不难,看看ip6_forward的实现便知道了:
int ip6_forward(struct sk_buff *skb)
{...mtu = dst_mtu(dst);if (mtu < IPV6_MIN_MTU)mtu = IPV6_MIN_MTU;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_BH(net,ip6_dst_idev(dst), IPSTATS_MIB_INTOOBIGERRORS);IP6_INC_STATS_BH(net,ip6_dst_idev(dst), IPSTATS_MIB_FRAGFAILS);kfree_skb(skb);return -EMSGSIZE;}...
}
不符合要求的数据包,直接在forward过程中就被丢了,主要丢两种:
- 不符合要求的独立报文
数据包长度大于当前路由项的mtu,当然要被丢弃,因为IPv6规定中间设备不能帮这些报文分片。 - 不符合要求的重组报文
首先要明白为啥要重组呢?重组不是必须在目的地进行吗?但是在某些情况下,比如防火墙必须探究4层内容时(防火墙,NAT[虽不建议,但不阻止]等),就必须先进行重组,探究分析完之后再按照重组前原样分片。
因此,如果这些分片中最大的分片长度大于mtu时,将来按原样分片后的这个分片报文依然无法通过,按照上述1,丢弃。
好吧,千言万语一张图:
皮鞋湿,不会胖。
我说,简单直接的东西最高效,确实如此,因为不需要考虑太多。
有协议不如没协议,线缆两边直接放脉冲效率是最高的,同样的道理我们已经目睹过好多,比如电视越来越智能,打开电视机的速度越来越慢,同理还有手机,汽车…
然而,复杂智能却是一条覆水难收一路飙进的不归路,谁也阻止不了,我们不能为了效率让事情更简单,但是我们却可以纵向分割它。分层网络协议模型给我们提供了一个样例。
虽然事情越来越复杂,但是TCP/IP却越来越高效,这是为什么?
我们不能一锅粥地阻止七层变复杂,却可以 硬性规定底层必须怎样怎样做! 七层就是为了复杂而存在的,但是它却不用关心传输。对于IP层,我们可以在它保证传输透明的前提下,强制它按照某种规则行事,这就是高效率的根源!
是的,完全按照规则,不要自行其是不要猜,并且不要判断。是的,把判断放到规则制定之前!我来举一个例子。
- 普通道路:如果遇到红灯,停车等待,如果遇到不守规则的行人,停车等待,如果在等待时看到绿灯且斑马线无行人,开车走,如果忘记关燃气,下一个路口掉头…
- 高速公路:非机动车24*365不能上路,机动车只能往前走,莫回头。
然后你看,结果是不是很明显。
是的,道路就是通行的,如果混成一锅粥地让所有事情一起复杂,那就是老式的市集了,货品买卖,行人,机动车,牛马,被安排在同一个地方…复杂是复杂,然而效率呢?
后记
作为正则之父,终于买了一本《精通正则表达式(第三版)》,准备猛学!
我的《闲谈IPv6》系列还会写下去的。
前天早上等班车期间,发了两则朋友圈,回答了两个问题,其实这是我自问自答的:
- 为什么皮鞋比布鞋更落后?
今天我来说说为什么皮鞋比布鞋更显得落后。
我猜原始人类穿的第一双鞋要么是皮鞋,要么就是草鞋,但绝对不是布鞋。
最有可能是皮鞋。
因为即便是草鞋也需要复杂的编织工艺,而皮鞋只需要杀一头野兽简单剥皮加工就够了。
布鞋,那是进入文明社会后的事情了,只有社会分工才能促使纺织工艺的进步,而纺布是极其复杂的。首先你要种植能产出细纤维的植物,然后要做成细线,然后依靠复杂的拼装机械将线纺织成布。。。野人是做不到的。
如果将一个人置于野外环境,任其流浪,如果必须有一双鞋,他能做出来的一定是皮鞋而不是布鞋。
所以,皮鞋特别是真皮皮鞋,是落后而不是先进的。
浙江温州皮鞋湿,下雨进水不会胖。
- 犹太人个体为什么比较成功?
为什么犹太人聪明且成功,出了那么多牛人,科学家,音乐家,经济学家,且作为团体也是非常成功,这是为什么?
我认为这原因很简单,如果不懂历史很难理解。
因为三千多年来,犹太人被埃及人,亚述人,巴比伦人,波斯帝国,希腊人,罗马帝国,阿拉伯人,突厥人,奥斯曼帝国,纳粹德国,英国人持续地不断进行人工选择,帮这个种族淘汰掉了超级多的弱者,剩余的活下来的当然是强人了。
这就跟为什么参加过长征的那一代老红军整天抽烟喝酒也能长寿是一样的道理,因为那些身体不行的在长征途中就已经挂了。
浙江温州皮鞋湿,下雨进水不会胖。
浙江温州皮鞋湿,下雨进水不会胖!