网络

【tcp】关于 TCP FIN_WAIT2状态的一个细节问题

2022-08-13  本文已影响0人  Bogon

一个关于TCP挥手断开的细节的讨论,越发感到TCP协议真的是非常令人讨厌,这个协议已经成了人们装逼的谈资,就是因为它非常复杂,且毫无确定性可言!
如果你能说出它的任何细节方面的前因后果,那你一定就是牛人了,但这其实毫无意义。

截一张TCP状态机的局部,来自RFC793 [page 22]

image.png

当触发超时会主动关闭连接,这里涉及到了四次挥手,作为关闭方会发送fin,对端内核会回应ack,这时候客户端从fin-wait1到fin-wait2,而服务端在close-wait状态,等待触发close syscall系统调用。
服务端什么时候触发close动作?需要等待服务端业务逻辑执行完毕。

image.png

这个图非常权威。
我们注意到FINWAIT-2这个状态,它的转移条件只有一个,即收到对端的FIN,然后进入TIMEWAIT。

那么问题来了,如果对端死活不发送FIN,本端会一直待在FINWAIT-2状态吗?

按照TCP全双工的概念推论,答案显然是:收到对端的FIN之前,本端会一直保持FINWAIT-2状态。

这是合理的,也是符合TCP规范的,因为TCP是一个双向全双工的传输协议,本端发送FIN仅仅意味着本端到对端这个方向上的传输结束了,而对端到本端的传输依然可以继续,直到对端也发送一个FIN过来。
所以说我们看到断开连接的挥手动作是4次,其实就是两个来回,每一个来回关闭一个方向的数据传输。

非常完美的解释,非常完美的规范!

但是….TCP总是伴随着这么些烦人的但是。

如果对端故意不发送FIN,且也不传输数据,那么意味着本端始终处在FINWAIT-2状态而资源无法释放,这不正是一个DDoS的典型场景吗?
但是如果不这么做,也不符合TCP双向全双工独立控制的规范啊!
是规范重要还是现实中的问题重要?

我们仔细想想迄今为止有多少TCP连接时关闭一半的,即客户端始发方向关闭连接,而服务端依然在传输大量数据这种情形,似乎几乎是没有的。
相反,几乎很多的C/S模式的TCP连接都是单向的,比如文件下载,至始至终,数据几乎都是从服务器往客户端发送,在最初的客户端请求文件结束后,事实上就相当于客户端始发方向的连接已经被关闭了!

因此,为了实现双向全双工的语义,完全可以在应用层做,完全没有必要在挥手关闭连接的逻辑上照本宣科而较真儿,事实上,我认为,TCP当初这么设计就是错误的,至少是不合理的,除了增加了复杂性之外,毫无意义。
也许当初设计协议的都是学院派,始终保持着一种对完备性的笃信和追求,所以既然TCP取自传输控制之名,那么就必须完成传输与控制之完备性的逻辑,也许换个名字会好些。

从Telnet、FTP、到Apache,Nginx,几乎所有的TCP服务的实现均遵循了收到客户端的FIN之后立即发送FIN这么一个不成文的事实,也就是说,对于主动关闭的一方,当它发送完FIN进入FINWAIT-2状态后,可以在预期的时间内收到对端的FIN从而进入TIMEWAIT状态,而且这个所谓的“预期的时间”不会太长,以秒计算,因此给定一个超时时间是明智的。

因此,针对上面问题“如果对端死活不发送FIN,本端会一直待在FINWAIT-2状态吗?”的回答我把可能的答案罗列:

收到对端的FIN之前,本端会一直保持FINWAIT-2状态(标准的要求)
收到对端的FIN之前,本端会保持FINWAIT-2状态一段足够的时间,超过此时间,连接即释放(现实的要求)

我们看到,历史选择了现实而摒弃了理想。Linux任意使用2.2内核以上的发行版,看tcp的manual,其中:

tcp_fin_timeout (integer; default: 60; since Linux 2.2)
This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed. This is
strictly a violation of the TCP specification, but required to prevent denial-of-service attacks. In Linux 2.2,
the default value was 180.

现在FINWAIT-2为什么会有个超时时间的问题已经解释清楚了,接下来的问题是,如果FINWAIT-2的timer超时了,这个TCP连接将何去何从?

我事先还真没有了解过实现的细节,但是按照我对这个问题逻辑的理解,我认为timer到期后连接应该被销毁,顺便给对端发送一个reset。

既然在预期的时间内对端没有发送FIN(是的,FIN会丢失,但是TCP也会重传,另外,网线也可能被剪断),那么说明对端是“违约”的,至少是不符合常理的,对待不遵守游戏规则的,当然也不需要规则内的措施,直接释放连接是本端的原则,而发送reset则是针对对端“违约”的告知,就是想告诉对端“你违约了,明白吗?”

我和两位同事楼下抽根烟讨论了这个问题,一位同事持有不同意见,认为不会发送reset,事实证明他是对的,确实不会发送reset。

如果TCP对端违约,按照这个理念,超时后把连接资源默默释放即可,不必再与之对话!

从社会工程学的角度来看,如果你只是为了说一句“你错了”,而发送一个reset,搞不好就会被绕进去,所以不理它就是了。

好了,最后一个问题,在FINWAIT-2超时之后,连接还会进入TIMEWAIT状态吗?

我认为是不会的,连接会直接消失。
但是一位同事通过代码确认了一个不同的意见,他认为在经历了FINWAIT-2之后,即FINWAIT-2的timer到期后,连接依然会进入到TIMEWAIT状态,其通过tcp_time_wait函数的调用路径可以确认,调用参数为:

tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);

我始终是持怀疑态度的,一堆堆的if分支,不去实际run的话,也许你能讲清楚的那个分支反而是99%进不去的分支,所以我依然选择设计实验来验证。

两台机器,一台作为Client,IP地址为192.168.44.138,另一个为监听了22端口的Server,IP地址为192.168.44.111。

我在Client上配置以下的iptables规则,以阻止Server发送的FIN到达Client的TCP处理逻辑,以模拟对端永远不回复FIN导致FINWAIT-2到期的情形:

iptables -A INPUT -s 192.168.44.111 -p tcp --tcp-flags SYN,FIN,RST FIN  -j DROP

然后把Client的fin_timeout设置短一些:

sysctl -w net.ipv4.tcp_fin_timeout=5    

最后我在Client上发起一个连接并随即Ctrl-]+q关闭,以使一个TCP连接进入FINWAIT-2状态
iptables规则会阻止对端的FIN,因此本端将进入FINWAIT-2而不是TIMEWAIT。

# telnet 192.168.44.111 22
Trying 192.168.44.111...
Connected to 192.168.44.111.
Escape character is '^]'.
SSH-2.0-OpenSSH_7.4
^]
telnet> q
Connection closed.

迅速观察netstat:

# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       ESTABLISHED 96417/telnet        

# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -           
        
# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -                
  
# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -                   

# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -                   

# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -                   

# netstat -anpt|grep 111
tcp        0      0 192.168.44.138:53068    192.168.44.111:22       FIN_WAIT2   -                   

大概5秒钟,连接灰飞烟灭,什么也没有剩下,连接并没有进入到TIMEWAIT,与此同时tcpdump抓包,也没有看到任何reset。
实验很简单,但是却说明了问题。

你没看错,连接在FINWAIT-2超时后并不会进入TIMEWAIT状态,也不会发送reset,而是直接默默消失。

关于tcp_time_wait这个函数的代码非常恼人,只说几个细节:

  1. 只要调用tcp_time_wait,TCP连接状态就会变成TCP_TIME_WAIT;

  2. 如果以TCP_FIN_WAIT2参数调用tcp_time_wait,则TCP_FIN_WAIT2作为substate处理对端的FIN;

  3. 不管是TCP_FIN_WAIT2还是TCP_TIME_WAIT,均是将TCP连接从Establish哈希链表摘除,重新分配TW item链接进入哈希表。

关于TCP的任何东西,代码、文档、文章和测试case脚本,都是让人感慨的:

TIME-WAIT太多的问题
PAWS问题
PAWS与NAT的问题
Nagle问题
Nagle与CORK问题
同时打开,同时关闭问题
Reno,CUBIC的问题
慢启动,慢慢慢
keepalive问题
tcp repair问题
滑动窗口问题
….

你知道TIME-WAIT持续多久吗?你真的知道吗?

很多人会回答120秒,很多人会回答2MSL,很多人不知道什么是MSL,2MSL为什么是120秒而不是360秒,我要是说这是根据光速以及地球的周长算出来的你信吗?
事实上确实是和地球周长有关的。
如果是在火星上,TCP的TIME-WAIT超时值一定至少半小时。

可是,对于Linux系统,上面的说法全是谎言,在Linux上,TIMEWAIT的定义是:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                  * state, about 60 seconds */

你不信吗?试试看呗,还是刚才那个Low逼实验,这次把iptables规则去掉,Ctrl-]+q之后,观察TIMEWAIT的时间,观察期间把你的Windows系统右下角的钟表打开,看看是不是60秒后TIME-WAIT连接就消失了。

Why?为什么TCP的实现一而再地违反所谓的规范?到底有没有规范?到底什么是MUST的,什么是MAY的。

我同样不喜欢HTTP,确切的说,是HTTP 1.x,同样的原因,它太松散了。
请问HTTP的头部最长有多长?标准并没有明确的规定,读到\r\n\r\n为结束,但是Web服务器的实现却规定了。
不然呢?不然一个恶意的客户端可以产生100T的头部,瞬间耗尽服务器的所有资源。

所以我就很看好HTTP 2.0,它解决了这个问题。

谈谈企业招聘和面试。

懂TCP有什么了不起吗?并没有。
可是TCP几乎是各家必考的内容,令人不解的是,其实很多面试官也不懂,一群不懂TCP的人招了另一群不懂TCP的人,这并不耽误所有的人持续跪舔TCP!

精不精通不重要,懂就行,TCP是行话,大家一说TCP就知道都在一个坑里找食的。

IP层的东西难道不重要吗?

有人碰到一个问题,说是在一台机器上确认TCP的init cwnd就是10,然而一个进程发包的时候只能发出去1个包

我让他赶紧ip route ls tab all,结果发现了一条路由:

..... initcwnd 1
image.png

这不就是问题的根源吗?很多人不知道initcwnd还能针对路由来指定。不知道吧,哈哈,你也你知道,但你知道这是为什么吗?

什么是路由?TCP是不管路由的。但是TCP的拥塞控制却完全离不开路由。

我们都知道,一条路和另一条路的拥塞程度是完全不同的,IP层的路由正是基于这个拥塞程度的不同,想办法用参数告诉你,哪条路更好走一些,仅此而已。悲哀的是,端到端的TCP并没有“路”的概念。

参考

解决Linux服务器 FIN_WAIT2 连接过多的问题
https://blog.51cto.com/professor/1725386

TCP之RST
https://www.yuque.com/infuq/others/yyvoa7

Linux处理TIME_WAIT和FIN_WAIT_2状态
https://www.cnblogs.com/Gsealy/p/14537774.html

上一篇下一篇

猜你喜欢

热点阅读