2. QUIC 连接建立流程:握手过程详解
QUIC 的连接建立,说白了就是客户端和服务器互相确认身份、协商参数、建立加密通道的过程。它把 TCP 的三次握手和 TLS 的握手合并到了一起,效率高了不少。
我刚开始接触 QUIC 时,最直观的感受就是:它比 TCP+TLS 少了一个 RTT。嗯,这里要注意,这个“少”是有前提的——你得用上 TLS 1.3。
2.1 握手流程概览
一个完整的 QUIC 握手,通常只需要 1-RTT(首次连接)或 0-RTT(再次连接)。我们先用一张图把整体流程看清楚。
核心要点:QUIC 握手 = 传输层握手 + 加密层握手,两者并行完成。
我的经验:有一次我在调试一个 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 连接中最常见的问题。我总结了几种典型场景:
- Initial 包丢失:客户端发送 Initial 包后,没有收到服务器的任何回复。客户端会重试,重试次数由
initial_max_retransmit控制 - 版本不匹配:客户端和服务器支持的 QUIC 版本不同。服务器会回复版本协商包,客户端需要重新连接
- 传输参数冲突:比如客户端要求使用某个参数,但服务器不支持。连接会被拒绝
- 证书验证失败:证书过期、域名不匹配、自签名证书等
- 0-RTT 被拒绝:服务器不支持 0-RTT,或者 0-RTT 密钥过期。服务器会回退到 1-RTT 握手
重要:握手超时的时间由 max_idle_timeout 决定。如果这个值设得太小,比如 5 秒,而网络延迟有 200ms,加上证书传输时间,很容易超时。我建议服务端至少设 30 秒。