从 TCP 三次握手说起 -- 浅析 TCP 协议中的疑难杂症(1)

阅读数:1821 2019 年 8 月 22 日 17:58

从TCP三次握手说起--浅析TCP协议中的疑难杂症(1)

说到 TCP 协议,相信大家都比较熟悉了,对于 TCP 协议总能说个一二三来,但是 TCP 协议又是一个非常复杂的协议,其中有不少细节点让人头疼点。本文就是来说说这些头疼点的,浅谈一些 TCP 的疑难杂症。那么从哪说起呢?当然是从三次握手和四次挥手说起啦,可能大家都知道 TCP 是三次交互完成连接的建立,四次交互来断开一个连接,那为什么是三次握手和四次挥手呢?反过来不行吗?

1. 疑症 (1)TCP 的三次握手、四次挥手

下面两图大家再熟悉不过了,TCP 的三次握手和四次挥手见下面左边的”TCP 建立连接”、”TCP 数据传送”、”TCP 断开连接”时序图和右边的”TCP 协议状态机”

从TCP三次握手说起--浅析TCP协议中的疑难杂症(1)

TCP 三次握手、四次挥手时序图

从TCP三次握手说起--浅析TCP协议中的疑难杂症(1)

TCP 协议状态机

要弄清 TCP 建立连接需要几次交互才行,我们需要弄清建立连接进行初始化的目标是什么。TCP 进行握手初始化一个连接的目标是:分配资源、初始化序列号 (通知 peer 对端我的初始序列号是多少),知道初始化连接的目标,那么要达成这个目标的过程就简单了,握手过程可以简化为下面的四次交互:

1)client 端首先发送一个 SYN 包告诉 Server 端我的初始序列号是 X。 2)Server 端收到 SYN 包后回复给 client 一个 ACK 确认包,告诉 client 说我收到了。
3)接着 Server 端也需要告诉 client 端自己的初始序列号,于是 Server 也发送一个 SYN 包告诉 client 我的初始序列号是 Y。
4)Client 收到后,回复 Server 一个 ACK 确认包说我知道了。
整个过程 4 次交互即可完成初始化,但是,细心的同学会发现两个问题:[1]. Server 发送 SYN 包是作为发起连接的 SYN 包,还是作为响应发起者的 SYN 包呢?怎么区分?比较容易引起混淆 [2}.Server 的 ACK 确认包和接下来的 SYN 包可以合成一个 SYN ACK 包一起发送的,没必要分别单独发送,这样省了一次交互同时也解决了问题 [1]. 这样 TCP 建立一个连接,三次握手在进行最少次交互的情况下完成了 Peer 两端的资源分配和初始化序列号的交换。

大部分情况下建立连接需要三次握手,也不一定都是三次,有可能出现四次握手来建立连接的。如下图,当 Peer 两端同时发起 SYN 来建立连接的时候,就出现了四次握手来建立连接 (对于有些 TCP/IP 的实现,可能不支持这种同时打开的情况)。

从TCP三次握手说起--浅析TCP协议中的疑难杂症(1)

在三次握手过程中,细心的同学可能会有以下疑问:

(1). 初始化序列号 X、Y 是可以是写死固定的吗,为什么不能呢?
(2). 假如 Client 发送一个 SYN 包给 Server 后就挂了或是不管了,这个时候这个连接处于什么状态呢?会超时吗?为什么呢?
TCP 进行断开连接的目标是:回收资源、终止数据传输。由于 TCP 是全双工的,需要 Peer 两端分别各自拆除自己通向 Peer 对端的方向的通信信道。这样需要四次挥手来分别拆除通信信道,就比较清晰明了了。

1)Client 发送一个 FIN 包来告诉 Server 我已经没数据需要发给 Server 了。
2)Server 收到后回复一个 ACK 确认包说我知道了。
3)然后 server 在自己也没数据发送给 client 后,Server 也发送一个 FIN 包给 Client 告诉 Client 我也已经没数据发给 client 了。
4)Client 收到后,就会回复一个 ACK 确认包说我知道了。

到此,四次挥手,这个 TCP 连接就可以完全拆除了。在四次挥手的过程中,细心的同学可能会有以下疑问:

(3). Client 和 Server 同时发起断开连接的 FIN 包会怎么样呢,TCP 状态是怎么转移的?
(4). 左侧图中的四次挥手过程中,Server 端的 ACK 确认包能不能和接下来的 FIN 包合并成一个包呢,这样四次挥手就变成三次挥手了。
(5). 四次挥手过程中,首先断开连接的一端,在回复最后一个 ACK 后,为什么要进行 TIME_WAIT 呢 (超时设置是 2*MSL,RFC793 定义了 MSL 为 2 分钟,Linux 设置成了 30s),在 TIME_WAIT 的时候又不能释放资源,白白让资源占用那么长时间,能不能省了 TIME_WAIT 呢,为什么?

2. 疑症(2),TCP 连接的初始化序列号能否固定

如果初始化序列号(缩写为 ISN:Inital Sequence Number)可以固定,我们来看看会出现什么问题。假设 ISN 固定是 1,Client 和 Server 建立好一条 TCP 连接后,Client 连续给 Server 发了 10 个包,这 10 个包不知怎么被链路上的路由器缓存了 (路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧 Client 挂掉了,然后 Client 用同样的端口号重新连上 Server,Client 又连续给 Server 发了几个包,假设这个时候 Client 的序列号变成了 5。接着,之前被路由器缓存的 10 个数据包全部被路由到 Server 端了,Server 给 Client 回复确认号 10,这个时候,Client 整个都不好了,这是什么情况?我的序列号才到 5,你怎么给我的确认号是 10 了,整个都乱了。

RFC793 中,建议 ISN 和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始,这需要 4 小时才会产生 ISN 的回绕问题,这几乎可以保证每个新连接的 ISN 不会和旧的连接的 ISN 产生冲突。这种递增方式的 ISN,很容易让攻击者猜测到 TCP 连接的 ISN,现在的实现大多是在一个基准值的基础上进行随机的。

3. 疑症(3),初始化连接的 SYN 超时问题

Client 发送 SYN 包给 Server 后挂了,Server 回给 Client 的 SYN-ACK 一直没收到 Client 的 ACK 确认,这个时候这个连接既没建立起来,也不能算失败。这就需要一个超时时间让 Server 将这个连接断开,否则这个连接就会一直占用 Server 的 SYN 连接队列中的一个位置,大量这样的连接就会将 Server 的 SYN 连接队列耗尽,让正常的连接无法得到处理。目前,Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP 才会把断开这个连接。由于,SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server(俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。

4. 疑症(4) ,TCP 的 Peer 两端同时断开连接

由上面的”TCP 协议状态机 “图可以看出,TCP 的 Peer 端在收到对端的 FIN 包前发出了 FIN 包,那么该 Peer 的状态就变成了 FIN_WAIT1,Peer 在 FIN_WAIT1 状态下收到对端 Peer 对自己 FIN 包的 ACK 包的话,那么 Peer 状态就变成 FIN_WAIT2,Peer 在 FIN_WAIT2 下收到对端 Peer 的 FIN 包,在确认已经收到了对端 Peer 全部的 Data 数据包后,就响应一个 ACK 给对端 Peer,然后自己进入 TIME_WAIT 状态;但是如果 Peer 在 FIN_WAIT1 状态下首先收到对端 Peer 的 FIN 包的话,那么该 Peer 在确认已经收到了对端 Peer 全部的 Data 数据包后,就响应一个 ACK 给对端 Peer,然后自己进入 CLOSEING 状态,Peer 在 CLOSEING 状态下收到自己的 FIN 包的 ACK 包的话,那么就进入 TIME WAIT 状态。于是,TCP 的 Peer 两端同时发起 FIN 包进行断开连接,那么两端 Peer 可能出现完全一样的状态转移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就会 Client 和 Server 最后同时进入 TIME_WAIT 状态。同时关闭连接的状态转移如下图所示:

从TCP三次握手说起--浅析TCP协议中的疑难杂症(1)

5. 疑症(5)四次挥手能不能变成三次挥手呢??

答案是可能的。TCP 是全双工通信,Cliet 在自己已经不会在有新的数据要发送给 Server 后,可以发送 FIN 信号告知 Server,这边已经终止 Client 到对端 Server 那边的数据传输。但是,这个时候对端 Server 可以继续往 Client 这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要 2+2 = 4 次挥手来完全终止这个连接。但是,如果 Server 在收到 Client 的 FIN 包后,在也没数据需要发送给 Client 了,那么对 Client 的 ACK 包和 Server 自己的 FIN 包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了 (似乎 linux 协议栈就是这样实现的)

6. 疑症(6) TCP 的头号疼症 TIME_WAIT 状态

要说明 TIME_WAIT 的问题,需要解答以下几个问题

一、Peer 两端,哪一端会进入 TIME_WAIT 呢?为什么?

相信大家都知道,TCP 主动关闭连接的那一方会最后进入 TIME_WAIT。那么怎么界定主动关闭方呢?是否主动关闭是由 FIN 包的先后决定的,就是在自己没收到对端 Peer 的 FIN 包之前自己发出了 FIN 包,那么自己就是主动关闭连接的那一方。对于疑症(4) 中描述的情况,那么 Peer 两边都是主动关闭的一方,两边都会进入 TIME_WAIT。为什么是主动关闭的一方进行 TIME_WAIT 呢,被动关闭的进入 TIME_WAIT 可以不呢?我们来看看 TCP 四次挥手可以简单分为下面三个过程

过程一. 主动关闭方发送 FIN;
过程二. 被动关闭方收到主动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方发送 FIN;
过程三. 主动关闭方收到被动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方等待自己 FIN 的 ACK

问题就在过程三中,据 TCP 协议规范,不对 ACK 进行 ACK,如果主动关闭方不进入 TIME_WAIT,那么主动关闭方在发送完 ACK 就走了的话,如果最后发送的 ACK 在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己 FIN 的 ACK 就不能关闭连接,接着被动关闭方会超时重发 FIN 包,但是这个时候已经没有对端会给该 FIN 回 ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 的 ACK。

二、TIME_WAIT 状态是用来解决或避免什么问题呢?

TIME_WAIT 主要是用来解决以下几个问题:

1)上面解释为什么主动关闭方需要进入 TIME_WAIT 状态中提到的: 主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 包的 ACK。如果主动关闭方不进入 TIME_WAIT,那么在主动关闭方对被动关闭方 FIN 包的 ACK 丢失了的时候,被动关闭方由于没收到自己 FIN 的 ACK,会进行重传 FIN 包,这个 FIN 包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个 FIN 包,协议栈会认为对方疯了,都还没建立连接你给我来个 FIN 包?,于是回复一个 RST 包给被动关闭方,被动关闭方就会收到一个错误 (我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到 RST 包的时候,还往这个连接写数据,就会收到 Broken pipe 错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。
2)防止已经断开的连接 1 中在链路中残留的 FIN 包终止掉新的连接 2(重用了连接 1 的所有的 5 元素 (源 IP,目的 IP,TCP,源端口,目的端口)),这个概率比较低,因为涉及到一个匹配问题,迟到的 FIN 分段的序列号必须落在连接 2 的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是 32 位的,会回绕。
3)防止链路上已经关闭的连接的残余数据包 (a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。这个问题和 2)类似。

三、TIME_WAIT 会带来哪些问题呢?

TIME_WAIT 带来的问题注意是源于:一个连接进入 TIME_WAIT 状态后需要等待 2*MSL(一般是 1 到 4 分钟) 那么长的时间才能断开连接释放连接占用的资源,会造成以下问题

1) 作为服务器,短时间内关闭了大量的 Client 连接,就会造成服务器上出现大量的 TIME_WAIT 连接,占据大量的 tuple,严重消耗着服务器的资源。
2) 作为客户端,短时间内大量的短连接,会大量消耗的 Client 机器的端口,毕竟端口只有 65535 个,端口被耗尽了,后续就无法在发起新的连接了。
(由于上面两个问题,作为客户端需要连本机的一个服务的时候,首选 UNIX 域套接字而不是 TCP)

TIME_WAIT 很令人头疼,很多问题是由 TIME_WAIT 造成的,但是 TIME_WAIT 又不是多余的不能简单将 TIME_WAIT 去掉,那么怎么来解决或缓解 TIME_WAIT 问题呢?可以进行 TIME_WAIT 的快速回收和重用来缓解 TIME_WAIT 的问题。有没一些清掉 TIME_WAIT 的技巧呢?

本文转载自公众号小时光茶舍(ID:gh_7322a0f167b5)。

原文链接:

https://mp.weixin.qq.com/s/18OXZ2Jabgx5aWo_kzgABw

评论

发布