一、背景
QUIC是什么?
QUIC是一种安全的通用传输协议。
QUIC是一个面向连接的协议,可在客户端和服务端之间创建有状态的交互。
QUIC握手主要包括密钥协商和传输参数协商。 QUIC使用TLS([TLS13])密钥协商握手协议进行密钥协商,使用定制的框架来保护数据包,没有使用TLS记录层协议(数据分片和加密)。QUIC对TLS的使用,在[QUIC-TLS]中有更详细的描述。 握手流程的设计使得可以尽快交换应用数据:如果之前有过握手过程和保存配置,那么客户端可以启用立即发送数据(0-RTT)的选项。
在QUIC协议中,终端(Endpoint)通过交换QUIC包(Packet)通信。大多数数据包中包含帧(Frame),帧携带控制信息和应用数据。QUIC验证每个数据包的完整性,并尽可能对所有数据包进行加密。QUIC协议承载在[UDP]协议之上,以更方便其在现有系统和网络中部署。
应用层协议(如HTTP3)通过流在QUIC连接上交换信息,流是有序的字节序列。 可以创建两种类型的流:双向流,允许两个终端都发送数据;单向流,仅允许单个终端发送数据。credit-based scheme被用于限制流的创建,并约束可以发送的数据量。
QUIC提供必要的反馈,以实现可靠传输和拥塞控制。在[QUIC-RECOVERY]中描述了一种检测和恢复丢包的算法。QUIC依赖于拥塞控制来避免网络拥塞,在[QUIC-RECOVERY]中也描述了一个典型的拥塞控制算法。
QUIC连接未严格绑定到单个网络路径上。连接迁移使用连接标识符来允许连接转移到新的网络路径。在当前这个版本的QUIC Version 1中,只有客户端能够进行迁移。此设计还允许在网络拓扑或地址映射发生变化(如NAT重新绑定可能引起的变化)后继续连接。
建立连接后,将提供多个选项来终止连接。 应用程序可以管理正常关闭,终端可以协商超时期限,错误可以导致立即断开连接,无状态机制提供了在一个终端丢失状态后终止连接的功能。
更多背景详见:QUIC 协议初探
TCP+TLS 和 QUIC 协议对比
从协议栈以及握手流程可以看出:QUIC = TLS + UDP
QUIC本身就包括安全加密传输(也是基于TLS实现的),TCP+TLS是在TCP之上又建了一层协议进行加密传输
报文结构
QUIC 报文格式
QUIC报文基本通信数据单元是Packet,由 header 和 data 两部分组成。
header 是明文的,包含 4 个字段:Flags、Connection ID、QUIC Version、Packet Number;
Data 是加密的,并由Frame组成,可以包含 1 个或多个 frame,每个 frame 又分为 type 和 payload,其中 payload 就是应用数据;
数据帧有很多类型:Stream、ACK、PADDING、PING、CRYPTO等。握手过程主要是使用CRYPTO帧。
QUIC Packet Type
Packet Number是在[0, 2^62-1]范围内的整数,在报文封装中是个变长整型格式。 每个终端为发包和收包,维护独立的Packet Numbe序列。
Packet Numbe限制在此范围内,因为它们需要在ACK帧的 Largest Acknowledged 字段中完整表示。
Version Negotiation / Retry 没有packet number字段。
Packet Number被拆分为3个空间:
- Initial space:所有的Initial packet使用。
- Handshake space:所有的Handshake packet使用。
- Application data space:所有0-RTT和1-RTTpacket使用。
如[QUIC-TLS]中所述,每种数据包类型使用不同的保护密钥。
从概念上讲,Packet Numbe Space是packet被处理和响应(ACK)的上下文空间。
Initial packet只能被Initial等级的密钥加密,其ACK报文也是Initial packet。 Handshake packet只能被Handshake等级的密钥加密,并且ACK报文也是Handshake packet。
每种数据包类型使用不同的保护密钥。
+=====================+=================+==================+
| Packet Type | Encryption Keys | PN Space |
+=====================+=================+==================+
| Initial | Initial secrets | Initial |
+---------------------+-----------------+------------------+
| 0-RTT Protected | 0-RTT | Application data |
+---------------------+-----------------+------------------+
| Handshake | Handshake | Handshake |
+---------------------+-----------------+------------------+
| Retry | N/A | N/A |
+---------------------+-----------------+------------------+
| Version Negotiation | N/A | N/A |
+---------------------+-----------------+------------------+
| Short Header | 1-RTT | Application data |
+---------------------+-----------------+------------------+
Frame Type
I: Initial
H: Handshake
0: 0-RTT
1: 1-RTT
ih: 只有类型0x1c的CONNECTION_CLOSE帧才能出现在Initial或Handshake包中。
其他的Frame
RESET_STREAM Frames
STOP_SENDING Frames
NEW_TOKEN Frames
MAX_DATA Frames
MAX_STREAM_DATA Frames
MAX_STREAMS Frames
DATA_BLOCKED Frames
STREAM_DATA_BLOCKED Frames
STREAMS_BLOCKED Frames
NEW_CONNECTION_ID Frames
RETIRE_CONNECTION_ID Frames
PATH_CHALLENGE Frames
PATH_RESPONSE Frames
Extension Frames
Stream Type
STREAM Frames
STREAM frames隐式地创建一条stream并携带stream数据。 STREAM frame采用 0b00001XXX 格式(从0x08到0x0f区间)。
type中低三位的值决定了frame中存在的字段:
- The OFF bit (0x04):表示是否存在Offset字段。设为1时表示会有个Offset字段,设为0时没有。设为0时当前的Stream Data起始offset为0(即帧中包含流的第一个字节,或流的末端不包含数据)。
- The LEN bit (0x02):表示是否存在Length字段。设为0时没有Length字段,并且Stream Data字段延伸到packet结束位置。设为1时有Length字段。
- The FIN bit (0x01):stream的结束标识位,stream的最终大小是offset与该帧length之和。
如果终端在从发送的单向流中收到STREAM frame,或者在本地初始但还没有创建成功的流上收到STREAM帧,必须以STREAM_STATE_ERROR 连接错误关闭连接。
STREAM Frame {
Type (i) = 0x08..0x0f,
Stream ID (i),
[Offset (i)],
[Length (i)],
Stream Data (..),
}
STREAM Frame 包含以下字段:
- Stream ID:变长整数,表示Stream唯一标识.
- Offset:变长整数,表示当前frame数据在整个stream中的偏移量,是否存在该字段由OFF bit决定。当OFF bit为1时,该字段存在。当Offset字段不存在时,偏移量为0。
- Length: 变长整数,表示当前Stream Data字段的长度,是否存在该字段由LEN bit决定。当LEN bit为0时,该Stream Data的长度为当前packet的所有剩余长度。
- Stream Data:传输的数据字节流。
当Stream Data字段长度为0时,offset为下一个会被发送的字节下标。
Stream中第一个字节offset为0。Stream中最大的offset不能超过2^62-1,因为流控也不可能提供这么大的数据额度。必须把收到超过限制的帧的情况当成FRAME_ENCODING_ERROR或FLOW_CONTROL_ERROR类型的连接错误。
如何保持数据有序
QUIC将传输顺序和交付顺序分离:用Packet Number表示传输顺序,交付顺序由STREAM帧中的流偏移量(offset)决定。
QUIC Packet Number 在一个包序号空间内严格递增,并直接编码了传输顺序。高的包序号意味着后发,小的包序号意味着先发。 当一个包含ack-eliciting帧的包丢失了,QUIC将必要的帧包含在一个使用新的包序号的新包中,消除了收到一个ACK时哪个包被确认的二义性。 因此,可以进行更精确的RTT测量,很少发生伪重传,因此像快速重传这样的机制可以只根据包序号被广泛的应用。
这个设计点极大的简化了QUIC的丢包检测机制。大多数TCP机制根据TCP序列号隐式地推断传输顺序。
QUIC握手流程都做了那些事?
QUIC是基于UDP,而UDP不是面向连接,没有握手流程,因此鉴于这个特性,在QUIC建联的时候也不会有TCP类似的SYN,ACK,FIN等三次握手建联,断开连接也不会有四次挥手。
QUIC加密传输握手
QUIC把加密层和传输层的握手协商结合起来,以此降低握手的延迟。 QUIC使用CRYPTO Frame来传输加密层的握手包。 本文中定义的QUIC版本标识为 0x00000001(Version 1),并使[QUIC-TLS]中描述TLS协议和语义(与标准TLS1.3会有些许不同), 不同的 QUIC 版本可能表明使用了不同的加密握手协议。
QUIC提供可靠有序的加密握手数据的传递。QUIC Packet中的握手协议内容尽可能多的被加密保护。加密握手过程必须提供如下特性:
- 经过认证的密钥交换(key exchange):
-
- 服务端总是需要被认证
- 客户端被认证是可选的
- 每条连接独立且互不关联的密钥
- 加密参数和材料都可以被用于0-RTT和1-RTT packet
- 两个终端的传输参数值的认证交换,以及服务器传输参数的加密保护
- 应用层协议的认证协商(TLS为此使用ALPN[ALPN])。
CRYPTO Frame可以被放在多个Packet Number空间中发送。
在每个Packet Number空间中,加密握手数据的传输顺序是通过offset实现的,offset是从0开始的。
下图展示一个简化的握手数据包和帧的交换流程。有可能在握手过程中也会交换应用程序数据,用'*'表示应用数据。一旦握手完成,终端就可以自由交换应用程序数据。
Client Server
Initial (CRYPTO)
0-RTT (*) ---------->
Initial (CRYPTO)
Handshake (CRYPTO)
<---------- 1-RTT (*)
Handshake (CRYPTO)
1-RTT (*) ---------->
<---------- 1-RTT (HANDSHAKE_DONE)
1-RTT <=========> 1-RTT
Simplified QUIC Handshake
\
加密传输握手就被用来协商密钥。 加密握手信息由Initial和Handshake Packet携带。
下图表示了1-RTT握手协商的交互内容: 每条线的格式Packet Type[Packet Number]: frames(包含在这些Packet中的)。比如说第一行表示Initial这个Packet Type,Packet Number是0,并且其中包含一个CRYPTO frame(包含了ClientHello)。
注意多个QUIC Packets(甚至不同类型的包)可以被塞进同一个UDP datagram,因此握手流程中最少只需要4个UDP Datagram,也可以由更多的UDP datagram组成(受协议固有的限制)。 比如说服务端的第一个Datagram中可以同时包含Initial Packet和Handshake Packet。
Client Server
Initial[0]: CRYPTO[CH] ->
Initial[0]: CRYPTO[SH] ACK[0]
<- Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[0]: STREAM[0, "..."] ->
Handshake[1]: ACK[0]
<- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0]
Example 1-RTT Handshake
下两图是0-RTT握手的一个例子,其中有一个Packet包含0-RTT数据。服务端在1-RTT包中对0-RTT数据进行确认,并且客户端发送的1-RTT Packets是在相同的Packet Number空间下的。
Client Server
Initial[0]: CRYPTO[CH]
0-RTT[0]: STREAM[0, "..."] ->
Initial[0]: CRYPTO[SH] ACK[0]
Handshake[0] CRYPTO[EE, FIN]
<- 1-RTT[0]: STREAM[1, "..."] ACK[0]
Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[1]: STREAM[0, "..."] ACK[0] ->
Handshake[1]: ACK[0]
<- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1]
Example 0-RTT Handshake
Client Server
ClientHello
(0-RTT Application Data) -------->
ServerHello
{EncryptedExtensions}
{Finished}
<-------- [Application Data]
{Finished} -------->
[Application Data] <-------> [Application Data]
() Indicates messages protected by Early Data (0-RTT) Keys
{} Indicates messages protected using Handshake Keys
[] Indicates messages protected using Application Data (1-RTT) Keys
TLS Handshake with 0-RTT
密钥推演生成
大概流程
Client Server
====== ======
Install tx Initial Keys
Get Handshake
Initial ------------->
Install tx 0-RTT Keys
0-RTT - - - - - - - ->
Install tx Initial Keys
Handshake Received
Get Handshake
<------------- Initial
Install rx 0-RTT keys
Install Handshake keys
Get Handshake
<----------- Handshake
Install tx 1-RTT keys
<- - - - - - - - 1-RTT
Handshake Received (Initial)
Install Handshake keys
Handshake Received (Handshake)
Get Handshake
Handshake ----------->
Handshake Complete
Install 1-RTT keys
1-RTT - - - - - - - ->
(HANDSHAKE_DONE)
Handshake Received
Handshake Complete
Handshake Confirmed
<--------------- 1-RTT
(HANDSHAKE_DONE)
Handshake Confirmed
TLS1.3 密钥衍生
QUIC密钥衍生
QUIC Connection ID协商
每个连接有一组connection ID:Destination Connection ID由数据包的接收方选择,用于提供一致的路由、Source Connection ID用于设置对端使用的Destination Connection ID。每一个都可以标识这条连接,Connection ID由两端独立选取。
Connection ID的基本功能是用来在底层(UDP,IP层)的地址发生变化时,仍能够使得QUIC连接内的Packet被送达正确的终端。Connection ID除了被用来保证路由的正确性之外,还被接收方终端用来识别Packet属于哪条连接。
Long Header Packet含两个CID:
- Destination Connection ID
- Source Connection ID
Short Header Packet 只包含Destination Connection ID
在握手流程中,Long Header Packet,实际上是使用Initial Packet / Retry Packet 被用来协商双方使用的Connection ID。 每个终端使用Source Connection ID字段来指定己方使用的Connection ID,同时这个字段会被填入向对方回包的Destination Connection ID字段中。在处理完第一个Initial packet后,每个终端用它收到的Source Connection ID来填充它发送的Packet中的Destination Connection ID。
客户端在建连过程中是有可能改变两次Destination Connection ID的,第一次在收到Retry Packet时,第二次在对服务端的第一个Initial Packet进行应答时
Client Server
Initial: DCID=S1, SCID=C1 ->
<- Initial: DCID=C1, SCID=S3
...
1-RTT: DCID=S3 ->
<- 1-RTT: DCID=C1
Negotiating Connection IDs By Initial Packet
Client Server
Initial: DCID=S1, SCID=C1 ->
<- Retry: DCID=C1, SCID=S2
Initial: DCID=S2, SCID=C1 ->
<- Initial: DCID=C1, SCID=S3
...
1-RTT: DCID=S3 ->
<- 1-RTT: DCID=C1
Negotiating Connection IDs By Retry Packet
QUIC Version Negotiation
版本协商允许服务端表示,它不支持客户端使用的版本以及它支持的版本。
客户端发送的第一个数据包的大小将确定服务器是否发送版本协商数据包。支持多个QUIC版本的客户端应确保,他们发送的第一个UDP数据报的大小应为所支持的所有版本中最小数据报大小的最大值,并在必要时使用PADDING帧。这将确保服务端可以回复一个互相都支持的版本。 如果服务端接收到的数据报小于其他版本中指定的最小大小,则服务器可能不会发送版本协商包
如果客户端选择的版本不为服务端所接受,服务器将以版本协商数据包(Version Negotiation Packet)做出响应。内容包括服务端将接受的版本列表。终端不能发送版本协商数据包来响应接收版本协商数据包。
Transport Parameters Negotiation
在建连期间,两端都可以对其Transport Parameters进行校验,要求两端都遵守每个参数定义的限制。
Transport Parameters 如下:(不是全部,详细见RFC9000[18.2])
max_idle_timeout(0x01) 整数值,单位为毫秒的整数,如果两端都没有这个字段,或字段为0表示idle timeout被disable。
max_udp_payload_size(0x03) 最大UDP数据报大小,整数,用来表示终端愿意接收的最大UDP数据报的大小。超过这个参数的UDP数据报被处理的可能性较小。 默认值为UDP payload的最大可能尺寸65527,小于1200的值是无效的。 这个限制和链路MTU类似,是数据报尺寸的另一个限制。但它是一个终端的属性而不是链路的属性。这是一个终端来存放送入数据的(存储)空间的期望值。
initial_max_data (0x04) 整数,表示连接上能发送的最大数据量初始值。在完成握手之后,这个等同于发送MAX_DATA。
initial_max_stream_data_bidi_local (0x05) 整数,表示初始flow control限制(对己方初始化的双向stream生效)。在客户端参数中,这个限制被用来最后2位为0x0的stream上。在服务端参数中,这个被用在最后2位为0x1的stream上。
initial_max_stream_data_bidi_remote (0x06) 整数,表示初始flow control限制(对端初始化的双向stream使用)。这个限制被用在对端启用的双向stream上。在客户端参数中,这个被用在最后2位为0x1的stream上。在服务端参数中,这个被用在最后2位为0x0的stream上。
initial_max_stream_data_uni (0x07) 整数,表示单向stream的初始flow control限制。被用在接收方启用的新单向stream上。在客户端参数中,这个被用在最后两位为0x3的stream上;在服务端参数中,这个被用在最后两位为0x2的stream上。
initial_max_streams_bidi (0x08) 整数,表示对端可以初始化的双向stream最大值。如果值为空或0,对端不可以启用双向stream,直到收到MAX_STREAMS调大限制。这个参数等效于发送具有相同value的相同类型的MAX_STREAMS。
initial_max_streams_uni (0x09) 整数,表示对端可以初始化的单向stream最大值。如果值为空或0,对端不可以启用单向stream,直到收到MAX_STREAMS调大限制。这个参数等效于发送具有相同value的相同类型的MAX_STREAMS。
ack_delay_exponent (0x0a) 整数,表示用来编码ACK Delay字段的指数。如果值缺失,默认值为3(表示乘数为2^3=8)。这个默认值也被用在Initial和Handshake packet中的ACK frame。超过20的值认为是非法的。
max_ack_delay (0x0b) 整数,单位ms,表示终端发送ACK会delay的时间。这个值应当(SHOULD)包含接收方定时器的唤醒时间。举个栗子,如果接收方设置了一个5ms的定时器,定时器通常有1ms唤醒延迟,那么它应当发送6ms的max_ack_delay参数值。如果值缺失,默认值是25ms。2^14或更大的值无效。
QUIC在加密层握手中包含了这些加密的传输层参数。一旦握手完成,这些被对端宣告的传输层参数信息就生效了。两端都会验证其对端提供的值。
应用层协议协商(ALPN;见[TLS ALPN])
Transport Parameters Negotiation 数据是是包含在ClientHello和ServerHello,复用了TLS Extension字段 如下图,