网络3️⃣TCP-三握

1、三次握手 🔥

TCP 是面向连接的协议,

通信之前必须先建立连接(aka. 三次握手)。

  • 握一:客户端的 SYN 报文。

  • 握二:服务端的 SYN+ACK 报文。

  • 握三:客户端的 ACK 报文。

    TCP 三次握手

最初,客户端和服务端都处于 CLOSE 状态

服务端先主动监听某个端口,处于 LISTEN 状态

1.1、客户端 SYN

  1. 生成 SYN 报文:不包含应用层数据。

    • 随机初始化序号(client_isn),填入 TCP 首部的序列号
    • SYN 标志位设 1
  2. 发送报文

    • 把报文发送给服务端,表示向服务端发起连接
    • 之后客户端处于 SYN-SENT 状态

    第一个报文 —— SYN 报文

1.2、服务端 SYN+ACK

  1. 收到客户端的 SYN 报文。

  2. 生成 SYN+ACK 报文:不包含应用层数据。

    • 随机初始化序号(server_isn),填入 TCP 首部的序列号
    • client_isn + 1 填入 TCP 首部的确认应答号
    • SYNACK 标志位设 1
  3. 发送报文

    • 把报文发给客户端,表示向客户端建立连接
    • 之后服务端处于 SYN-RCVD 状态

    第二个报文 —— SYN + ACK 报文

1.3、客户端 ACK

  1. 收到服务端的 SYN+ACK 报文。

  2. 生成 ACK 报文:最后一个应答报文,可以携带应用层数据

    • server_isn + 1 填入 TCP 首部的确认应答号
    • ACK 标志位设 1
  3. 发送报文

    1. 把报文发送给服务端。
    2. 之后客户端处于 ESTABLISHED 状态

    第三个报文 —— ACK 报文

服务端收到客户端的 ACK 报文后,也进入 ESTABLISHED 状态

  • 此时连接建立完成,双方可以互相发送数据。

  • 在 Linux 中查看 TCP 状态netstat -napt 命令

    TCP 连接状态查看

2、三握分析

2.1、为什么是三次

  • 表面原因:三握才能保证双方都具有接收和发送的能力
  • 深层原因:结合 TCP 连接的定义,深层原因如下。
    1. 避免历史连接的初始化(主要)
    2. 避免资源浪费
    3. 同步双方的初始序列号

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。

  • FHash 算法。根据 TCP 四元组生成一个随机数值。

    ISN = M + F(localhost, localport, remotehost, remoteport)
    

4、分片 🔥

4.1、MTU 和 MSS

显然,MTU > MSS

  • MTU(最大传输单元):一个网络包的最大长度,以太网中一般为 1500 字节(网络层使用)。

  • MSS(最大报文段长度):除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度(传输层使用)。

    MTU 与 MSS

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 为单位重传,提高传输效率。

    握手阶段协商 MSS

5、SYN 攻击

  • 攻击者:短时间内伪造不同 IP 地址的 SYN 报文,发送给服务端。

  • 服务端

    • 每收到一个 SYN 报文,回复 SYN+ACK 报文并进入SYN_RCVD 状态。
    • 无法收到攻击者 IP 主机的 ACK 应答。
  • 结果:攻击者的请求占满服务端的半连接队列,使服务端不能为正常用户服务。

    SYN 攻击

5.1、半连接、全连接队列

TCP 三次握手的时候,Linux 内核会维护两个队列

  • 半连接队列(SYN 队列)
  • 全连接队列(Accept 队列)

5.1.1、工作流程

  1. 服务端

    • 接收到客户端的 SYN 报文后,创建一个半连接的对象并加入到内核的 SYN 队列
    • 发送 SYN+ACK 给客户端,等待客户端回应 ACK 报文。
    • 接收到 ACK 报文后,从 SYN 队列取出一个半连接对象,创建一个新的连接对象放入到 Accept 队列
  2. 应用调用 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 应对 SYN 攻击

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
  • 第二次握手

    • 服务器生成 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 报文(握一)中的应用数据
  • 客户端:需要重新发送应用数据
posted @ 2023-05-09 01:11  Jaywee  阅读(94)  评论(0编辑  收藏  举报

👇