TCP的TIME_WAIT快速回收与重用
声明一点:
Linux中是无法修改tcp的TIME_WAIT值的,除非重新编译,起码我是没有找到怎么改。值得注意的是,net.ipv4.tcp_fin_timeout这个参数是FIN_WAIT_2的值,而不是TIME_WAIT的值。我不知道为何很多人都会把它当成是TIME_WAIT的值,想了一下,我觉得是两点:
1.TIME_WAIT过于耀眼,以至于所有出现timeout,加上里面有个tcp的配置,都会想当然往TIME_WAIT上联系;
2.FIN_WAIT_2过于默默无闻,以至于很少有人知道它也是一种状态。
所以,我想大家在学习的时候,不能想当然。
TIME_WAIT的作用
TCP初见于互联网早期,当时的网络很不稳定,大量的丢包,为了冗余,大量包复制多径传输,网速慢--正因为如此,TCP才会更有意义。为了保证TCP的严格语义,就要避免上述冗余机制以及网速慢导致的问题,这些问题集中体现在连接关闭时的四次挥手上。
由于TCP是全双工的,因此关闭连接必须在两个方向上分别进行。首先发起关闭的一方为主动关闭方,另一方为被动关闭方。很多人都会在这里晕掉,实际上四次挥手比三次握手还简单。四次挥手简单地分为三个过程:
过程一.主动关闭方发送FIN,被动关闭方收到后发送该FIN的ACK;
过程二.被动关闭方发送FIN,主动关闭方收到后发送该FIN的ACK;
过程三.被动关闭方收到ACK。
以上三步下来,圆圈就闭合了!也就是说,在过程三后,被动关闭方就可以100%确认连接已经关闭,因此它便可以直接进入CLOSE状态了,然而主动关闭的一方,它无法确定最后的那个发给被动关闭方的ACK是否已经被收到,据TCP协议规范,不对ACK进行ACK,因此它不可能再收到被动关闭方的任何数据了,因此在这里就陷入了僵局,TCP连接的主动关闭方如何来保证圆圈的闭合?这里,协议外的东西起作用了,和STP(Spanning tree)依靠各类超时值来收敛一样,IP也有一个超时值,即MSL,这类超时值超级重要,因为它们给出了一个物理意义上的不可逾越的界限,它们是自洽协议的唯一外部输入。MSL表明这是IP报文在地球上存活的最长时间,如果在火星上,Linux的代码必须要重新定义MSL的值并且要重新编译。
于是问题就解决了,主动关闭一方等待MSL时间再释放连接,这个状态就是TIME_WAIT。对于被动关闭的一方,发出FIN之后就处在了LAST_ACK状态了,既然已经发出FIN了,缺的无非也就是个ACK,连接本身其实已经关闭了,因此被动关闭的一方就没有TIME_WAIT状态。
实际上,两倍MSL才能说明一个报文的彻底丢失,因为还要记入其ACK返回时的MSL。
TIME_WAIT的问题
这个就不多说了,由于TIME_WAIT的存在,短连接时关闭的socket会长时间占据大量的tuple空间。
TIME_WAIT的快速回收
Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待两倍的MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。释放了之后,一个连接的tuple元素信息就都没有了,而此时,新建立的TCP却面临着危险,什么危险呢?即:
1.可能被之前迟到的FIN包给终止的危险;
2.被之前连接劫持的危险;
...
于是需要有一定的手段避免这些危险。什么手段呢?虽然曾经连接的tuple信息没有了,但是在IP层还可以保存一个peer信息,注意这个信息不单单是用于TCP这个四层协议的,路由逻辑也会使用它,其字段包括但不限于:
对端IP地址
peer最后一次被TCP触摸到的时间戳
...
在快速释放掉TIME_WAIT连接之后,peer依然保留着。丢失的仅仅是端口信息。不过有了peer的IP地址信息以及TCP最后一次触摸它的时间戳就足够了,TCP规范给出一个优化,即一个新的连接除了同时触犯了以下几点,其它的均可以快速接入,即使它本应该处在TIME_WAIT状态(但是被即快速回收了):
1.来自同一台机器的TCP连接携带时间戳;
2.之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本机;
3.新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳。
看样子只有以上的3点的同时满足才能拒绝掉一个新连接,要比TIME_WAIT机制设置的障碍导致的连接拒绝几率小很多,但是要看到,上述的快速释放机制没有端口信息!这就把几率扩大了65535倍。然而,如果对于单独的机器而言,这不算什么,因此单台机器的时间戳不可能倒流的,出现上述的3点均满足时,一定是老的重复数据包又回来了。
但是,一旦涉及到NAT设备,就悲催了,因为NAT设备将数据包的源IP地址都改成了一个地址(或者少量的IP地址),但是却基本上不修改TCP包的时间戳,这就带来了问题。
假设PC1和PC2均启用了TCP时间戳,它们经过NAT设备N1往服务器S1的22端口连接:
PC1:192.168.100.1
PC2:192.168.100.2
N1外网口(即NAT后的地址):172.16.100.1
S2:172.16.100.2
所有涉事机器的配置:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
TCP的时间戳是根据本机的jiffers,uptime计算的,现在我能保证PC2的时间戳肯定远小于PC1。现在在PC1上先做一个telnet:
telnet 172.16.100.2 22
连接成功,S1上抓包,得到时间戳timestamps:TS val 698583769
为了让S1主动关闭进而快速回收TIME_WAIT,在S1上执行:
kill $(ps -ef|grep [s]sh|grep acce|awk -F ' ' '{print $2}');
目的是把仅仅光完成三次握手的连接终止掉而不触动已经连接的ssh。此时马上在PC2上telnet:
telnet 172.16.100.2 22
不通!在S1上抓包,得到时间戳timestamps:TS val 27727766。明显小于PC1的!由于有NAT设备,S1看来是同一台机器发出的,且出现了时间戳倒流,连接拒绝!此时在S1上查看计数值:
cat /proc/net/netstat
发现了PAWSPassive对应的值增加了1,每次PC2重发SYN,该计数值均会增加1,直到一个MSL时间过后,才能连接成功。如果反过来就没有问题,即先在PC2上telnet,然后S1主动关闭,然后紧接着PC1上telnet依然可以成功,这是因为时间戳是递增的,不满足上述的第三点。
仅仅两台机器就出现了这个问题,试问如果大量的源端机器在服务器的入口处遇到了NAT设备会怎样?即一台三层NAT设备部署在高负载网站的入口处...没有谁能保证时间戳小的机器一定先发起连接,各个机器频繁连接断开后依然按照时间戳从小到大的顺序连接!!
TIME_WAIT快速回收在Linux上通过net.ipv4.tcp_tw_recycle启用,由于其根据时间戳来判定,所以必须开启TCP时间戳才有效。
建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题。
TIME_WAIT重用
如果说TIME_WAIT(输入法切换太烦人了,后面简称TW)回收只是一种特定系统的优化实现的话,那么TW重用则有相关的规范,即:如果能保证以下任意一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用:
1.初始序列号比TW老连接的末序列号大
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大
Linux上完美实现了上述的特性,可以通过下面的实验来证实:
S1上的服务程序:侦听端口1234,accept新连接,发送一段数据后调用close主动关闭连接。
S1上的额外配置:通过iptables禁止RESET包进入第四层,因为它会将TW状态终结。
PC1上客户端程序:绑定192.168.100.1,2000端口,连接S1,获取数据后调用close关闭连接。
PC2上客户端程序:使用IP_TRANSPARENT选项同样绑定192.168.100.1地址和2000端口,其它和PC1的程序相同。
启动服务端S1:172.16.100.2,不断侦听端口1234;
启动PC1上的C1:192.168.100.1,端口2000,连接S1的1234端口;
此时在S1上抓包,获取正常的三次握手/数据传输/四次挥手数据包。此时在S1上netstat -ant可以看到一个TW状态的连接。
启动PC2上的C2:192.168.100.1,端口2000,连接S1的1234端口;
此时在S1上抓包,SYN序列号seq 3934898078大于PC1发起连接时的最后一个序列号[F.], seq 2513913083, ack 3712390788,S1正常回复SYNACK:Flags [S.], seq 3712456325, ack 3934898079, ...对于这种TW重用的情况,S1的SYNACK的初始序列号是通过TW状态老连接的最后一个ack,即3712390788,加上常量65535+2算出来的!
以上的实验是在关闭时间戳的情况下完成的,实际上开启时间戳的话,重用的可能性更高一些,毕竟是否能重用一个TW连接是通过以上的条件之一来判断的!
从外部干掉TIME_WAIT
TIME_WAIT状态时则一个阑尾!Linux系统上,除了使能recycle tw,在Linux系统上你无法更简单地缩短TW状态的时间,但是80%的问题却都是由TW状态引发,在Windows系统上,你需要在注册表添加一个隐式的项,稍微拼写错误都会引发沉默的失败!TW确实让人生气,因此我一直都希望干掉TW状态的连接,特别是干掉服务端TW状态的连接!我们可以通过TCP的RESET来干死TW连接。这个怎么说呢?
根据TCP规范,收到任何的发送到未侦听端口或者序列号乱掉(窗口外)的数据,都要回执以RESET,这就是可以利用的!一个连接等待在TW,它自身无能为力,但是可以从外部间接杀掉它!具体来讲就是利用了IP_TRANSPARENT这个socket选项,它可以bind不属于本地的地址,因此可以从任意机器绑定TW连接的peer地址以及端口,然后发起一个连接,TW连接收到后由于序列号乱序会直接发送一个ACK,该ACK会回到TW连接的peer处,由于99%的可能该peer已经释放了连接(对端由于不能收到FIN-ACK的ACK,进而不放心ACK是否已经到达对端,等待MSL以便所有的老数据均已经丢失),因此peer由于没有该连接会回复RESET,TW连接收到RESET后会释放连接,进而为后续的连接腾出地方!
Linux实现Tips
Linux照顾到了一种特殊情况,即杀死进程的情况,在系统kill进程的时候,会直接调用连接的close函数单方面关闭一个方向的连接,然后并不会等待对端关闭另一个方向的连接进程即退出。现在的问题是,TCP规范和UNIX进程的文件描述符规范直接冲突!进程关闭了,套接字就要关闭,但是TCP是全双工的,你不能保证对端也在同一个时刻同意并且实施关闭动作,既然连接不能关闭,作为文件描述符,进程就不会关闭得彻底!所以,Linux使用了一种“子状态”的机制,即在进程退出的时候,单方面发送FIN,然后不等后续的关闭序列即将连接拷贝到一个占用资源更少的TW套接字,状态直接转入TIMW_WAIT,此时记录一个子状态FIN_WAIT_2,接下来的套接字就和原来的属于进程描述符的连接没有关系了。等到新的连接到来的时候,直接匹配到这个主状态为TW,子状态为FIN_WAIT_2的TW连接上,它负责处理FIN,FIN ACK等数据。
TIME_WAIT快速回收与重用
通过以上描述,我们看到TW状态的连接既可以被快速回收又可以被重用,但是二者的副作用是不同的。对于快速回收,由于丢失了TW连接的端口信息,全部映射到了IP地址信息,所以整个IP地址,也就是整机均被列入了考察对象,这本身并没有什么问题,因为快速回收只考虑时间戳信息,只要其保持单调递增即可,一般的机器时间是不会倒流的,但是遇到NAT合并就不行了,NAT设备为所有的内部设备代理一个IP地址即主机标识,然而却不触动其时间戳,而各个机器的时间戳并不满足任何规律...
TW重用解决了整机范围拒绝接入的问题,但是却面临资源消耗的问题。它这个做法的依据之一仍然为,一般一个单独的主机是不可能在MSL内用同一个端口连接同一个服务的,除非它做了bind。因此等待一些遗留的数据丢失或者到达是有盼头的。有一点我有异议,我个人感觉,如果处在默默地TW等待中,有默默地非递增SYN或者递增时间戳SYN到来,千万别发ACK,只要默默丢弃即可,因为你发了ACK,对方在已经终止了连接的情况下,就会发RESET,进而终止掉本段连接。
TIME_WAIT的80/20悲剧
80%的问题都由20%的TW引发,甚至在各种的TCP实现中,大量的代码在处理TW!我个人觉得这有点过了!引入TW状态是为了确认老数据到来或者消失,且等待时延那么久,这已经是很多年以前的事了,那时我可能刚出生,家里可能还没有装电话...那时的网络条件,引入这些机制是确实需要的,但是随着网络技术的发展,TW已经慢慢成了鸡肋。即便新的TCP连接被老的FIN终止又怎样,即使新的连接被老的劫持又能怎样,即便不考虑这些,MSL未免也太长了些吧,话说当年DDN年代,这个值就已经很久了...不要试图保持TCP的安全了,即使面对中间人又能怎样?我们不是可以用SSL吗?TCP作为一种底层的传输协议,一定要简单,可是现在呢?虽然其内核保持着原汁原味,但是其细节使多少求知若渴的人踱步门外啊,不得不说,TCP的细节太复杂了,即使是再好的作家,也无法写出一本让人彻底明白的关于TCP细节的书。
看看规范,各种公式,各种不可插拔的算法,各种魔术字,即使作者本人估计都很难说清楚内中细节。不得不说,TCP有点过度设计了,作为当年的设计精品,在当今越发往上层移动的年代,不合适了。如今越来越多的协议或者开元软件使用简单的UDP做扩展,在实现按序到达,确认,否认,时间戳,可靠连接等机制中实现自己需要的而不是所有,从TLS到OpenVPN,无一没有把UDP当成下一代的天骄。我很讨厌TCP,很讨厌这种乱七八糟的东西。你可能会反驳我,但我觉得你被洗脑了,你要知道,如果让你设计一个可靠的有连接协议,你可能做的真的比TCP更好。