[计算机网络]-TCP-连接管理

TCP 连接的建立与终止

一个 TCP 连接由一对 端点套接字 构成,其中通信的每一端都由一对 (IP 地址,端口号) 的二元组所唯一标识。一个 TCP 连接通常分为 启动数据传输(连接已建立)退出 三个时期

TCP 连接建立及终止的示意图如下:
连接建立及终止的示意图
连接状态示意图

连接建立

一开始,双方都处于 CLOSED 状态,先是服务端主动监听某个端口,处于 LISTEN 状态

  1. 主动开启方 (我们称为客户端) 发送一个 SYN 报文段 (SYN 位启用的报文段,用于初始化连接),指明 自己想要连接的端口号 和它的客户端 初始序列号(记为 ISNc)。之后客户端处于 SYN_SENT 状态
  2. 被动开启方(服务器)接收到客户端的 SYN 报文段后,发送自己的 SYN 报文段作为响应,并包含了它的 初始序列号 (记为 ISNS),此外,为了确认客户端的 SYN(报文段),他还需要发送对应的 ACK,其数值为 ISNc + 1 (SYN 报文段不能携带数据,但要消耗掉一个序列号),SYN 跟 ACK 一起发送。因此,每发送一个 SYN,序列号就会加一,这样如果出现丢失的情况,该 SYN 就会被重发。之后服务端处于 SYN_RCVD 状态
  3. 客户端为了确认服务器的 SYN 同样需要发送一个 ACK,数值为 ISNS + 1。ACK 报文段可以携带数据,如果不携带数据就不消耗序列号 (不携带数据称为 “纯 ACK”)。之后客户端处于 ESTABLISHED 状态。服务端接收到 ACK 后也进入 ESTABLISHED 状态

通过上述三个报文段就能完成一个连接的建立,通常称为 三次握手

为什么同样不携带数据,SYN 报文需要消耗一个序列号而 ACK 不需要

只要记住:需要对方确认的报文段都需要消耗序列号SYN 报文需要对方确认,所以需要消耗序列号;而发送 (不携带数据的) ACK不需要对方确认,所以不需要消耗序列号

初始序列号

在一个连接中,TCP 报文段在经过网络路由后可能会存在延迟抵达与排序混乱的情况,为了解决这一问题,需要仔细选择 初始序列号,然后序列号从初始序列号开始有规律地变化,对应着每个报文段原本的顺序,从而尽管会有报文段到达接收方时排序混乱的情况,也能根据序列号进行调整

初始序列号会随时间而改变 (注意,说的是初始序列号而不是序列号),因此 每一个连接都拥有不同的初始序列号初始序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加一,其目的在于 为一个连接的报文段安排序列号时防止出现与其它连接的序列号重叠

初始序列号除了需要 防止重叠 以外,还有一个是 安全性问题。试想一下,如果选择合适的序列号,IP 地址以及端口号,那么任何人都能伪造出一个 TCP 报文段。抵御这种行为的方法是使初始序列号变得相对难以猜出,另一种方法是加密

现代系统通常采用半随机的方法选择初始序列号
Linux 系统中,采用基于 时钟 的方案,并且针对每一个连接为时钟设置 随机的偏移量,随机偏移量是在 连接标识(即 四元组) 的基础上利用 加密散列函数 得到的,而 散列函数的输入 每隔 5 分钟就会改变一次,也就是说对于同一个连接的不同实例得到的随机偏移量也是不同的。在 32 位的初始序列号中,最高的 8 位是一个保密的序列号,而剩余的各位则由散列函数生成

为什么是三次握手不是两次/四次

>>> 为什么不能是两次?

主要原因是 为了防止已失效的连接请求报文突然又传送到了服务端导致服务端进入连接状态

假设只有两次握手。考虑这样一种情况,客户端发送的第一个连接请求报文段 并没有丢失,但是在某些网络节点长时间滞留,或者由于网络拥塞长时间未到达服务端。那么客户端会重发 SYN,然后重新建立起连接

假设第一个连接请求报文段延误,直到客户端跟服务端连接释放后的某个时间才到达服务端,那么本来这个请求报文已经 失效 了。但服务端收到后误以为是客户端的又一次新的连接请求,于是就向客户端返回 ACK 同意建立连接,并进入 ESTABLISHED 状态等待客户端发送数据,对服务端来说,新的连接就已经建立了,这样服务端就浪费了许多资源

服务端不能判断请求报文段是否有效,但客户端可以 (根据 ACK 的序列号/时间戳,如果要服务端来判断,那么服务端需要存储每个客户端上次连接的时间,明显不合理) 判断本次连接是否有效,所以需要让客户端判断 ACK 是否有效,有效才进行第三次握手,告知服务端可以建立连接

换句话说,只有两次握手的话,服务端在接收到 SYN 后就会进入 ESTABLISHED 状态,中间没有一个缓冲状态,不论 SYN 是否有效,这就可能会导致服务端浪费资源。解决办法就是在服务端建立连接之前阻止掉无效的历史连接,这就需要第三次握手来完成

其次,可以确保双方的序列号同步。客户端发送 SYN 到服务端,然后服务端响应了 ACK,就说明从客户端到服务端这个方向上的序列号成功同步了;同理,服务端发送 ACK 时也会发送自己的 SYN,然后客户端响应 ACK,就说明从服务端到客户端的这个方向上的序列号同步了,一共三步,才能确保了双方的序列号都被成功接收了

>>> 为什么不是四次?

其实在服务端发给客户端 ACK 以及 SYN 这里可以看成两步,即发送响应客户端 的 SYN 的 ACK,以及发送自己的 SYN,但可以合并为一步所以就合并了

像这样的三步已经理论上足够一个可靠连接的建立,所以不需要使用更多的通信次数了

第一次握手丢失会发生什么

第一次握手丢失,即客户端发出的连接建立超时,那么会触发 超时重传 机制,重传 SYN 报文,如果一直没有收到 ACK 应答,就会一直重传。重传的相隔时间是上一次重传相隔时间的两倍,这一行为被称为 指数回退,每次回退数值都是前一次数值的两倍

在 Linux 中,系统配置变量 net.ipv4.tcp_syn_retries 表示在一次主动打开连接的申请中尝试重新发送 SYN 报文段的最大次数;相应地,变量 net.ipv4.tcp_synack_retries 表示在响应对方的主动打开连接请求时尝试重新发送 SYN + ACK 的最大次数,它们默认数值为 5

第二次握手丢失会发生什么

第二次握手,包含了服务端对第一次握手,即客户端的 SYN 报文的确认 ACK;还包含了服务端发送给客户端的 SYN。所以需要从这两个方面分析

对客户端的 SYN 报文的 ACK 丢失了,就会触发客户端超时重传 SYN。其实对于客户端来说,第二次握手丢失触发的事件跟第一次握手丢失触发的事件是一样的

服务端的 SYN 丢失了,对服务端来说就相当于自己发出了SYN却没有得到对方的A CK,那么也会触发超时重传,重传 SYN + ACK 报文。SYN + ACK 报文最大重传次数由参数net.ipv4.tcp_synack_retries 决定

可以思考一下,第三次握手丢失会发生什么,其实第三次握手丢失,对于服务端来说,跟第二次握手是一样的,不管是第二次握手丢失,还是第三次丢失,对于服务端来说,他只知道自己没有收到对方的ACK,所以会重传 SYN + ACK (ACK 值与上次的 SYN + ACK 报文中的 ACK 值相等)

注意,虽然说的是重传 SYN + ACK 报文,但是ACK是不会重传的,当ACK丢失了,就由发送方重传对应的报文

连接终止

连接的任何一方都能够发起一个关闭操作。通常发起关闭连接的是客户端,然后一些服务器如Web服务器在对请求做出响应之后也会发起一个关闭操作。通常一个关闭操作是由应用程序提出关闭连接的请求而引发的 (如使用系统调用 close())

TCP 一端通过发送一个 FIN 报文段 (即 FIN 控制位置位的报文段) 来发起关闭操作,而且只有当连接双方都完成关闭操作后才构成一个完整的关闭

  1. 连接的主动关闭者,这里我们假设为客户端,发送一个 FIN 段指明接收端希望收到的序列,还包含了一个 ACK 由于确认对方最近一次发来的数据。之后客户端进入 FIN_WAIT_1 状态

  2. 被动关闭者 (服务端) 接收到对方的 FIN 报文段后需要发送一个 ACK 表明自己已经接收到它的FIN 报文,之后服务端进入 CLOSE_WAIT 状态

    此时上层的应用程序会被告知连接的另一端已经提出了关闭操作,通常 (TCP支持半关闭,见下文),这将导致应用程序发起自己的关闭操作。客户端接收到服务端的 ACK 应答报文后,进入 FIN_WAIT_2 状态

  3. 接着,服务端就会成为主动关闭者,然后向对方发出自己的 FIN 报文段,其序列号跟刚刚响应第一个 FIN 的 ACK 报文段的序列号相等,因为那个 ACK 报文段不消耗序列号。同时还要携带 与那个ACK报文段中的ACK字段值相等的ACK值,之后服务端进入 LAST_ACK 状态

  4. 客户端接收到服务端的关闭请求 FIN 报文段后,同样需要发送一个 ACK 报文段用于确认,之后进入 TIME_WAIT 状态。如果出现 FIN 报文丢失的情况,发送方将重新传输直到接收到一个ACK确认为止。服务端接收到 ACK 应答报文后,就进入 CLOSED 状态,至此,服务端已经完成连接的关闭。客户端在 TIME_WAIT 状态结束后自动进入 CLOSED 状态,至此客户端的关闭操作也完成了

综上所述,连接的关闭需要四个报文段,又称为 四次挥手。每一方关闭其到对方的连接都需要两个报文段,加起来一共四个报文。两次关闭操作在分析时可以对称地分析

第一次挥手丢失了会发生什么

当主动关闭方发送 FIN 报文段后,长时间没有收到对方的 ACK应答,也会触发超时重传机制;最大重发次数由 tcp_orphan_retries 参数控制。当重发次数达到 tcp_orphan_retries 后,且最后一次重发后在等待时间内也没有收到 ACK,就不再发送 FIN 报文,直接进入 CLOSED 状态

TCP 半关闭

可能会出现有的应用程序在自己的数据已经发送完成了,发送 FIN 关闭从自己到对方的连接,但它仍然希望能接收来自对方的数据,直至对方发送了 FIN,即只关闭自己到对方的连接而不关闭对方到自己的连接,这就是 半关闭

应用程序可以通过调用 shutdown() 来代替 close() 函数,就能实现上述操作

关于 FIN_WAIT_2 状态

FIN_WAIT_2 状态表示某通信端已主动发送一个 FIN 并已得到另一端的确认。从示意图中可以看出,在这种状态下,只有等对方发送了 FIN 并且这一端接收到了 FIN,这一端才会进入 TIME_WAIT 状态,这意味着这一端能够一直保持这种状态,对方也能一直保持 CLOSE_WAIT 状态,直到对方的应用程序决定关闭连接

为了防止连接处于这种可能无限等待的状态,如果主动关闭的应用程序执行的是一个 完全关闭 操作,而不是一个 半关闭,那么就会设置一个 计时器,如果当计时器超时时连接是空闲的,主动关闭的这一端就会从 FIN_WAIT_2 进入到 CLOSED 状态

在 Linux 中通过变量 net.ipv4.tcp_fin_timeout 来设置计时器的秒数,默认为 60s。这里说的半关闭操作其实就是 shutdown() 函数,net.ipv4.tcp_fin_timeout 参数无法控制调用 shutdown() 函数进行关闭连接的通信端,所以主动关闭的通信端调用的是 shutdown() 函数的话,在接收到对方的 FIN 之前,都会处于FIN_WAIT_2状态

关于 TIME_WAIT 状态

连接的主动关闭者在发出自己的 FIN,并接收到对方的 ACK 后,就会进入 FIN_WAIT_2 状态,等待对方的 FIN 报文。在接收到对方的 FIN 报文后,就会发送 ACK 给对方,并进入 TIME_WAIT 状态。这个状态下有一个 时间等待计时器,其设置的时间为 2MSL ,有时也称为 加倍等待,TCP需要等待这个时间,才会进入 CLOSED 状态

MSL, M a x i m u m   S e g m e n t   L i f e t i m e Maximum\ Segment\ Lifetime Maximum Segment Lifetime,最大段生命,代表任何报文段在被丢弃前在网络中被允许存在的最长时间。2MSL 在 Linux 中是通过一个宏定义 TCP_TIMEWAIT_LEN 设置的,默认是 60s,想要修改的话需要修改这个参数并重新编译内核。2MSL 的时间是从客户端收到 FIN 后发送 ACK 才开始计时的,也就是说,假设在 TIME_WAIT 时间内,由于客户端发送的 ACK 超时,导致了服务端重传 FIN,那么客户端再次接收到 FIN,并发送 ACK,计时器将重新计时

为什么需要 TIME_WAIT 状态

  1. 保证最后一个 ACK 能够到达对方,使其能正常关闭。假设客户端发给服务端的最后一个 ACK 丢失了,处于 LAST_ACK 状态的服务端就收不到客户端对自己发出的 FIN 的确认报文,那么它就会超时重传这个 FIN 报文,而客户端就能在 2MSL 时间内收到这个重传的 FIN,然后再次发送确认报文,重新启动 2MSL 计时器。假如客户端不在 TIME_WAIT 状态等待一段时间,而是发完最后一个 ACK 后立即释放连接,那么就无法收到服务端重传的 FIN 报文,也就不会再发送一次 ACK,服务端也就不能正常地进入 CLOSED 状态
    也正是考虑到这种 ACK 丢失然后服务端重传 FIN 的情况,客户端需要等待重传的 FIN 到达,一个 ACK 加上一个重传 FIN,两个报文,所以需要 2MSL 作为 TIME_WAIT 的时间
  2. 防止历史连接中的数据,被后面的连接接收。正如上面在连接建立部分讲到的为什么三次握手不能是两次握手的问题中的”已失效的连接请求报文“。客户端在发送最后一个ACK后再经过时间2MSL,就可以使本连接持续时间内所产生的所有报文段都从网络上消失,确保下一次连接中不会出现旧连接产生的报文段

TIME_WAIT 状态过多有什么危害

  1. 占用内存资源
  2. 占用本地端口资源。一个通信端的端口资源是有限的,一般可以开启的端口为 32768~61000,可以通过参数 net.ipv4.ip_local_port_range 来修改。如果发起连接方的 TIME_WAIT 状态过多,端口被占满,就无法创建新的连接;而对于服务端,虽然它可能只需要监听一个端口,然后不同的客户端访问这一个端口产生不同连接,也就是说它不太会因为 TCP 连接过多而导致端口资源受限,但连接过多也会占用系统资源,如 文件描述符,内存资源,CPU,线程 等等

如何优化 TIME_WAIT

  1. 设置 net.ipv4.tcp_tw_reuse 参数为1,表示可以复用处于 TIME_WAIT 的 socket 供新的连接使用。前提是需要打开时间戳选项的支持
  2. 修改 net.ipv4.tcp_max_tw_buckets 参数的值,该值表示当系统中处于 TIME_WAIT 状态的连接一旦超过这个数值时系统就会将后面的 TIME_WAIT 连接状态重置
  3. 设置 net.ipv4.tcp_tw_recycle 参数为 1,表示快速回收 TIME_WAIT 状态的连接。该参数不能轻易开启,参考资料
  4. 有一个 so_linger 选项,其为 linger 结构体类型,其中含有两个 int 型变量:
struct linger {
     int l_onoff;
     int l_linger;
};

当 l_onoff 为 0 时,表示整个选项关闭,l_linger 的值也会被忽略;
当 l_onoff 为 1,且 l_linger 为 0,那么主动关闭方调用 close() 函数后会立刻发送一个 RST 报文,所以连接会跳过四次挥手,自然也就跳过了 TIME_WAIT 状态;
当 l_linger 为非 0,连接会设置一个 超时 时间,在这段时间内可以发送缓冲区内残留的数据,如果时间到了之前所有数据都被发送且收到确认应答,那么内核就会用正常的四次挥手来关闭连接,否则就用 RST 报文的方式来关闭

TCP的状态转换图

TCP有限状态机

TCP的选项

部分选项
每一个选项的头一个字节为 “种类”,指明该选项的类型。种类值为 0 或 1 的选项仅占用 1 个字节。每种选项根据自己的种类确定自身的字节数 len,选项字段的总长度就包括种类与 len 个字节。上图中的长度指的是选项字段的总长度 (包括种类)

NOP ( N o   O p e r a t i o n No\ Operation No Operation) 选项的目的是允许发送者在必要的时候填充某个字段,使整个 TCP 头部长度达到 32bit 的倍数

EOL ( E n d   O f   O p t i o n   L i s t End\ Of\ Option\ List End Of Option List) 选项指出了选项列表的结尾,说明无需对选项列表再处理

最大段大小选项

最大段大小 (MSS, M a x i m u m   S e g m e n t   S i z e Maximum\ Segment\ Size Maximum Segment Size)是指 TCP 协议所允许的从对方接收到的最大报文段,也即通信双方发送数据时能够使用的最大报文段,最大段大小只记录 TCP 数据的字节数,不包括其它相关的 TCP 及 IP 头部。其默认数值为 536 字节

选择确认选项

由于接收的数据是无序的,所以接收到的数据的序列号也是不连续的,那么 TCP 接收方的数据队列中就会出现空洞的情况,如收到了序列号 1-3,6-7 的数据,那么就有一个空洞 4-5

而由于 TCP 提供的是字节流的服务,它要保证交付给应用程序的数据是有序的,需要防止应用程序使用超出空洞的数据
如果 TCP 发送方能够知道接收方当前的空洞情况,他就能在报文段丢失或者被接收方遗漏时更好地进行重传工作。这就是 选择确认 的功能

通过接收 SYN (或者 SYN + ACK) 中的 “允许选择确认” 选项,TCP 通信方就知道自己能发布 SACK 的信息。当接收到乱序的数据时,它就能提供一个 SACK 选项来描述这些乱序的数据。SACK 选项中包含了 接收方已经成功接收的数据块 的序列号范围,每个范围被称为一个 SACK 块,由 一对 32 位的序列号 表示。因此,一个 SACK 选项包含 n 个 SACK 块的话,其长度就为 8n + 2 字节,2 个字节用于保存 种类 和 长度。通过序列号范围就能得到空洞的范围

由于头部长度的限制,一个报文段中发送的最大 SACK 块数目为 3 (假设使用了时间戳选项,这是现代 TCP 实现中的典型情况)

窗口缩放选项

窗口缩放选项 (WSCALE 或 WSOPT, W i n d o w   S c a l e   O p t i o n Window\ Scale\ Option Window Scale Option) 能够有效地 将 TCP 窗口广告字段的范围从 16 位增加至 30 位。选项总长度为 3 字节,其中一个字节表示种类,一个字节表示长度,剩下一个字节就是窗口缩放的 比例因子

假设比例因子设值为 s,那么窗口数值就会扩大到原先的 2s 倍。原本的窗口大小最大值为 65535 (216 - 1),比例因子的最大值允许是 14,也就是说窗口的实际最大值是 65535 * 214,这个数值接近 1 GB。RFC 7323 中提到,如果一个通信端接收到了一个大于 14 的字段值,他也只能使用 14

该选项只能出现于一个 SYN 报文段中,因此当连接建立后比例因子是与方向绑定的,要调整窗口大小的话就通过修改窗口大小字段的值来调整

为了保证窗口调整,通信双方都需要在 SYN 报文段中包含该选项,当然,这还是由通信端自己选择包不包含,如果双方有一方没有包含该选项,那么双方都不会启用这个选项。主动打开连接的一方发送了 SYN,而被动打开的一方只有在收到的 SYN 报文中指出了该选项自己才能发送该选项;而如果主动打开连接的一方发送了 SYN,且包含了该选项,但是没有接收到来自对方的窗口缩放选项,它会将自己发送与接收的比例因子数值都设为 0

时间戳选项与防回绕序列号

时间戳选项 (TSOPT 或 TSopt)的示意图如下:

时间戳选项格式
时间戳选项要求发送方在每一个报文段中添加 2 个 4 字节的时间戳数值,其总长度为 10 字节 (还有 1 个字节为种类,1 个字节为长度),接收方将会在确认 ACK 报文中反映 (回显) 这些数值,允许发送方针对每一个接收到的 ACK (注意不是每一个报文,因为 TCP 采用的是累积确认) 估算 TCP 连接的往返时间

发送方将一个 32 位的数值填充到时间戳数值字段 (TSV / TSval) 作为时间戳选项的第一个部分;接收方则将收到的时间戳数值原封不动地填充到第二部分的 时间戳回显重试 字段 (TSER / TSecr),然后在第一部分处填充自己的时间戳数值

估算一条 TCP 连接的往返时间 主要是为了设置重传时间,时间戳选项使我们获得了更多的往返时间样本,从而提升了精确估算往返时间的能力

除此之外,时间戳选项 也为接收者提供了避免接收旧报文段与判断报文段正确性的方法,即 防回绕序列号 ( P r o t e c t i o n   A g a i n s t   W r a p p e d   S e q u e n c e   n u m b e r s Protection\ Against\ Wrapped\ Sequence\ numbers Protection Against Wrapped Sequence numbers,PAWS)
试想一下,一个 相对高速的连接 中,序列号即使有 232 个,也会很快就循环完一轮,假设当前这一轮中有某些序列号对应的数据丢失了,然后重传,但是在序列号循环的第二轮才出现,接收方只根据序列号的话是很难区分这些数据是最近发送的还是以前发送的

而有了时间戳选项。就可以判断报文段的时间戳是否小于最近接收到的报文段的时间戳,是的话防回绕序列号算法就将其丢弃

防回绕序列号算法并不要求在发送者跟接收者之间有任何形式的时钟同步,接收者所需要的是保证时间戳数值单调递增,并且每一个窗口的数据至少增加 1

可以看出来,时间戳选项对于前面我们提到的 “来自历史连接的报文段” 也可以进行处理,判断出其是来自历史连接还是本次连接
将参数 net.ipv4.tcp_timestamps 的值设为1就可以开启时间戳选项的支持。默认即为 1

用户超时选项

用户超时选项数值 ( U S E R _ T I M E O U T USER\_TIMEOUT USER_TIMEOUT) 指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间

认证选项

TCP认证选项 ( T C P   A u t h e n t i c a t i o n   O p t i o n TCP\ Authentication\ Option TCP Authentication Option,TCP-AO) 是用于增强连接的安全性的。它使用了一种加密散列算法以及TCP连接双方共同维护的一个秘密值来认证每一个报文段。当发送数据时,TCP会根据共享密钥生成一个通信密钥,并根据一个特殊的算法计算散列值,接收者装配有相同的密钥,同样也能够生成通信密钥,借助通信密钥接收者就可以确认到达的报文段是否在传输过程中被篡改过。由于需要创建并分发一个共享密钥,该选项并没有得到广泛使用
RFC 5925中提到,每个连接的通信密钥跟连接本身一样唯一,即一个连接 (实例) 只会有一个通信密钥

重置报文段

一个将 RST 控制位置位的报文段即称为 重置报文段。当发现一个到达的报文段对于相关连接而言是不正确的时,TCP 就会发送一个重置报文段

产生报文段的场景有以下几种:

  1. 当一个连接请求 SYN 到达本地却没有相关进程在目的端口监听时就会产生一个重置报文段。重置报文段中的 ACK 位必须被置位,对于响应一个连接请求的 SYN 报文段,由于 SYN 报文段不携带数据,但其本身会占用一个字节的序列号,所以返回的重置报文段中的 ACK 数值应为 SYN 报文中的初始序列号加上数据长度 0 以及 SYN 占用的 1 字节

    ACK 字段的数值必须在正确窗口的范围内,这样有助于防止一种简单的 攻击,在这种攻击中任何人都能生成一个与相应连接匹配的重置报文段从而扰乱这个连接,但伪造的重置报文段很难做到 ACK 数值在正确窗口的范围内

  2. 终止一条连接。我们称通过通信一方发送 FIN 来终止连接的方法为 有序释放;而在任何时刻,都可以通过发送一个重置报文段替代 FIN 来终止一条连接,这种方式又称为 终止释放

    终止一条连接可以为应用程序带来两大特性:任何排队的数据都将被丢弃,一个重置报文段会被立即发送出去;重置报文段的接收方会说明通信另一端采用了终止的方式而不是一次正常关闭。API 必须提供一种实现上述终止行为的方式来取代正常的关闭操作
    套接字API通过将 逗留于关闭 套接字选项 SO_LINGER 数值设为 0 实现上述功能,这意味着 不会在终止之前为了确定数据是否到达另一端而逗留任何时间,那么四次挥手将会被跳过,TIME_WAIT 状态也会被跳过,这也是优化 TIME_WAIT 状态的一种手段

    还需要注意的是重置报文段不会被确认

  3. 半开连接。如果在未告知另一端的情况下通信的一端关闭或终止连接,那么就认为该条连接处于 半开 状态。例如,当一个连接中服务端崩溃,连接被切断,留给客户端一个半开连接,当服务端重启后,客户端尝试发送数据到服务端,服务端会对这条连接一无所知,此时,TCP 规定接收者将回复一个重置报文段作为响应

  4. 时间等待错误。处于 TIME_WAIT 状态的通信端通常不需要做任何操作,只需要维持着当前状态直到 2MSL 计时结束,而如果它在这段时间内接收到来自这条连接的一些报文段,或是更加特殊的重置报文段,它就会被破坏,这种情况称为 时间等待错误 ( T I M E − W A I T   A s s a s s i n a t i o n TIME-WAIT\ Assassination TIMEWAIT Assassination,TWA)

    当客户端主动发起连接并且四次挥手完成后,客户端将处于 TIME_WAIT 状态,而服务端已处于 CLOSED 状态,此时有可能客户端会接收到本次连接产生的旧的报文段 (到达时间比较晚),那么他就会发送一个 ACK 作为响应。然而,当服务端接收到这个报文段之后,它却没有关于这条连接的任何信息,因此它发送一个重置报文段作为响应,这不是服务端的问题,但却会使客户端过早地从 TIME_WAIT 状态转移到 CLOSED 状态

    许多系统规定,当处于 TIME_WAIT 状态时不对重置报文段作出反应,从而避免了上述问题

半连接队列和全连接队列

服务端接收到客户端的 SYN 请求后,内核会把连接放到 半连接队列 (也称 SYN 队列) 中,然后向客户端响应 SYN + ACK 报文,等待客户端返回 ACK,即第三次握手后,再把连接从半连接队列中移出,创建完整的连接添加到 全连接队列 (也称 accept 队列) 中,等待进程调用 accept() 时再把连接取出来

全连接队列中连接数目的最大值 为内核参数 net.core.somaxconn (默认为 128) 与 listen() 函数中 backlog 参数两者的较小值
全连接队列已满,服务端自然就无法接受一个新的连接。系统控制变量 net.ipv4.tcp_abort_on_overflow 可以设置服务端的行为:
当其值为 0,服务端只会忽略客户端的 ACK;
当其值为 1,服务端还会发送一个重置报文段给客户端,默认情况下这个功能是不开启的。因为此时客户端处于的是 ESTABLISHED 状态,它会尝试与服务器联系,发出请求。如果它收到了重置报文段,那它可能会认为没有服务器存在,但实际上服务器是因为繁忙而不是不存在,繁忙跟不存在完全不一样。假设服务端繁忙的状况能够在短时间内得到改善,那么它是有机会去接受刚才繁忙时没能接受的连接的 (只要客户端有在继续尝试联系,因为请求报文会携带 ACK,能触发服务端继续完成连接) ;而如果直接发送了重置报文,那么客户端也会放弃主动打开的操作,就算服务端短时间内不繁忙了,也不能再来接受这个连接了
所以只有当肯定全连接队列会长期溢出时,才应该将 net.ipv4.tcp_abort_on_overflow 参数设为 1 以尽快通知客户端不再尝试连接,节省其资源

如果 半连接队列已满,不一定只能丢弃连接,在开启 syncookies 功能的情况下就可以不使用 SYN 半连接队列而完成连接的建立。半连接队列 的最大数目不止与参数 net.ipv4.tcp_max_syn_backlog (默认为 1000) 有关,还跟全连接队列的最大数目有关

与 TCP 连接管理相关的攻击

SYN 泛洪

SYN 泛洪 是一种 TCP 拒绝服务攻击,指一个或多个恶意的客户端产生一系列 TCP 连接尝试 ,即 SYN 报文段),并将它们发送给一台服务器,服务器会为每一条连接分配一定数量的连接资源,由于连接尚未完全建立 (此时 服务端响应了这些 SYN 并发送自己的 SYN + ACK 报文段,处于 SYN_RCVD 状态并等待对方的 ACK,但 恶意的客户端并不会继续响应 ACK,即第三次握手),服务端为了维护大量的半打开连接会在耗尽自身内存后拒绝为后续的合法连接请求服务

一种针对这种问题的机制为 SYN cookies。其主要思想是,当一个 SYN 到达时,这条连接存储的大部分信息都会被编码并保存在 SYN + ACK 报文段的序列号字段。采用 SYN cookies 的目标主机并不需要为进入的连接请求分配任何存储资源,只有当 SYN + ACK 本身被确认后 (并且已返回初始序列号) 才会分配真正的内存,在这种情况下,所有重要的连接参数都能重新获得,同时连接也能够被设置为 ESTABLISHED 状态

该功能通过 net.ipv4.tcp_syncookies 参数设定,当其值为 0 时表示不开启,为 1 时表示仅当 SYN 队列已满时再开启,为 2 时表示无条件开启该功能

Linux 中,服务端在接收到一个 SYN 后会采用下面的方法设置初始序列号然后保存在 SYN + ACK 报文段中):首 5 位是 t 模 32 的结果,其中 t 是一个 32 位的计数器,每隔 64 秒增 1;接着 3 位是对服务器最大段大小的编码值;剩余的 24 位保存了四元组 (即连接标识) 与 t 值的散列值,该值是根据服务器选定的散列加密算法计算得到的

在采用该方法时,服务端总是以一个 SYN + ACK 报文段作为响应,在接收到第三次握手的 ACK 后,如果根据其中的 t 值可以计算出与加密的散列值相同的结果,那么服务器才会为该 SYN 重新构建队列

除此之外还可以通过 减少 SYN + ACK 的重传次数 来抵御 SYN 泛洪,即减小参数 net.ipv4.tcp_synack_retries 的值,这样 SYN + 重传次数减少了,处于SYN_RCVD状态的连接也就可以更快被断开

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值