网络3️⃣TCP-三握
1、三次握手 🔥
TCP 是面向连接的协议,
通信之前必须先建立连接(aka. 三次握手)。
-
握一:客户端的
SYN
报文。 -
握二:服务端的
SYN+ACK
报文。 -
握三:客户端的
ACK
报文。
最初,客户端和服务端都处于
CLOSE
状态。服务端先主动监听某个端口,处于
LISTEN
状态。
1.1、客户端 SYN
-
生成
SYN
报文:不包含应用层数据。- 随机初始化序号(
client_isn
),填入 TCP 首部的序列号。 - 把
SYN
标志位设1
。
- 随机初始化序号(
-
发送报文:
- 把报文发送给服务端,表示向服务端发起连接。
- 之后客户端处于
SYN-SENT
状态。
1.2、服务端 SYN+ACK
-
收到客户端的
SYN
报文。 -
生成
SYN+ACK
报文:不包含应用层数据。- 随机初始化序号(
server_isn
),填入 TCP 首部的序列号。 - 将
client_isn + 1
填入 TCP 首部的确认应答号。 - 把
SYN
和ACK
标志位设1
。
- 随机初始化序号(
-
发送报文:
- 把报文发给客户端,表示向客户端建立连接。
- 之后服务端处于
SYN-RCVD
状态。
1.3、客户端 ACK
-
收到服务端的
SYN+ACK
报文。 -
生成
ACK
报文:最后一个应答报文,可以携带应用层数据。- 将
server_isn + 1
填入 TCP 首部的确认应答号。 - 把
ACK
标志位设1
。
- 将
-
发送报文:
- 把报文发送给服务端。
- 之后客户端处于
ESTABLISHED
状态。
服务端收到客户端的
ACK
报文后,也进入ESTABLISHED
状态。
-
此时连接建立完成,双方可以互相发送数据。
-
在 Linux 中查看 TCP 状态:
netstat -napt
命令
2、三握分析
2.1、为什么是三次
- 表面原因:三握才能保证双方都具有接收和发送的能力。
- 深层原因:结合 TCP 连接的定义,深层原因如下。
- 避免历史连接的初始化(主要)
- 避免资源浪费
- 同步双方的初始序列号
Hint:TCP 连接是指状态信息的组合,包括 Socket、序列号和窗口大小。
2.1.1、历史连接 & 资源浪费
RFC 793: The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
解释:三次握手的主要原因是为了防止旧的重复连接初始化造成混乱。
场景:
- 客户端因为某种原因,连续发送多个 ACK 报文。
- 其中旧的 ACK 报文就是历史连接,如果初始化会导致资源浪费。
示例:
- 客户端发送一个 SYN 报文后,发生宕机并重启后,又发送了一个新的 SYN 报文(序列号不同)。
- 不属于丢包重传(序列号相同),而是尝试建立一个新的连接(序列号不同)。
① 三握如何避免
客户端会检查服务端
SYN+ACK
报文中的确认应答号,确认是不是自己当前期望收到的。
-
是:发送
ACK
报文,正常完成握手。 -
不是:说明此
SYN+ACK
报文是对历史连接的回复,发送 RST 通知服务器释放连接。
场景:服务端在收到 RST 之前收到新的 ACK。
(i.e. 服务端连续收到 ACK 报文)
服务端处理:
- 第一次收到 SYN 报文(旧),回复
SYN + ACK
报文给客户端(确认应答号 91)。 - 第二次收到 SYN 报文(新),回复
Challenge ACK
报文给客户端(确认应答号也是 91)。Challenge ACK
报文的确认应答号和服务端上一次的 ACK 确认号相同。- 客户端收到此报文后,发现与期望的确认号 101 不符,回复 RST 报文。
② 两握无法避免
两次握手无法阻止历史连接,无法避免资源浪费。
原因:服务端没有建立连接之前的中间状态。
分析:在两次握手的情况下
- 服务端:收到 SYN 报文后就进入
ESTABLISHED
状态。- 代表可以收发数据。
- 此时连接已经建立,如果是冗余连接则浪费资源。
- 客户端:还没有进入
ESTABLISHED
状态。- 假如客户端判断到连接属于历史连接,会发送 RST 来断开。
- 服务端收到 RST 之后会中断连接,但在这之前可能已经发送了数据。
2.1.2、同步双方初始序列号
① 序列号
TCP 通信双方都必须维护一个序列号,用于实现可靠传输。
序列号作用:
- 接收方可以去除重复数据。
- 接收方可以按序接收数据包。
- 接收方可以通过
ACK
报文中的序列号,标识已被对方接收的数据包。
② 如何同步
一来一回,确保双方初始序列号的可靠同步。
-
客户端发送
SYN
报文时(携带初始序列号),需要服务端回一个ACK
报文(确认 SYN 报文中的初始序列号)。 -
同理,服务端发送
SYN
报文时,也需要客户端回一个ACK
报文。
三握:能避免历史连接的初始化,减少资源浪费,可靠地同步双方初始化序列号。
-
两握:
- 无法防止历史连接的建立,会造成双方资源的浪费。
- 无法可靠的同步双方序列号(只能保证一方的初始序列号能被对方成功接收)。
-
四握:可以合并服务端的 ACK 和 SYN,即优化为三次握手。
2.2、握手丢失的影响
2.2.1、客户端 SYN
客户端发送
SYN
报文(握一)后,进入SYN_SENT
状态。
SYN 报文丢失:客户端超时重传,直到成功或达到重传次数(断开连接)。
-
服务端:无法接收到客户端
SYN
报文(握一),不会响应SYN+ACK
报文(握二)。 -
客户端:迟迟收不到
SYN+ACK
报文(握二),触发超时重传。-
超时时间:不同版本的操作系统有所不同,每次超时时间是上次的 2 倍(如 1s, 2s, 4s, 8s, 16s, ...)。
-
重传次数:Linux 内核参数
tcp_syn_retries
决定(默认值 5)。五次超时重传后,会继续等待 32 秒,如果仍没有接收到ACK
则断开 TCP 连接。
cat /proc/sys/net/ipv4/tcp_syn_retries
-
2.3.2、服务端 SYN+ACK
服务端收到客户端的
SYN
报文(握一),回复
SYN+ACK
报文(握二)后,进入SYN_RCVD
状态。
SYN+ACK 报文丢失:客户端和服务端都会超时重传,直到成功或达到重传次数(断开连接)。
-
客户端:
- 迟迟没有收到
SYN+ACK
报文(握二),认为 SYN 报文(握一)丢失,触发超时重传。 - 不会回复
ACK
报文(握三)。
- 迟迟没有收到
-
服务端:迟迟没有收到客户端的
ACK
报文(握三),触发超时重传。-
重传次数:Linux 内核参数
tcp_synack_retries
决定。 -
通常默认值 5,可自定义。
cat /proc/sys/net/ipv4/tcp_synack_retries
-
2.3.3、客户端 ACK
客户端收到服务端的
SYN+ACK
报文(握二),回复
ACK
报文(握三),进入ESTABLISH
状态。
ACK 报文丢失:服务端超时重传,直到成功或达到重传次数(断开连接)。
-
客户端:发送的
ACK
报文(握三)丢失,此报文不会重传。 -
服务端:迟迟没有收到
ACK
报文(握三),认为 SYN+ACK 报文(握二)丢失,触发超时重传。
3、初始序列号 ISN
初始序列号(Initial Sequence Number)
3.1、每次连接的 ISN 不同
为什么每次建立 TCP 连接时,初始序列号都要求不一样呢?
原因:
- 防止历史报文被下一个相同四元组的连接接收(主要)。
- 防止黑客伪造相同序列号的 TCP 报文被对方接收(安全性)。
① 相同 ISN
假设每次建立连接,客户端和服务端的 ISN 都从 0 开始。
客户端和服务端建立一个 TCP 连接:
- 客户端:发送数据包被网络阻塞,进行超时重传。
- 服务端:恰好设备断电重启,之前与客户端建立的连接消失。
- 收到客户端的数据包时,回复 RST 报文。
- 假设此时客户端发送的第一个数据包还在网络中。
- 客户端:与服务端重新建立连接,与上一个连接的四元组相同。
- 问题:假设在连接建立后,上一个连接中被网络阻塞的数据包(即历史报文)正好抵达服务端。
- 历史报文的序列号正是服务端期望接收的序列号。
- 历史报文会被服务端正常接收,就会造成数据错乱。
② 不同 ISN
如果每次建立连接时,初始化序列号都不同。
有很大概率,历史报文的序列号不在对方接收窗口,避免历史报文接收。
3.2、ISN 随机生成算法
RFC 793 提到 ISN 随机生成算法,
基于时钟计时器递增,基本不会生成相同的 ISN。
-
M
:计时器。每隔 4 微秒加 1。 -
F
:Hash 算法。根据 TCP 四元组生成一个随机数值。ISN = M + F(localhost, localport, remotehost, remoteport)
4、分片 🔥
4.1、MTU 和 MSS
显然,MTU > MSS
-
MTU
(最大传输单元):一个网络包的最大长度,以太网中一般为1500
字节(网络层使用)。 -
MSS
(最大报文段长度):除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度(传输层使用)。
4.2、IP 分片
在发送方,传输层(
TCP
)数据包会经过下层的网络层(IP
)进行传输。TCP 数据包 = TCP 首部 + TCP 数据
当 IP 层接收到一个超过 MTU
的 TCP 数据包,在发送之前需要进行分片。
- 发送方 IP 层把数据分割成若干片,每个分片都小于
MTU
。 - 接收方 IP 层需要将分片重新组装后,再交给上层 TCP 传输层。
问题:IP 层没有重传机制。
如果发生 IP 分片丢失,整个 IP 报文的所有分片都得重传。
分析:当某一个 IP 分片丢失
- 接收方:
- 网络层(IP):无法组装成一个完整的 TCP 数据包,无法送到 TCP 层。
- 传输层(TCP):不会响应
ACK
。
- 发送方:迟迟收不到
ACK
报文,触发超时重传(整个 TCP 数据包,包括首部 + 数据)。
结论:IP 层分片传输的效率低(发生分片丢失时需要重传整个 TCP 数据包)。
为了达到最佳的传输效能,需要由 IP 上层的 TCP 负责分片。
4.3、TCP 分片 🔥
TCP 负责分片,IP 无需分片。
-
TCP 在建立连接时,通常要协商双方的
MSS
值。- 握手时,双方均在 TCP 首部中写入 MSS 项。
- 选择较小值作为 MSS。
-
当 TCP 发现数据超过
MSS
时进行分片,形成的 IP 数据包不会超过MTU
。 -
如果发生 TCP 分片丢失,只需以 MSS 为单位重传,提高传输效率。
5、SYN 攻击
-
攻击者:短时间内伪造不同 IP 地址的
SYN
报文,发送给服务端。 -
服务端:
- 每收到一个
SYN
报文,回复SYN+ACK
报文并进入SYN_RCVD
状态。 - 无法收到攻击者 IP 主机的
ACK
应答。
- 每收到一个
-
结果:攻击者的请求占满服务端的半连接队列,使服务端不能为正常用户服务。
5.1、半连接、全连接队列
TCP 三次握手的时候,Linux 内核会维护两个队列
- 半连接队列(SYN 队列)
- 全连接队列(Accept 队列)
5.1.1、工作流程
-
服务端:
- 接收到客户端的
SYN
报文后,创建一个半连接的对象并加入到内核的 SYN 队列。 - 发送
SYN+ACK
给客户端,等待客户端回应ACK
报文。 - 接收到
ACK
报文后,从 SYN 队列取出一个半连接对象,创建一个新的连接对象放入到 Accept 队列。
- 接收到客户端的
-
应用:调用 socket 接口
accpet()
,从 Accept 队列取出连接对象。
5.1.2、长度限制
半连接队列和全连接队列都有长度限制
- 超过限制时,默认处理是丢弃报文。
- SYN 攻击方式会把 TCP 半连接队列占满,服务端只能丢弃后续的 SYN 报文,无法为用户提供服务。
6.2、避免 SYN 攻击
① 调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。
控制该队列的最大值如下参数(默认值 1000),需要适当调大参数值。
net.core.netdev_max_backlog = 10000
② 增大 TCP 半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数
- net.ipv4.tcp_max_syn_backlog
- listen() 函数中的 backlog
- net.core.somaxconn
③ 开启 net.ipv4.tcp_syncookies
开启
tcp_syncookies
:可以跳过 SYN 半连接队列,进行后续流程并建立连接。
服务端处理过程:当 SYN 队列满之后,收到 SYN 包不会丢弃。
-
根据算法计算出一个
cookie
值,填入SYN+ACK
的序列号并回复客户端。 -
接收到客户端的 ACK 报文时,检查其合法性。如果合法,将该连接对象放入到「 Accept 队列」。
-
应用程序调用
accpet()
,从 Accept 队列取出连接。
tcp_syncookies
参数值:
-
0:关闭
-
1:当 SYN 半连接队列占满时开启;
-
2:无条件开启
# 应对SYN攻击时设为1即可 $ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
④ 减少 SYN+ACK 重传次数
当服务端受到 SYN 攻击时,会有大量处于 SYN_REVC 状态的 TCP 连接。
处于此状态的 TCP 触发超时重传。
对策:减少 SYN+ACK
的重传次数,如减少到 2。
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
6、TCP Fast Open
TCP Fast Open(Linux 3.7+ 内核版本):
绕过 TCP 三次握手发送数据,减少 TCP 连接建立的时延。
6.1、要求
- 客户端和服务端均支持
TCP Fast Open
功能。 - 客户端和服务端已经完成过一次通信(i.e. 完成过一次三握)。
6.2、工作流程
核心是
Fast Open Cookie
。
① 初次建立连接
第三次握手才可以携带应用数据。
-
第一次握手:客户端发送 SYN 报文。
- 该报文包含 Fast Open 选项,且选项的
Cookie
为空。 - 代表客户端请求
Fast Open Cookie
。
- 该报文包含 Fast Open 选项,且选项的
-
第二次握手:
- 服务器生成
Cookie
并置于 SYN-ACK 报文中的Fast Open
选项,发回客户端。 - 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的
Cookie
。
- 服务器生成
-
第三次握手:客户端发送 ACK 报文,可以携带应用数据。
至此,客户端拥有了
Fast Open Cookie
。用于向服务器 TCP 证明,之前与客户端 IP 地址的三握已成功完成。
② 后续建立连接
假设 Cookie 有效,第一次握手就可以携带应用数据(i.e. 绕过了三次握手发送数据)。
- 第一次握手:客户端发送 SYN 报文,可携带应用数据和
Fast Open Cookie
。 - 第二次握手:服务器校验
Cookie
有效。- 接收应用数据
- 发送 SYN-ACK 报文,包含对客户端 SYN 的 seq 确认、对应用数据 seq 的确认、服务器响应数据
- 第三次握手:客户端发送 ACK 报文,包含对服务端 SYN 的 seq 确认、后续应用数据。
如果 Cookie 无效
- 服务端:丢弃 SYN 报文(握一)中的应用数据
- 客户端:需要重新发送应用数据