2. QUIC 连接建立流程:握手过程详解

QUIC 的连接建立,说白了就是客户端和服务器互相确认身份、协商参数、建立加密通道的过程。它把 TCP 的三次握手和 TLS 的握手合并到了一起,效率高了不少。

我刚开始接触 QUIC 时,最直观的感受就是:它比 TCP+TLS 少了一个 RTT。嗯,这里要注意,这个“少”是有前提的——你得用上 TLS 1.3。

2.1 握手流程概览

一个完整的 QUIC 握手,通常只需要 1-RTT(首次连接)或 0-RTT(再次连接)。我们先用一张图把整体流程看清楚。

核心要点:QUIC 握手 = 传输层握手 + 加密层握手,两者并行完成。

QUIC 1-RTT 握手流程 客户端 服务器 Initial 包 ClientHello + 传输参数 ① 客户端发起连接 Initial 包 ServerHello + 传输参数 ② 服务器回复 Handshake 包 证书 + 完成 ③ 服务器发送证书 Handshake 包 客户端完成 ④ 客户端确认完成 连接建立,开始传输数据

我的经验:有一次我在调试一个 QUIC 客户端,发现握手总是卡在 Initial 包阶段。后来抓包一看,是客户端忘了设置传输参数中的 initial_max_stream_data。服务器直接拒绝了连接。嗯,这种低级错误,排查起来最费时间。

2.2 TLS 1.3 集成

QUIC 没有自己搞一套加密方案,它直接用了 TLS 1.3。为什么是 TLS 1.3?因为 TLS 1.3 的握手只需要 1-RTT,和 QUIC 的设计理念完全吻合。

我个人习惯把 QUIC 的加密层理解成“TLS 1.3 跑在 QUIC 的包上面”。具体来说:

  • Initial 包:携带 ClientHello 和 ServerHello,完成密钥协商
  • Handshake 包:携带证书和 Finished 消息,完成身份验证
  • 1-RTT 包:握手完成后,用 1-RTT 密钥加密的数据包
  • 0-RTT 包:如果之前连接过,客户端可以直接发送 0-RTT 数据

注意:0-RTT 数据存在重放攻击的风险。我曾经在项目中遇到过,客户端用 0-RTT 发送了一个订单请求,结果服务器处理了两次。解决方案是在应用层做幂等性校验。

2.3 Transport 参数协商

QUIC 的传输参数是在握手过程中协商的。这些参数决定了连接的行为,比如最大流数量、初始窗口大小、空闲超时时间等。

参数通过 transport_parameters 扩展在 TLS 握手中传递。客户端和服务器各自发送自己支持的参数,对方收到后必须遵守。

参数名称 参数 ID 说明 我的建议
initial_max_streams_bidi 0x00 双向流最大数量 建议设为 100+,避免流不够用
initial_max_stream_data 0x01 单个流初始窗口大小 至少 1MB,否则小文件传输效率低
initial_max_data 0x04 连接级初始窗口大小 建议 10MB 起步
idle_timeout 0x03 空闲超时时间(毫秒) 移动端建议 30s,服务端建议 60s
max_udp_payload_size 0x05 最大 UDP 包大小 默认 1200 字节,不要改小

避坑指南:我曾经遇到过一个案例,客户端把 idle_timeout 设成了 10 秒,服务器默认是 30 秒。结果客户端在 10 秒后主动断开了连接,但服务器还在等数据。两边参数不一致,就会出问题。记住:最终生效的是两者中的较小值。

2.4 Initial 包与 Handshake 包

这两个包是 QUIC 握手阶段的核心。我分别说一下它们的作用和特点。

2.4.1 Initial 包

Initial 包是连接的第一个包。它有几个关键特征:

  • 必须使用版本协商:客户端发送的 Initial 包包含 QUIC 版本号,如果服务器不支持,会回复版本协商包
  • 必须包含连接 ID:用于标识连接,防止冲突
  • 使用 Initial 密钥加密:这个密钥是从目标连接 ID 推导出来的,不是真正的加密密钥
  • 载荷长度有限:Initial 包的最大载荷是 1200 字节,这是强制要求

为什么 Initial 包要限制 1200 字节?说白了,是为了防止放大攻击。如果服务器回复的包比客户端发送的包大太多,攻击者就可以用很小的流量放大攻击。1200 字节这个限制,保证了放大倍数不超过 3 倍。

2.4.2 Handshake 包

Handshake 包在 Initial 包之后出现。它使用从 TLS 握手推导出的 Handshake 密钥加密。

Handshake 包的特点:

  • 不受 1200 字节限制:可以携带更大的证书
  • 使用独立的密钥:和 Initial 密钥不同,安全性更高
  • 可以携带多个帧:比如 CRYPTO 帧、ACK 帧、STREAM 帧等

我的经验:有一次我在调试证书过大的问题。服务器证书链有 8KB,结果 Handshake 包被分片了。客户端收到分片后,需要等待所有分片到齐才能验证证书。如果网络丢包严重,握手就会超时。我的建议是:证书链尽量控制在 4KB 以内。

2.5 握手过程中的关键帧

握手过程中,Initial 包和 Handshake 包携带了多种帧。我整理了一个表格,方便你对照。

帧类型 帧 ID 携带的包类型 作用
CRYPTO 0x06 Initial, Handshake 传输 TLS 握手消息
ACK 0x02 Initial, Handshake, 1-RTT 确认收到的包
PING 0x01 任意 保活或探测
PADDING 0x00 Initial 填充到 1200 字节

你想想看,如果没有 PADDING 帧,Initial 包可能只有几十字节。攻击者就可以用很小的包触发服务器发送大包。所以 QUIC 强制要求 Initial 包必须填充到 1200 字节。

2.6 握手超时诊断要点

握手超时是 QUIC 连接中最常见的问题。我总结了几种典型场景:

  1. Initial 包丢失:客户端发送 Initial 包后,没有收到服务器的任何回复。客户端会重试,重试次数由 initial_max_retransmit 控制
  2. 版本不匹配:客户端和服务器支持的 QUIC 版本不同。服务器会回复版本协商包,客户端需要重新连接
  3. 传输参数冲突:比如客户端要求使用某个参数,但服务器不支持。连接会被拒绝
  4. 证书验证失败:证书过期、域名不匹配、自签名证书等
  5. 0-RTT 被拒绝:服务器不支持 0-RTT,或者 0-RTT 密钥过期。服务器会回退到 1-RTT 握手

重要:握手超时的时间由 max_idle_timeout 决定。如果这个值设得太小,比如 5 秒,而网络延迟有 200ms,加上证书传输时间,很容易超时。我建议服务端至少设 30 秒。