TCP 三次握手期间异常,会发生什么?
# TCP 三次握手期间异常,会发生什么?
# 复习三次握手
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
(三次握手)
一开始,客户端和服务端都处于 CLOSED
状态。先是服务端主动监听某个端口,处于 LISTEN
状态。
- 第一次握手:
- 客户端随机初始化序号,将此序号置于 TCP 首部的「序号」字段中,同时把
SYN
标志位置为1
,表示发起连接的报文。接着把第一个SYN
报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN_SENT
状态。
- 客户端随机初始化序号,将此序号置于 TCP 首部的「序号」字段中,同时把
- 第二次握手:
- 服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号,将此序号填入 TCP 首部的「序号」字段中(seq=y
),其次把 TCP 首部的「确认应答号」字段填入(ack=x+1
),接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN_RCVD
状态。
- 服务端收到客户端的
- 第三次握手:
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入y+1
(即ack=y+1
),最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于ESTABLISHED
状态。 - 服务器收到客户端的应答报文后,也进入
ESTABLISHED
状态,因为它也一发一收成功了。
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
但是如果在三次握手的过程中,发生了异常,会怎么样呢?下面分情况来讨论。
# 第一次握手丢失了,会发生什么?
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT
状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
对于超时时间,不同版本的操作系统可能不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
5
2
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。
举个例子,假设 tcp_syn_retries
参数值为 3,那么当客户端的 SYN 报文一直在网络中丢失时,会发生下图的过程:
(第一次握手丢失)
具体过程:
- 当客户端超时重传 3 次 SYN 报文后,由于
tcp_syn_retries
为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
# 第二次握手丢失了,会发生什么?
当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD
状态。
第二次握手的 SYN-ACK 报文其实有两个目的:
- 第二次握手里的 ACK, 是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由
tcp_syn_retries
内核参数决定; - 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由
tcp_synack_retries
内核参数决定。
在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
2
举个例子,假设 tcp_syn_retries
参数值为 1,tcp_synack_retries
参数值为 2,那么当第二次握手一直丢失时,发生的过程如下图:
(第二次握手丢失)
具体过程:
- 当客户端超时重传 1 次 SYN 报文后,由于
tcp_syn_retries
为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。 - 当服务端超时重传 2 次 SYN-ACK 报文后,由于
tcp_synack_retries
为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
# 第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH
状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries
参数值为 2,那么当第三次握手一直丢失时,发生的过程如下图:
(第三次握手丢失)
具体过程:
- 当服务端超时重传 2 次 SYN-ACK 报文后,由于
tcp_synack_retries
为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
# 关于 SYN 攻击
# 什么是 SYN 攻击
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入 SYN_RCVD
状态,但服务端发送出去的 SYN-ACK
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务器不能为正常用户服务。
(SYN 攻击)
下面先来看一下什么是 TCP 半连接和全连接队列。
# 半连接和全连接队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列,也称 SYN 队列。
- 全连接队列,也称 Accpet 队列。
先来看下 Linux 内核的 SYN
队列(半连接队列)与 Accpet
队列(全连接队列)是如何工作的?
(SYN 队列 和 Accpet 队列正常流程)
正常流程:
- 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「SYN 队列」。
- 接着发送 SYN-ACK 给客户端,等待客户端回应 ACK 报文。
- 服务端接收到 ACK 报文后,从「SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「Accept 队列」。
- 应用通过调用
accpet()
socket 接口,从「Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就是把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
# 避免 SYN 攻击方式
避免 SYN 攻击方式,可以有以下四种方法:
- 调大
netdev_max_backlog
- 增大 TCP 半连接队列
- 开启
tcp_syncookies
- 减少 SYN-ACK 重传次数
# 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 半连接队列的大小,这里暂不深入研究。
# 3)方式三:开启 net.ipv4.tcp_syncookies
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
(SYN 队列占满,启动 cookie)
具体过程:
- 当「SYN 队列」满之后,后续服务器收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值。
- 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端。
- 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「Accept 队列」。
- 最后应用程序通过调用
accpet()
接口,从「Accept 队列」取出的连接。
可以看到,当开启了 tcp_syncookies
了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。
net.ipv4.tcp_syncookies
参数主要有以下三个值:
0
值,表示关闭该功能。1
值,表示仅当 SYN 半连接队列放不下时,再启用它。2
值,表示无条件开启功能。
那么在应对 SYN 攻击时,只需要设置为 1
即可:
$ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
# 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
(完)