从Linux 2.6.8内核的一个TSO/NAT bug引出的网络问题排

四年多前的一个往事

大约在2010年的时候,我排查了一个问题。问题描述如下:

服务端:Linux Kernel 2.6.8/192.168.188.100

客户端:Windows XP/192.168.40.34

业务流程(简化版):

1.客户端向服务端发起SSL连接

2.传输数据

现象:SSL握手的时候,服务端发送Certificate特别慢。

分析:

具体思路,也就是当时怎么想到的,我已经忘了,但是记住一个结论,那就是纠出了Linux 2.6.8的NAT模块的一个bug。

在抓取了好多数据包后,我发现本机总是发给自己一个ICMP need frag的报错信息,发现服务端的Certificate太大,超过了本机出网卡的MTU,以下的一步步的思路,最终纠出了bug:

1.证实服务端程序设置了DF标志。这是显然的,因为只有DF标志的数据包才会触发ICMP need frag信息。

2.疑问:在TCP往IP发送数据的时候,会检测MTU,进而确定MSS,明知道MSS的值,怎么还会发送超限的包呢?计算错误可能性不大,毕竟Linux也是准工业级的了。

3.疑问解答:幸亏我当时还真知道一些名词,于是想到了TCP Segment Offload这个技术。

TCP Segment Offload简称TSO,它是针对TCP的硬件分段技术,并不是针对IP分片的,这二者区别应该明白,所以这与IP头的DF标志无关。对于IP分片,只有第一个分片才会有完整的高层信息(如  果头长可以包括在一个IP分片中的话),而对于TSO导致的IP数据包,每一个IP数据包都会有标准的TCP头,网卡硬件自行计算每一个分段头部的校验值,序列号等头部字段且自动封装IP头。它旨在提高TCP的性能。

4.印证:果然服务器启用了TSO

5.疑问:一个大于MTU的IP报文发送到了IP层,且它是的数据一个TCP段,这说明TCP已经知道自己所在的机器有TSO的功能,否则对于本机始发的数据包,TCP会严格按照MSS封装,它不会封装一个大包,然后让IP去分片的,这是由于对于本机始发而言,TCP MSS对MTU是可以感知到的。对于转发而言,就不是这样了,然而,对于这里的情况,明显是本机始发,TCP是知道TSO的存在的。

6.猜测:既然TCP拥有对TSO的存在感知,然而在IP发送的时候,却又丢失了这种记忆,从TCP发往IP的入口,到IP分片决定的终点,中间一定发生了什么严重的事,迫使TCP丢失了TSO的记忆。

7.质疑:这种故障情况是我在公司模拟的,通过报告人员的信息,我了解到并不是所有的情况都会这样。事实上,我一直不太承认是Linux协议栈本身的问题,不然早就被Fix了,我一直怀疑是外部模块或者一些外部行为比如抓包导致的。

8.可用的信息:到此为止,我还有一个信息,那就是只要加载NAT模块(事实上这是分析出来的,报告人员是不知道所谓的NAT模块的,只知道NAT规则)就会有这个现象,于是目标很明确,死盯NAT模块。

9.开始debug:由于Linux Netfilter NAT模块比较简单,根本不需要高端的可以touch到内存级的工具,只需要printk即可,但是在哪里print是个问题。

10.出错点:在调用ip_fragment(就是该函数里面发送了ICMP need frag)之前,有一个判断(省略了不相关的):

if (skb->len > dst_pmtu(skb->dst) && !skb_shinfo(skb)->tso_size) {

return ip_fragment(skb, ip_finish_output);

}

前一个判断显然为真,如果要想调用ip_fragment的话,后一个判断一定要是假,实际上,如果开启了TSO,就不该调用ip_fragment的。

11.查找tso_size字段:事情很明显了,一定是哪个地方将tso_size设置成了0!而且一定在NAT模块中(98%以上的可能性吧...),于是在NAT模块中查找设置tso_size的地方。

12.跟踪ip_nat_fn:这是NAT的入口,进入这个入口的时候,tso_size不是0,可是调用了skb_checksum_help之后tso_size就是0了,问题一定在这个函数中,注意,调用这个help有一个前提,那就是硬件已经计算了校验和。在这个help函数中,有一个skb_copy的操作,正是在这个copy之后,tso_size变成了0,于是进一步看skb_copy,最终定位到,copy_skb_header的最后,并没有将原始skb的tso_size复制到新的skb中,这就是问题所在!

13.触发条件:什么时候会调用skb_copy呢?很简单,如果skb不完全属于当前的执行流的情况下,按照写时拷贝的原则,需要复制一份。故障现象就是慢,而数据为本机始发,且为TCP。我们知道,TCP在没有ACK之前,skb是不能被删除的,因此当前的skb肯定只是一个副本,因此就需要拷贝一份了。

14.影响:如此底层的一个函数。搜索代码,影响巨大,各种慢!对于那次的慢,其慢的流程为:socket发送DF数据--感知TSO--丢失TSO--ICMP need frag--TCP裁成小段继续发送...如果禁止了lo的ICMP,那么更慢,因为TCP会触发超时重传,而不是ICMP的建议裁减,并且重传是不会成功的,直到用户程序感知,自行减小发送长度。

为什么旧事重提

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/16219.html