• TCP状态机:当服务端主动发FIN进TIME_WAIT,客户端源端口复用会发生什么


    0X01

    正常情况下TCP连接会通过4次挥手进行拆链(也有通过RST拆除连接的可能,见为什么服务器突然回复RST——小心网络中的安全设备),下图TCP状态机展示了TCP连接的状态变化过程:

    我们重点看4次挥手的过程:

    1. 想要拆除连接的一方A发送FIN报文,自身进入到FIN_WAIT_1状态;
    2. 被拆除连接的一方B接收到FIN报文,发ACK,自身进入到CLOSE_WAIT状态;
    3. A收到ACK,进入FIN_WAIT_2状态;
    4. B发送FIN,自身进入LAST_ACK状态;
    5. A收到FIN,发送ACK,自身进入TIME_WAIT状态;
    6. B收到ACK报文,B上的这个socket关闭,端口释放;
    7. A等待2MSL后socket关闭,释放端口。

    从以上连接拆除过程我们可以看到:主动发送第一个FIN报文的一方会进入TIME_WAIT状态;进入TIME_WAIT状态的一方需要等待2MSL时间才会释放端口,在2MSL时间内,这个socket对应的四元组(源目IP、源目端口)处于冻结状态。

    TIME_WAIT状态的作用主要有两个:

    1. 避免拆链报文在链路中丢失造成连接关闭异常:在第6步,B没有收到ACK报文的时候会认为A没有收到FIN包,进而会重传第4步的FIN,如果这个时候没有TIME_WAIT状态,A侧socket已经关闭,A会针对B发送的FIN包响应RST,有可能导致B连接异常。
    2. 避免乱序到来的业务报文在新生成的socket连接中引发混乱:假设在拆链前有TCP报文由于中间网络传输原因导致在第7步完成之后才到达,如果没有TIME_WAIT状态而A和B又使用同样的4元组新建了一个新的socket,那么迷路的数据包就会进入到新的socket中进行处理,可能导致业务异常。

    通过TIME_WAIT状态可以很好的规避上面提到的两个问题,TIME_WAIT状态的老化时间是2MSL,MSL是最大分段生存时间,表示的是一个TCP分段可能在网络上存在的最大时间。2倍MSL的设计可以很好的满足报文在A、B之间一来一回的最大需要消耗的时间,最大程度上避免上述两个问题。在CentOS系统中,MSL的时间一般是30S。

    0X02

    下图抓包截图展示了一个完整的连接拆除又复用同样的端口新建连接的过程。

    在图中server 192.168.221.1运行Web服务,监听82端口,client 192.168.252.2 使用31387端口连接server(抓包截图从挥手前开始截取)。可以看到在编号为3的报文中服务器主动拆除连接,服务器和客户端交互完4个完整的挥手报文后,客户端立即使用相同的源端口和服务器的监听端口建立新的连接。下面逐报文对整个交互过程进行分析:

    1. server→client(PSH、ACK):服务器推送数据最后一个分段给客户端
    2. client→server(ACK):客户端对第1个报文进行接收确认
    3. server→client(FIN、ACK):服务器发送挥手报文给客户端协商断开连接,这是四次挥手的第一步。同时ACK标志位置位,由于第2个报文中没有载荷数据,所以ack值=第2个报文的seq。此时服务器进入FIN_WAIT_1状态
    4. client→server(ACK):客户端对接收到的FIN包进行响应,这是四次挥手的第二步。其中seq值不变,ack=第3个报文的seq+1(因为FIN报文在逻辑上占一个长度)。此时客户端进入CLOSE_WAIT状态,服务器收到ACK报文后进入FIN_WAIT_2状态
    5. client→server(FIN、ACK):客户端给服务器发送FIN包,这是四次挥手的第三步。其seq和ack的值和第4个报文相同。此时客户端进入LAST_ACK状态,等待服务器响应ACK报文后即可关闭连接
    6. server→client(ACK):服务器收到客户端发送的FIN包后会立即给客户端发送ACK包,这是四次挥手的最后一步。其中seq=第3个报文中的seq+1,ack=第5个报文中的seq+1。客户端收到ACK后会立即close该四元组对应的socket,而此时服务器在发送ACK后会进入TIME_WAIT状态,服务器侧对应的TCP四元组会被冻结2MSL
    7. client→server(SYN):客户端连接拆除后立即使用同一个源端口31387向服务器的82端口发起新的SYN连接握手报文
    8. server→client(ACK):通过seq和ack可以看出服务器重传第6个报文。由于服务器对应的四元组仍然在TIME_WAIT状态中,因此对于接受到的报文会认为是迷路的数据包或者客户端没有收到服务器发送的最后一个挥手的ACK报文,所以服务器重新向客户端发送该ACK报文
    9. client→server(RST):客户端向服务器发送一个RST报文,其中seq为server挥手ack包(第6和第8个报文)的ack值。这是因为对服务器侧而言,对应的四元组仍然处于TIME_WAIT状态,而客户端侧并不存在这个四元组的socket信息,客户端正准备使用这个四元组新建连接。这是前文为什么服务器突然回复RST——小心网络中的安全设备中TCP发送RST的第三种情况:TCP接收到一个数据段,但是这个数据段所标识的连接不存在。于是客户端使用ACK报文中的ack值作为seq,发送RST报文给服务器

    可以看到当客户端发送完RST后,客户端再次进行了SYN报文的重传,而此次即使仍然复用之前的四元组,客户端和服务器的TCP三次握手正常建立。这是因为当服务器收到RST报文后,无论处在TCP的哪个状态,都会立即进入close状态,进而服务器侧对应被TIME_WAIT状态冻结的四元组得以被释放,客户端侧的复用就成功了。

    0X03

    如上所述的业务场景是某应用系统使用反向代理地址连接后端服务器的抓包。服务器主动拆链+客户端立即复用源端口,这是一种危险的实现,如果客户端没有RST或者服务器端识别不了RST则很有可能在2MSL时间内,客户端使用被冻结的4元组进行连接建立的操作都会失败。对于服务器主动拆链的场景应该保证终端可用源端口尽可能的多,尽量避免立即端口复用的情况。此外对于服务器主动拆链的场景应该尽可能调短服务器的MSL时间,避免大量TIME_WAIT状态的连接存在影响服务器性能。

  • 相关阅读:
    RT throttling分析【转】
    linux异步IO的两种方式【转】
    linux select 与 阻塞( blocking ) 及非阻塞 (non blocking)实现io多路复用的示例【转】
    10. linux输入子系统/input 设备【转】
    Unix/Linux进程间通信(一):概述
    Linux进程间通信(九):数据报套接字 socket()、bind()、sendto()、recvfrom()、close()
    Linux进程间通信(八):流套接字 socket()、bind()、listen()、accept()、connect()、read()、write()、close()
    PHP函数 rtrim() 的一个怪异现象
    Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl()
    Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
  • 原文地址:https://www.cnblogs.com/yurang/p/12154453.html
Copyright © 2020-2023  润新知