网络:TCP的三次握手

TCP三次握手(面试题)

在 HTTP 传输数据之前,首先需要 TCP 建立连接,TCP 连接的建立,通常称为三次握手

这个所谓的「连接」,只是双方计算机里维护一个状态机,在连接建立的过程中,双方的状态变化时序图如下

  • 一开始,客户端和服务端都处于 CLOSED状态。
  • 服务器必须准备好接受外来的连接, 主动监听某个端口。这通常通过socket、bind、listen这3个函数来完成,我们称之为被动打开, 此时服务端处LISTEN状态。

然后:

  • 第一次握手:客户端调用connect,给服务端发一个SYN报文,并指明客户端的初始化序列号ISN。此时客户端进于SYN_SNED状态
  • 第二次握手:服务端收到客户端的SYN报文后,会以自己的SYN报文作为应用,并且也指明了自己的初始化序列号ISN,同时会把客户端的ISN + 1作为ACK的值,此时服务端处于SYN_RECV的状态
  • 第三次握手:客户端收到SYN报文后,会发送一个ACK报文,同时,也把服务端的ISN+1作为ACK的值,此时客户端进入established状态
  • 服务收到ACK报文之后,也进入established状态。此时,双方建立了连接。

三次握手的作用

  • 确认双方的接受能力,发送能力是否正常
  • 指定自己的初始化序列号,为后面的可靠传输做准备

ISN是固定的吗?

  • 三从握手的一个重要功能是客户端和服务端交换ISN,以便让对方知道接下来接收数据的时候如何按照序列化组织数据
  • 如果ISN是固定的,攻击者很容易猜出后继的确认号,因此ISN是动态生成的

什么是半连接队列

  • 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
  • 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

网络:TCP的三次握手_第1张图片

关于SYN-ACK重传次数(连接时SYN超时):服务器发送完SYN-ACK包,如果未收到客户端确认包,服务端进行首次重传,等待一段时间仍未收到客户端确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s,

三次握手过程中可以携带数据吗

第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

第1/2次:如果携带数据,容易被攻击(这个时候不知道连接是不是可信的)

第一次握手丢失了,会怎么样?

也就是客户端发送了SYN(第一次握手),但是没有收到服务端的ACK-SYN(第二次握手)

此时,客户端处于SYN_RECV状态,如果一直没有收到SYN-ACK,就会触发[超时重传]机制,重传SYN报文,而且重传的SYN报文的序列号是一样的

不同版本的操作系统可能超时时间不同,有1s的,也有3s的,这个超时时间是写死在内核中的,如果需要更改则需要重新编译内核,比较麻烦。

那么客户端会重发几次SYN报文呢?

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

# cat /proc/sys/net/ipv4/tcp_syn_retries
5

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍

当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。

所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。

也就是说每次超时时间是上一次的2倍,达到最大重传次数之后,会再等待一段时间(时间为上一次超时时间的 2 倍)如果还是没有收到,那么就断开连接

第二次握手丢失了,会怎么样

也就是服务端发送的SYN_ACK不能到达客户端,此时服务端处于SYN_RCVD状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的ACK,是对第一次握手的确认报文
  • 第二次握手里的SYN,是服务端发起建立TCP连接的报文

如果第二次握手丢失了,那么:

  • 客户端收不到SYN-ACK(第二次握手),会认为自己的SYN(第一次握手)丢失了,那么就会超时重传,重传SYN报文,最大重传次数由 tcp_syn_retries内核参数决定;
  • 服务端收不到ACK-ACK(第三次握手),会认为自己的ACK-SYN丢失了,也会超时重传,重传SYN-ACK报文,最大重传次数由 tcp_synack_retries 内核参数决定。

第三次握手丢失了,会怎么样?

此时客户端出于ESTABLISH 状态,服务端处于SYN_RCVD状态。

此时:

  • 服务端收不到第三次握手的ACK,会任务自己的SYN-ACK丢失了,于是就超时重传,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传机制
  • 注意,客户端并不会重传ACK,ACK报文是不会有重传的,当ACK丢失了,就由对方重传对应的报文

初始序列号 ISN 与MSL

RFC793中规定:

  • ISN会和一个时钟始终绑定在一起,这个时钟每4us会对ISN做加一操作,直到超过2^32,又从0开始,这样,一个ISN的周期大概是4.55小时。
  • 因此,我们规定TCP Segment在网络上的存活时间不会超过MSL(RFC793定义了MSL为2分钟,Linux设置成了30s),所以,只要MSL的值小于4.55小时,我们就不会重用到ISN

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节
  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

如果在TCP的整个报文(头部+数据)交给IP层进行分片,会有什么异常呢?

当IP层有一个超过MTU大小的数据要发送,那么IP层就会分片,把数据分成若干片,保证每一片都小于MTU,把一份IP数据报进行分片之后,由目标主机的IP层来进行重新组装之后,再交给上一层TCP传输层

隐患是如果一个IP分片丢失,那么整个IP报文的所有分配都会重传,因为IP层本身没有超时重传机制,它由传输层的TCP来负责超时和重传

当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的 TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能TCP协议在建立连接时通常需要协商双方的MSS值,当TCP层发送数据超过MSS时,则就会先进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

在这里插入图片描述
经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

网络:TCP的三次握手_第2张图片

什么是 SYN 攻击?如何避免 SYN 攻击?

现象

假如攻击者短时间内伪造不同IP地址的SYN报文,服务端每接收到一个SYN报文,就会进入SYN_RECV状态,但是服务端收不到第三次握手的回文(服务器就需要默认等待63s才断开连接),这会导致半连接队列被占满,当TCP半连接队列满了,SYN报文就会被丢弃,导致客户端无法再和服务端建立连接

怎么解决

  • 调整netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 减少 SYN+ACK 重传次数
  • 开启 tcp_syncookies,
  • 改变tcp_abort_on_overflow ,处理不过来干脆就直接拒绝连接了。

(1)方式一:调大 netdev_max_backlog

  • 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
net.core.netdev_max_backlog = 10000

(2)方式二:增大 TCP 半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:

  • 增大 net.ipv4.tcp_max_syn_backlog
  • 增大 listen() 函数中的 backlog
  • 增大 net.core.somaxconn

要想增大半连接队列,我们得知不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn 和 backlog,也就是增大全连接队列。否则,只单纯增大 tcp_max_syn_backlog 是无效的。

  • 增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 内核参数:
  • 增大 backlog 的方式,每个 Web 服务都不同

(3)方式三:开启 net.ipv4.tcp_syncookies

  • 开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
    • 当SYN队列满了之后,TCP会通过源地址端口、目标地址端口和时间长打造出一个特别的Sequence Numbe发回去(又叫cookie)
    • 如果是攻击者就不会有响应,如果是正常连接,则会把这个SYN Cookie发回来,然后服务器就可以通过cookie建立连接(即使你不在SYNC队列中)
  • net.ipv4.tcp_syncookies 参数主要有以下三个值:
    • 0 值,表示关闭该功能;
    • 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;(在应对 SYN 攻击时,只需要设置为 1 即可:echo 1 > /proc/sys/net/ipv4/tcp_syncookies)
    • 2 值,表示无条件开启功能;
  • 请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。
  • 因为,synccookies是妥协版的TCP协议,并不严谨。

网络:TCP的三次握手_第3张图片

(4)方式四:减少 SYN+ACK 重传次数

  • 当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。
  • 那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。

SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:

$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries

如何查看 TCP 的连接状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看

网络:TCP的三次握手_第4张图片

已建立连接的TCP,收到SYN会发生什么?

问题

  • 个已经建立的 TCP 连接,客户端中途宕机了,而服务端此时也没有数据要发送,一直处于 establish 状态,客户端恢复后,向服务端建立连接,此时服务端会怎么处理?

回答

这个问题要看客户端发送的SYN报文中的源端口是否与上一次连接的源端口相同。

(1)SYN报文中的端口号与历史连接不同

  • 此时服务端会认为有新的连接要建立,于是会重新三次握手
  • 那么旧连接里的 establish 状态的服务端最后会怎么样呢?
    • 如果服务端发送了数据给客户端,由于客户端连接已经关闭了,此时客户端的内核会回传RST报文,服务端收到后就会释放连接
    • 如果服务端一直没有发送数据给客户端,在超时一段时间后,TCP保活机制会重启,检测到客户端没有存活后,服务端会释放连接

(2)SYN报文中的端口号与历史连接相同

  • 此时,服务端收到SYN报文后,此时SYN报文是乱序的(因为SYN初始ISN是随机数),会回复一个携带了正确序列号和确认号的ACK
  • 然后,客户端接收到这个ACK之后,发现序列号不是自己想要的,于是会回复RST,服务端收到之后,就会释放该连接

SYN 报文什么时候情况下会被丢弃?

工作场景

  • 客户端向服务端发起了连接,但是连接并没有建立起来,通过抓包分析发现,服务端是收到 SYN 报文了,但是并没有回复 SYN+ACK(TCP 第二次握手),说明 SYN 报文被服务端忽略了,然后客户端就一直在超时重传 SYN 报文,直到达到最大的重传次数。

那么,什么时候SYN报文会被丢弃呢?

  • 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃
  • TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃

分析

(1)tcp_tw_recycle

  • TCP 四次挥手过程中,主动断开连接方会有一个 TIME_WAIT 的状态,这个状态会持续 2 MSL 后才会转变为 CLOSED 状态。
  • 在 Linux 操作系统下,TIME_WAIT 状态的持续时间是 60 秒,这意味着这 60 秒内,客户端一直会占用着这个端口。要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000
  • 如果客户端(发起连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务器发起连接了,但是被使用的端口,还是可以继续对另外一个服务器发起连接的。
  • 不过,Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接,这两个参数都是默认关闭的:
    • net.ipv4.tcp_tw_reuse,如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,**如果内核选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。**所以该选项只适用于连接发起方。
    • net.ipv4.tcp_tw_recycle,如果开启该选项的话,允许处于TIME_WAIT 状态的连接被快速回收
  • 要使得这两个选项生效,有一个前提条件,就是要打开 TCP 时间戳,即 net.ipv4.tcp_timestamps=1(默认即为 1))。
  • tcp_tw_recycle 在使用了 NAT 的网络下是不安全的!
  • 对于服务器来说,如果同时开启了recycle 和 timestamps 选项,则会开启一种称之为「 per-host 的 PAWS 机制」。
    • 什么是PAWS 机制
      • tcp_timestamps 选项开启之后, PAWS 机制会自动开启,它的作用是防止 TCP 包中的序列号发生绕回。
      • 正常来说每个 TCP 包都会有自己唯一的 SEQ,出现 TCP 数据包重传的时候会复用 SEQ 号,这样接收方能通过 SEQ 号来判断数据包的唯一性,也能在重复收到某个数据包的时候判断数据是不是重传的。但是 TCP 这个 SEQ 号是有限的,一共 32 bit,SEQ 开始是递增,溢出之后从 0 开始再次依次递增。
      • 所以当 SEQ 号出现溢出后单纯通过 SEQ 号无法标识数据包的唯一性,某个数据包延迟或因重发而延迟时可能导致连接传递的数据被破坏,比如:
      • PAWS 就是为了避免这个问题而产生的,在开启 tcp_timestamps 选项情况下,一台机器发的所有 TCP 包都会带上发送时的时间戳,PAWS 要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。
    • 什么是 per-host 的 PAWS 机制呢?
      • er-host 是对「对端 IP 做 PAWS 检查」,而非对「IP + 端口」四元组做 PAWS 检查。
      • 但是如果客户端网络环境是用了 NAT 网关,那么客户端环境的每一台机器通过 NAT 网关后,都会是相同的 IP 地址,在服务端看来,就好像只是在跟一个客户端打交道一样,无法区分出来。
      • Per-host PAWS 机制利用TCP option里的 timestamp 字段的增长来判断串扰数据,而 timestamp 是根据客户端各自的 CPU tick 得出的值。
      • 当客户端 A 通过 NAT 网关和服务器建立 TCP 连接,然后服务器主动关闭并且快速回收 TIME-WAIT 状态的连接后,客户端 B 也通过 NAT 网关和服务器建立 TCP 连接,注意客户端 A 和 客户端 B 因为经过相同的 NAT 网关,所以是用相同的 IP 地址与服务端建立 TCP 连接,如果客户端 B 的 timestamp 比 客户端 A 的 timestamp 小,那么由于服务端的 per-host 的 PAWS 机制的作用,服务端就会丢弃客户端主机 B 发来的 SYN 包。
      • tcp_tw_recycle 在 Linux 4.12 版本后,直接取消了这一参数。
  • accpet 队列满了
    • 在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
      • 半连接队列,也称 SYN 队列;
      • 全连接队列,也称 accepet 队列;
    • 服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
    • 半连接队列满了
      • 当服务器造成syn攻击,就有可能导致 TCP 半连接队列满了,这时后面来的 syn 包都会被丢弃。
      • 但是,如果开启了syncookies 功能,即使半连接队列满了,也不会丢弃syn 包。
      • syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功
    • 全连接队列满了
      • 在服务端并发处理大量请求时,如果 TCP accpet 队列过小,或者应用程序调用 accept() 不及时,就会造成 accpet 队列满了 ,这时后续的连接就会被丢弃,这样就会出现服务端请求数量上不去的现象。
      • 我们可以通过 ss 命令来看 accpet 队列大小,在「LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:
        • Recv-Q:当前 accpet 队列的大小,也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接个数;
        • Send-Q:当前 accpet 最大队列长度,上面的输出结果说明监听 8088 端口的 TCP 服务进程,accpet 队列的最大长度为 128;
      • 如果 Recv-Q 的大小超过 Send-Q,就说明发生了 accpet 队列满的情况。
      • 要解决这个问题,我们可以:
        • 调大 accpet 队列的最大长度,调大的方式是通过调大 backlog 以及 somaxconn 参数
        • 检查系统或者代码为什么调用 accept() 不及时;

网络:TCP的三次握手_第5张图片







待整理

  • 一开始,客户端和服务端都处于 CLOSED 状态。
  • 服务器必须准备好接受外来的连接, 主动监听某个端口。这通常通过socket、bind、listen这3个函数来完成,我们称之为被动打开, 此时服务端处LISTEN状态。
  • 然后客户端主动调用connect应该连接请求(主动打开)
    • 这个时候客户端TCP会发送一个SYN分节(标志位SYN=1, 发送序号SEX=x) ,它告诉服务器客户将在(待建立)的连接中发送的数据的初始序列号。
    • 通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部,一个TCP首部和可能有的TCP选项。
    • 客户端发送完SYN之后,将处于 SYN-SENT 状态,等待服务端确认。
  • 服务端收到数据报后由标志位SYN=1知道客户端请求建立连接,
    • 必须 ACK(确认)客户端的SYN(将标志位SYN和ACK都位于1,确认序号ACK=x+1),同时随机产生一个发送序号SEQ=y(表示服务器将在同一连接中发送的数据的初始序列号),并将该数据包发送给客户端以确认连接请求。
    • 服务器收到客户端的SYN之后处于 SYN-RCVD 状态。
  • 客户端收到确认后:
    • 检测确认序号ACK是否为x+1,标志位ACK是否为1,如果正确,则将ACK置为1,确认序号ACK=y+1,并将该数据包发送给服务端。
    • 客户端随后进入ESTABLISHED 状态(表示TCP链接成功),因为它一发一收成功了。
  • 服务端收到数据报之后:
    • 检测确认序号ACK是否为y+1,标志位ACK是否为1
    • 如果是表明建立成功,并进入 ESTABLISHED 状态,因为它也一发一收成功了了。

网络:TCP的三次握手_第6张图片

网络:TCP的三次握手_第7张图片

这种交换至少需要三个分组,因此被称为TCP的三次握手。建立链接后,客户端和服务器就可以相互通信了

你可能感兴趣的:(网络,tcp/ip,linux,unix)