简单看了以下几个QUIC和HTTP3相关RFC文档,对其设计有了一定了解。
RFC8999: Version-Independent Properties of QUIC
RFC9000: QUIC: A UDP-Based Multiplexed and Secure Transport
RFC9114: HTTP/3
RFC9204: QPACK: Field Compression for HTTP/3
RFC8999
RFC8999十分简单 甚至比一些简短的论文还要短,他的目录也十分简单,毕竟只是定义了QUIC协议的一些通识信息。
内容不多,最关键的就是描述了一个长包短包,ID和版本及版本协商相关内容。其中定义结构通过观察流量包内容可以得到验证
上面抓到的Version id都是1,基于RFC9000实现。查看Wireshark源码[wireshark/epan/dissectors/packet-quic.c](epan/dissectors/packet-quic.c · master · Wireshark Foundation / wireshark · GitLab)可以看到其实已经有很多公司内部实现的协议版本了。
以下是原文内容
1. QUIC的一个高度抽象的描述
QUIC 是两个端点之间的面向连接的协议。 这些端点交换 UDP 数据报。 这些 UDP 数据报包含 QUIC 数据包。 QUIC 端点使用 QUIC 数据包来建立 QUIC 连接,这是这些端点之间的共享协议状态。
2. 所有QUIC版本固定的属性
除了提供安全、多路复用的传输功能,QUIC《QUIC协议》还支持版本协商。这将使得协议能够在以后的岁月中为适应新的需求而更新,而协议的诸多特性可以随着版本的迭代而不断改变。
本文阐述QUIC的一些在新的QUIC版本开发及部署的过程中依旧保持不变的属性。所有这些不变属性都与IP版本无关。
本文的主要目标是确保后续QUIC新版本的迭代。通过陈述QUIC的这些不可更改的属性,本文试图维持QUIC终端对QUIC协议任何其他方面的改动进行协商的能力。因此,这也保证了暴露到一条QUIC连接两个终端之外的信息量最少。QUIC协议在本文明确禁止之外的任何方面,都是可以在不同版本之间修改的。
附录A包含一份非详尽的清单,列出了一些基于对QUIC版本1的了解可能做出的不正确的猜想,这些不适用于QUIC的任何版本。
3. 规约及定义
4. 标准规范
复杂的字段被命名后,由紧随命名的一个以一对花括号括起来的字段列表描述,列表中的字段以逗号分隔。
单个字段包括长度信息、带正号的定值、可选值或本字段的副本。单个字段使用下述标准规范,且所有长度都以比特为单位:
x (A)
: 表示x
是A
比特长度
x (A..B)
: 表示x
的长度可以是从A
到B
的所有值,省略A
表示最小零位,并且省略B
表示没有设置上限。这种格式的值总是以字符边界结束。
x (L) = C
: 表示x
有一个定值C
,且x
的长度为L
,L
可以用上述任何长度格式
x (L) ...
: 表示x
重复0次或以上次数,且每个实例长度为L
本文使用网络字节序(大端序)
示例结构:
|
|
5. QUIC Packets
QUCI终端之间交换包含一个或多个QUIC数据包的UDP报文。本章描述QUIC包的不变特性。一个版本的QUIC准许多个QUIC数据包包含于同一个UDP报文里,但是其不变的特性只要求报文里的首个数据包保持。
QUIC定义了两类数据包头:长包头与短包头。有长包头的数据包由其首个字节的首个比特位设置为1判定,而短包头数据包这个比特位设置为0。
QUIC数据包可能被完全保护,包括包头。然而,QUIC版本协商包不会被完全保护,详见第6章。
除了这里描述的值之外,QUIC数据包的有效负载是由各个版本决定且任意长度的。
5.1 长Header
长Header格式
|
|
QUIC长包头数据包其首字节最高比特位设置为1,其余比特位则视具体版本而定。
随后的四个字节包含一个32位版本字段,关于版本详见第5.4章。
接下来的一个字节包含紧随其后的目标连接ID字段的长度,且该长度值以字节计数,并被编码为一个8位无符号整数。目标连接ID字段紧随目标连接ID长度字段,其长度在0到255字节之间。连接ID详见第5.3章。
接下来的一个字节包含紧随其后的源连接ID字段的长度,且该长度值以字节计数,并被编码为一个8位无符号整数。源连接ID字段紧随源连接ID长度字段,其长度在0-255字节之间。
数据包接下来剩余字段包含与QUIC版本特定相关的内容。
5.2 短Header
短Header格式
|
|
短包头QUIC数据包的首字节的最高位设置为0。
短包头数据包紧随首字节之后是一个目标连接ID。短包头不会包含目标连接ID长度、源连接ID长度、源连接ID或版本字段。目标连接ID的长度不会编码在短包头数据包里,也不会受限于这个特性。
数据包接下来剩余字段有与版本特定相关的语义。
5.3 连接ID
连接ID是一个任意长度的不透明字段。
连接ID的主要功能是确保底层协议(UDP、IP及更底层的协议栈)发生地址变更时不会导致一个QUIC连接的数据包被传输到错误的QUIC终端上。连接ID由终端及支持的中间设备用以确保每个数据包能够被调度到相应终端的正确实体上。对于终端而言,连接ID用于标识数据包对应的QUIC连接。
连接ID由每个终端根据版本特定的方式选择,而同一个QUIC连接的数据包可能使用不同的连接ID。
5.4 版本
版本字段包含一个4字节标识符。该值可供终端用以标识一个QUIC版本。值为0x00000000
的版本字段保留给版本协商使用,详见第6章,而任何其余值均可能有效。
本文描述的属性适用于所有版本的QUIC。不符合本文所述属性的协议不是QUIC协议。后续文档可以给某个特定QUIC版本或一系列QUIC版本增加其他的属性。
6. 版本协商
QUIC终端收到一个带长包头的数据包及一个其不能理解或不支持的版本时,就可能回复一个版本协商包。短包头数据包不会触发版本协商。
版本协商包设置上首个字节的高位,也就是说它是一个在第5.1章定义的长包头数据包。版本协商包可以通过其版本字段识别,因为这个字段会被设置为0x00000000
。
版本协商包
|
|
版本协商包只有首字节最重要的那个比特位有着任意定义的值。其剩余的7个比特位标记为“未使用”,在发送的时候可以被设置为任意值,且必须被接收端忽略。
版本协商包紧随源连接ID字段后面的是一个支持版本字段的列表,该列表每个字段标识终端能够支持的版本(也就是客户端支持的QUIC版本的版本号挨个列下去)。版本协商包不包含其他字段,终端必须忽略不包含支持版本字段或包含不完整版本值的包。
版本协商包不使用完全的或加密的保护。某些特定的QUIC版本可能包含准许终端鉴别支持版本列表里的值是否存在修改或损坏的协议手段。
终端必须在其目标连接ID字段包含它收到的数据包的源连接ID值。必须将收到数据包的目标连接ID字段值复制给源连接ID字段,该值是客户端随机产生的。回显两个连接ID给客户端一些保证,表明服务端收到了包,且版本协商包不是由对其不可见的攻击者生成的。
终端收到版本协商包后,可以在随后的数据包中更改QUIC版本。终端更改QUIC版本的条件将取决于其选择的版本。
《QUIC协议》详细描述了支持QUIC版本1的终端如何创建及处理一个版本协商包。
7. 安全及隐私考量
中间件可能能够识别特定版本的QUIC的一些特性,并假设其他版本的QUIC表现出类似特征时,其底层正在表达一致的语义。这样的特性有好几个,详见附录A。QUIC版本1已经做了一些工作消除或隐藏一些可见的特征,但是许多这样的特性仍然维持现状。后续其他QUIC版本可能改变设计从而表现出不同的特征。
QUIC版本号不会出现在所有QUIC数据包中,这意味着以一个基于版本的特征从一条流中可靠地提取信息需要中间件为每个连接ID保留状态。
本文所述版本协商包不会被完整保护,仅仅对攻击者的介入做了适度保护。如果终端试图改用不同的QUIC版本,那么它必须验证版本协商包的语义内容。
附录A:不正确的猜想
QUIC版本1《QUIC协议》有一系列特性没有被保护为外部不可见,但是一般认为可以在后续的QUIC版本中更改。
本章列出了基于QUIC版本1的了解可能做出的一些错误猜想,其中一些陈述甚至对QUIC版本1也是错误的。这个列举并不充分,目的在于做一些阐述。
对于一个QUIC版本,下述任意一条甚至所有的陈述都可能是错的:
- QUIC使用TLS《QUIC TLS》,而链路上一些TLS的信息是可见的。
- QUIC长包头只在连接建立阶段发生更改。
- 五元组上的每条流都会包含一个连接建立过程。
- 一条流的前几个包使用长包头。
- 在一段长时间的静默前的最后一个包只包含一个确认信息。
- QUIC使用关联数据加密认证(AEAD)函数(AEAD_AES_128_GCM,详见RFC5116保护在连接建立阶段交换的数据包。
- QUIC数据包号是加密的,且是数据包的初始加密字节。
- QUIC数据包号每发送一个包递增1。
- 客户端发送的首个QUIC握手包不能小于一个特定尺寸。
- QUIC规定客户端发送第一个包。
- QUIC数据包首字节的第二个比特位总是设置为1的(0x40)。
- QUIC版本协商包只会由服务端发出。
- QUIC连接ID很少发生改变。
- QUIC终端会在发送完一个版本协商包后改变QUIC版本。
- QUIC长包头的版本字段在两个方向上都是一样的。
- QUIC包版本字段有一个代表某QUIC版本的值意味着连接用的就是相应的QUIC版本。
- 任何一对QUIC终端之间一次只有一个QUIC连接建联。
RFC9000
QUIC是一个面向连接的协议,在客户端及服务端之间建立有状态的交互。
QUIC握手由密钥协商及传输参数协商组成。QUIC集成了TLS握手《TLS 1.3》,同时以自定义的帧保护数据包。更多关于TLS与QUIC集成的细节描述详见《QUIC-TLS》。握手过程被设计成支持尽早交换应用数据(0-RTT),包含一个需要通过某种形式的提前交流或配置来开启的客户端选项。
终端通过QUIC交流是以交互QUIC数据包的形式实现的。大多数数据包装载着一个或多个在终端间搬运控制信息和应用数据的帧。QUIC会验证每个包的内容,并根据实际情况对每个数据包进行加密。QUIC数据包通过UDP报文《UDP》传输从而能够更好地支持现有的系统及网络环境。
应用层协议通过流在QUIC连接上交换信息,每条流都是有序的字节序列。流的类型分两种:双向流,支持双端发送数据;以及单向流,只支持一端发送数据。QUIC使用一种基于额度的方案来限制流的创建以及每条流可以发送的数据量。
QUIC以提供必要反馈的方式实现可靠传输及拥塞控制,《QUIC恢复》第6章描述了QUIC的一种数据丢失及恢复算法;QUIC通过拥塞控制避免网络拥塞,《QUIC恢复》第7章描述了QUIC的一种典型拥塞控制算法。
QUIC连接不会严格限制在一条单独的网络通道上。连接迁移根据连接标识符将连接迁移到一个新的网络通道上。当前版本QUIC只支持客户端进行连接迁移。在改变网络或地址映射——如NAT重定向——后,这个设计使连接仍然能够继续下去而不会断开。
有多种方式可以关闭连接。应用程序可以平滑关闭连接;双端可以协商一个超时时间段在超时后关闭连接;触发错误能够立即断开连接;一端失去状态后也能通过一种无状态机制关闭连接。
文档结构
流是QUIC支持的基本服务抽象层。
- 第2章描述流相关的核心概念,
- 第3章提供一个流状态的参考模型,
- 第4章概述流量控制的过程。
连接是QUIC终端交流的上下文。
- 第5章描述连接相关的核心概念,
- 第6章描述版本协商
- 第7章详细描述连接建立的过程,
- 第8章描述地址验证及危险的拒绝服务迁移攻击,
- 第9章描述终端如何将一个连接迁移到新的网络通道上,
- 第10章列举关闭一个已打开连接的各个方式,以及
- 第11章给流与连接错误处理提供指导。
数据包和帧是QUIC交流的基本单元。
第12章描述数据包与帧相关的概念,
第13章定义数据传输、重传和确认的模型,以及
第14章描述指定携带QUIC数据包的数据报大小的规则。
最后,QUIC协议要素的编码细节描述在:
- 第15章版本,
- 第16章整型编码,
- 第17章数据包头部,
- 第18章传输参数,
- 第19章帧,以及
- 第20章错误。
常用术语:
QUIC:本文描述的传输协议。QUIC是名称,不是首字母缩写。
终端(Endpoint):一个能够以创建、接收及处理QUIC数据包参与QUIC连接的实体。QUIC终端有两种类型:客户端(client)及服务端(server)。
QUIC数据包:QUIC的一个可以封装进UDP报文中的完整处理单元。单个UDP报文可以封装进一个或多个QUIC数据包。
ACK触发包:一个包含除确认帧(ACK)、填充帧(PADDING)及连接关闭帧(CONNECTION_CLOSE)外的帧的QUIC数据包。接收方收到这类包会发确认,详见第13.2.1章。
帧:一个结构化的协议信息单元。帧有多种类型,不同类型的帧携带不同类型的信息。帧由QUIC数据包承载。
地址:当使用不受限制,由IP版本、IP地址及UDP端口号构成的元组表示网络通道的一端。
连接ID:终端用来标识一条QUIC连接的标识符。每个终端选择一个或多个连接ID,从而在对端发送给本端的QUIC包中包含这些连接ID。该值对对端是不透明的。
流:QUIC连接上一个单向或双向的有序字节通道。一个QUIC连接可以同时承载多条流。
应用:一个使用QUIC发送及接收数据的实体
字段信息表述同RFC8999
示例结构体
|
|
0x2、流
流ID的最小有效位(0x01)标识流的发起者。客户端发起的流的ID是偶数(该位被置为0),服务端发起的流的ID是奇数(该位被置为1)。
流ID的次小有效位(0x02)标识流是双向流(该位被置为0)抑或单向流(该位被置为1)。
位 | 流类型 |
---|---|
0x00 | 客户端创建的双向流 |
0x01 | 服务端创建的双向流 |
0x02 | 客户端创建的单向流 |
0x03 | 服务端创建的单向流 |
表格1:流类型
每种流类型的流空间从其最小值开始(依次从0x00到0x03);每种流的每个流ID根据创建顺序依次线性递增。如果不按顺序地使用了一个流ID,将导致相同流类型的所有具有更小的流ID的流都被开启。
终端可以从一条流的同一个偏移位置多次接收数据。如果数据已经被接收过了,就可以直接被丢弃。
0x24、流操作
本文没有定义QUIC API,而是定义了一系列流操作相关的函数可以用于应用层协议的构建。应用层协议可以假定QUIC有关实现提供了本章描述的操作对应的接口。为一个特定应用层协议设计实现的QUIC协议可能仅仅提供该协议需要的这些操作。
在流的发送部分,应用层协议可以:
- 写数据,只有当流量控制给数据写出留足空间(第4.1章)才能成功写出;
- 结束流(清理并关闭),发送一个设置FIN位为1的流帧(第19.8章);
- 重置流(中止并关闭),当流未处在终止状态时发送一个RESET_STREAM帧(第19.4章)。
在流的接收部分,应用层协议可以:
- 读数据,以及
- 中止读取流数据并请求关闭流,该操作可能需要发送STOP_SENDING帧(第19.5章)。
应用层协议也可以请求在流状态改变的时候收到通知信息,包括当对端开启或重置流、对端中止流数据读取、有新数据可以读取、以及数据可以写出或因流控不能写出。
0x3、流状态
本章描述流的发送及接收相关组件。有两个状态机需要描述:一个是关于终端传输数据(第3.1章)的流,另一个是关于终端接收数据(第3.2章)的流。
单向流用到发送或接收的其中一个状态机,取决于流类型及终端角色。双向流双端都会用到两个状态机。在极大程度上,不论单向流还是双向流,在使用这两个状态机上是没有区别的。相对而言打开一条双向流会稍微复杂一些,因为同时打开发送和接收端意味着在两个方向上同时打开流。
本章展示的状态机极具信息量。本文使用流状态描述不同类型帧在何时以何种方式发送的相关规则,以及当收到不同类型的帧时应作出的反应。即使这些状态机目的在于指导如何实现QUIC协议,但其并不意味着限制QUIC实现的方式。一个QUIC实现完全可以定义不同的状态机,只要其行为与本文所述状态机的具体实现一致即可。
0x31、发送流状态
|
|
0x32、接收流状态
|
|
终端在收到最大流数据帧或停止发送帧后打开一条双向流。接收到一条未开启的流的最大流数据帧意味着对端已经开启了该流,并开始支持流量控制额度。而接收到一条未开启流的停止发送帧意味着对端不会再从该流接收数据。无论这两种帧的哪一种都可能先于流帧或流阻塞帧到达本端,原因是包丢失或乱序。
在一条流创建前,所有数值小于该流ID的同类型流都必须被创建。这样能确保双端流的创建次序保持一致。
在“接收”状态,终端接收流帧和流阻塞帧。传入数据将被缓存,并可以按照正确顺序重组以便递给应用层。随着应用层不断消耗数据,缓冲区重新空出来,终端发送最大流数据帧告知对端可以发送更多数据。
当收到一个带FIN置位的流帧时,数据的最终大小确定下来,详见第4.5章。流的接收部分随后转到“数据量确认”状态。在此状态,终端不再需要发送最大流数据帧,只需要接收重传数据即可。
一旦收完了一条流的所有数据,流的接收部分转入“接收完成”状态。这可能发生在收到导致转入“数据量确认”状态的同一个流帧后。在所有数据都收完后,可以丢弃该流的任何流帧或流阻塞帧。
0x33、Permitted Frame Types
流的发送端发送的帧只有三种能同时影响发送端和接收端状态:STREAM(第19.8章)、STREAM_DATA_BLOCKED(第19.13章),以及RESET_STREAM(第19.4章)。
流的接收端发送MAX_STREAM_DATA frames(第19.19章)及STOP_SENDING frames(第19.5章)。
0x34、双向流状态
可能的对于HTTP2的状态映射 在最简单的模型里,当发送和接收部分均处在非最终状态时,表示流处于“打开”状态;当两者均处于最终状态时,表示流处于“关闭”状态。
Table 2: Possible Mapping of Stream States to HTTP/2
Sending Part | Receiving Part | Composite State |
---|---|---|
No Stream / Ready | No Stream / Recv (*1) | idle |
Ready / Send / Data Sent | Recv / Size Known | open |
Ready / Send / Data Sent | Data Recvd / Data Read | half-closed (remote) |
Ready / Send / Data Sent | Reset Recvd / Reset Read | half-closed (remote) |
Data Recvd | Recv / Size Known | half-closed (local) |
Reset Sent / Reset Recvd | Recv / Size Known | half-closed (local) |
Reset Sent / Reset Recvd | Data Recvd / Data Read | closed |
Reset Sent / Reset Recvd | Reset Recvd / Reset Read | closed |
Data Recvd | Data Recvd / Data Read | closed |
Data Recvd | Reset Recvd / Reset Read | closed |
0x35、请求状态转换
A STOP_SENDING frame 请求接收方发送 a RESET_STREAM frame
如果双向流的一端想要将流的两个方向同时关闭,那么其可以通过发送一个RESET_STREAM关闭一个方向,并发送一个STOP_SENDING促使相反方向也迅速得到关闭。
0x4、流量控制
接收方需要限制缓存数据量以防发送方速度太快造成冲击或被恶意发送方消耗大量内存。为了让接收方能够限制连接的内存占用,不仅每条流有单独的流量控制,所有流也作为一个整体在连接层面有统一的流量控制。QUIC接收方控制发送方在一条流上以及任何时刻在所有流上可以发送的最大数据量,详见第4.1章或第4.2章。
同样地,为了限制连接并发,QUIC终端控制对方可以同时开启的最大流数量,详见第4.6章。
通过加密帧发送的数据不像流数据那样受流量控制制约。QUIC依赖于加密协议的实现来避免这些数据被过量缓存,详见《QUIC TLS》。为了防止在多个层次过量缓存数据,QUIC实现应该为加密协议实现提供一套接口以供其交流缓存区限制。
0x41、数据流量控制
QUIC使用一个基于限制的流量控制模型,接收者给出其准备在给定流或整个连接上准备接收的总字节数的上限。这使得QUIC中存在两层数据流量控制:
- 流的流量控制:通过限制每条流可以发送的数据量,防止单条流耗尽一条连接的全部接收缓冲区;
- 连接流量控制:通过限制所有流经由流帧可以发送的数据量,防止发送方超过连接接收方的缓冲区容量。
接收方在握手过程中(第7.4章)通过传输参数为所有流设置初始的流接收缓存区上限。随后,接收方发送最大流数据帧(第19.10章)或最大数据帧(第19.9章)以告知对方提高流接收缓存区上限。
如果发送方发送数据达到了流量控制上限,其将不能再发送新数据,且应认为其被阻塞住了。发送方应该发送一个流数据阻塞帧或数据阻塞帧来告知接收方其有数据要写出但是被流量控制所阻塞。如果发送方被阻塞的时间超过空等超时时间(第10.1章),接收方可以关闭连接,即便发送方有可传输的数据。为了保持连接不被关闭,在没有可引发ACK的数据包处于传输中时,被流量控制限制所阻塞的发送方应该定期发送一个流数据阻塞帧或数据阻塞帧。
0x43、流量控制性能
如果终端不能确保其对端始终在该连接上有大于对端带宽时延积的流量控制额度,其接收吞吐量将被流量控制限制。
包丢失会导致接收缓冲区出现空隙,从而阻碍应用层消耗数据并释放接收缓冲空间。
及时发送流量控制上限更新能提高性能。发送只包含流量控制更新的数据包会增加网络负载,对性能产生不利影响。将流量控制更新与其他帧一起发出,例如如ACK帧,可以降低此类更新带来的消耗。
0x45、流最终数据量
不管流是如何终止的,发送方始终试图将流的最终数据量可靠地发送给接收方。最终数据量是 带有FIN置位的流帧的Offset(下标)和Length(长度)字段值的总和,注意这些字段可能是隐式的。
0x46、并发控制
终端限制对端累计可以开启的流的数量。只有流ID小于(max_streams * 4 + first_stream_id_of_type)的流可以被开启,详见表1。初始限制由传输参数设置,详见第18.2章。随后的限制由最大流帧推出
max_streams不能超限$2^{60}$
0x5 连接
0x53 连接操作
当实现用户端时,应用层协议可以:
- 创建一个连接,开始进行第7章描述的交互过程;
- 如果支持,启用早期数据功能;
- 当早期数据被服务端接受或拒绝时,收到通知。
当实现服务端时,应用层协议可以:
- 监听传入的连接,准备进行第7章描述的交互过程;
- 如果支持早期数据,在发送给客户端的TLS恢复ticket中嵌入应用层控制数据;
- 如果支持早期数据,从接收自客户端的恢复ticket中恢复应用层控制数据,并根据该信息接受或拒绝早期数据。
当同时实现客户端及服务端时,应用层协议可以:
- 如传输参数([第7.4章])所述,为每种类型允许的流的配置最小的初始数量;
- 通过设置流级别及连接级别的流量控制限制,限制接收缓存区资源分配;
- 识别握手已经成功结束抑或仍在进行中;
- 保持连接不被默认关闭,即通过PING帧([第19.2章])或其他请求使得传输层在空闲超时([第10.1章])前发送额外的帧;以及
- 立即关闭连接([第10.2章])。
0x7 加密与传输握手
1-RTT example
|
|
0-RTT example
|
|
0x12 Packets and Frames
0x124 Frames and Frame Types
|
|
figure:QUIC payload
|
|
figure:Generic Frame layout
Type Value | Frame Type Name | Definition | Pkts | Spec |
---|---|---|---|---|
0x00 | PADDING | Section 19.1 | IH01 | NP |
0x01 | PING | Section 19.2 | IH01 | |
0x02-0x03 | ACK | Section 19.3 | IH_1 | NC |
0x04 | RESET_STREAM | Section 19.4 | __01 | |
0x05 | STOP_SENDING | Section 19.5 | __01 | |
0x06 | CRYPTO | Section 19.6 | IH_1 | |
0x07 | NEW_TOKEN | Section 19.7 | ___1 | |
0x08-0x0f | STREAM | Section 19.8 | __01 | F |
0x10 | MAX_DATA | Section 19.9 | __01 | |
0x11 | MAX_STREAM_DATA | Section 19.10 | __01 | |
0x12-0x13 | MAX_STREAMS | Section 19.11 | __01 | |
0x14 | DATA_BLOCKED | Section 19.12 | __01 | |
0x15 | STREAM_DATA_BLOCKED | Section 19.13 | __01 | |
0x16-0x17 | STREAMS_BLOCKED | Section 19.14 | __01 | |
0x18 | NEW_CONNECTION_ID | Section 19.15 | __01 | P |
0x19 | RETIRE_CONNECTION_ID | Section 19.16 | __01 | |
0x1a | PATH_CHALLENGE | Section 19.17 | __01 | P |
0x1b | PATH_RESPONSE | Section 19.18 | ___1 | P |
0x1c-0x1d | CONNECTION_CLOSE | Section 19.19 | ih01 | N |
0x1e | HANDSHAKE_DONE | Section 19.20 | ___1 |
0x16 可变长整型
最高的2个有效位 | 字节长度 | 可用位数 | 可表示范围 |
---|---|---|---|
00 | 1 | 6 | 0-63 |
01 | 2 | 14 | 0-16383 |
10 | 4 | 30 | 0-1073741823 |
11 | 8 | 62 | 0-4611686018427387903 |
0x17 Packet Formats
|
|
Long Packet Type
类型 | 名称 | 章节 |
---|---|---|
0x00 | 初始 | [第17.2.2章] |
0x01 | 0-RTT | [第17.2.3章] |
0x02 | 握手 | [第17.2.4章] |
0x03 | 重试 | [第17.2.5章] |
|
|
|
|
0x18 传输参数编码
|
|
0x19 帧类型和格式
|
|
RFC9114
HTTP/1.1直接使用空格和crlf来分割传递数据,虽然可读性高但是网络利用率不高,多个请求使用对个TCP连接并行请求还会加重拥塞。
HTTP/2引入了binary framing和multiplexing layer,在不修改传输层的清苦那个下改善延迟。然而,HTTP/2的多路复用并行性质对TCP的丢失机制是不可见的(TCP向上提供透明服务),一个丢失或者重新排序的数据包会导致所有的活动事务经历一个暂停,而不管该事务是否直接收到丢失数据包的影响。
QUIC协议合并了stream multiplexing和per-stream control,通过提供流级别的可靠性和整个连接的拥塞控制,提高HTTP性能。QUIC在传输层合并了TLS 1.3提供HTTPS的机密性和完整性,改进了TCP Fast Open的连接设置延迟。
QUIC提供了协议协商、基于流的多路复用和流控制。HTTP/3请求的多路复用是使用QUIC抽象流来执行的,每个请求响应对消耗单一的QUIC流,流之间相互独立,一个流的阻塞和抛弃不会影响其他流。
每个流中,HTTP/3通信的基本单元式frame。每个frame都有不同的用途,例如HEADERS
和DATA
frame构成了HTTP请求和相应的基础。
server push是HTTP/2中引入的交互模式,允许server在client发出指定请求前想clent推送一个request-respose exchange。几个HTTP/3 frame被用来管理server push,如PUSH_PROMISE
、MAX_PUSH_ID
和CANCEL_PUSH
.
文档组织
- 第三章介绍了如何发现HTTP/3断点和如何建立连接
- 第四章描述如何使用frame来表达HTTP语义
- 第五章描述了HTTP/3连接时如何被优雅或者突然地停止的
- 第六章描述了使用QUIC流的方式
- 第七章描述了大多数流上使用的帧
第三章、连接设置和管理
发现HTTP/3 endpoint
client访问https的uri资源,解析成IP地址,在指定端口建立QUIC连接,通过安全连接向server发送HTTP/3请求。连接问题(阻塞UDP)可能导致无法建立QUIC连接,这种情况client尝试基于tcp版本的HTTP
可以通过HTTP相应头字段告知端口支持HTTP/3协议
Alt-Svc: h3=":50781"
接收到Alt-Svc响应client尝试建立QUIC连接
连接建立
QUIC建立连接,使用TLS1.3以上进行握手,握手中通过选择APLN令牌h3表示HTTP/3支持,其他应用层协议的支持可以在相同握手中提供。QUIC核心协议相关选项在初始加密握手设置时,HTTP3的设置传入到SETTINGS
frame。QUIC连接建立后,SETTINGS
frame必须被每个端点作为HTTP控制流的初始frame发送。
连接复用
为了使用一个现有的连接到一个新的源,客户端必须验证服务器为新的源服务器提供的证书,这意味着客户端需要保留服务器证书和验证该证书需要的任何额外信息,不这样做的客户端将无法为其其他来源重用连接。
第四章、在HTTP3中表达HTTP语义
一个HTTP消息包括
-
- header,sent as single HEADERS frame
-
- Optional content,sent as a series of DATA frames
-
- Optional tailer section, sent as a single HEADERS frame
接收到无效的帧序列作为 H3_FRAME_UNEXPECTED
类型的连接错误,如DATA在HEADERS前
服务端可以在响应消息交错推送PUSH_PROMISE
帧,包含PUSH_PROMISE
帧的推送响应必然被视为H3_FRAME_UNEXPECTED
类型的错误连接
HTTP请求响应完全消费双向QUIC流,当client发送请求后必须关闭发送流,客户端绝对不能让流关闭依赖于接收到对其请求的响应,在发送最终响应后,服务端必须关闭发送流。这时,QUIC流完全关闭。
如果客户端发起流在没有提供足够HTTP message以供响应而终止,server abort错误码H3_REQUEST_INCOMPLETE
H3_REQUEST_REJECTED
vs H3_REQUEST_CANCELLED
server不能使用REJECTED处理部分响应请求
完整响应被取消可以使用 部分响应被取消不能使用 幂等请求可安全重试
畸形请求以H3_MESSAGE_ERROE
的流错误处理
HTTP Fields
QPACK(HTTP3)描述了HPACK(HTTP2)的变体,压缩包头和尾部部分,包括出现在报头部分的控制数据。为了获得更好的压缩效率,Cookies字段可能被分成多个fields line,如果解压缩的字段包含多个cookie字段,这些行必须以;
(0x3b 0x20)分割
server收到header过大可以发送一个HTTP 431状态码
HTTP Control Data
伪报头以:
(0x3a)开头 携带控制数据,请求伪报头不能出现在响应中 反之亦然,伪报头不能出现在尾部部分
所有HTTP/3请求必须包含 :method
, :scheme
, and :path
伪首部
响应是 :status
伪报头 携带状态码
HTTP3不支持HTTP升级机制
Server Push
推送ID从0开始,通过MAX_PUSH_ID
frame控制服务器可以承诺的最大推送ID
ID用于一个或多个PUSH_PROMISE
frames 携带控制数据和报头字段,这些frames发送到生成push的request stream,这允许server push和client请求关联,当相同的pushID被承诺在不同的request streams中时,解压后的request field必须包含相同的同顺序fields,键值也必须相同。然后pushID被包含在最终实现这些promise的push steam中,push stream识别他实现的pushID然后包含promised request对应的response
reordering,数据可能在PUSH_PROMISE
前到达,客户端缓冲数据等待匹配。
第六章、Stream Mapping and Usage
QUIC stream提供可靠的按序bytes,传输层对接收到的流数据进行缓冲和排序,向应用程序expose一个可靠的字节流。QUIC流可以是双向的也可以是单向的,可以由客户端发起也可以由服务端发起。HTTP使用QUIC发送数据,QUIC层处理大部分流管理,HTTP不需要做任何单独的多路复用处理,通过QUIC流发送的数据总是映射到一个特定的HTTP事务或整个HTTP3连接上下文。
双向流
所有客户端发起的双向流都用于HTTP请求和响应。双向流确保响应可以很容易地与请求相关联。这些流被称为请求流。
这意味着客户端第一个请求发生在QUIC流 0上,随后的请求发生在流4、流8上,以此类推。为了允许这些流打开,HTTP/3服务器应该为允许的流的数量和初始流的流量控制窗口配置非零的最小值。为了减少并行度限制,一次至少应该允许100个请求流。
单向流
任何一个单向流都可以用于不同的用于,用途由流类型表示,在流的开头以变长整数形式发送,这个整数后面的数据格式和结构由流类型决定。
单向流header:
|
|
本文档定义了控制流和push流,每个端点需要为HTTP控制流创建至少一个单向流,QPACK需要两个额外的单向流,因此server client发送的传输参数必须允许对等端创建至少三个单项流。
Control Streams
控制流由0x00表示,该流上的数据由HTTP3 frames组成
每一方必须在连接开始时启动一个控制流,并将其SETTINGS
frame作为第一个frame发送
Push Streams
推送流由0x01表示,后面跟着他锁实现的承诺的push ID,编码为边长整型,该流剩余数据又HTTP3 frames组成
push stream header:
|
|
Reserved Stream Types
第七章、HTTP Framing Layer
下表表示HTTP3 frames和他们允许的流类型
Frame | Control Stream | Request Stream | Push Stream | Section |
---|---|---|---|---|
DATA | No | Yes | Yes | Section 7.2.1 |
HEADERS | No | Yes | Yes | Section 7.2.2 |
CANCEL_PUSH | Yes | No | No | Section 7.2.3 |
SETTINGS | Yes (1) | No | No | Section 7.2.4 |
PUSH_PROMISE | No | Yes | No | Section 7.2.5 |
GOAWAY | Yes | No | No | Section 7.2.6 |
MAX_PUSH_ID | Yes | No | No | Section 7.2.7 |
Reserved | Yes | Yes | Yes | Section 7.2.8 |
Frame Layout
所有的frame有下面的格式
|
|
Type:变长整数表示frames类型
Length:边长整数表示frames长度
Frame Payload:数据
Frame Definitions
DATA
DATA frame 0x00 可变长字节序列
|
|
HEADERS
headers 0x01 用于携带QPACK编码的HTTP fields
|
|
headers frame 只能在请求流或者push stream中发送
CANCEL_PUSH
cancel_push 0x03 用来在接收到推送流之前取消server push
client发送表示不想要
server发送表示不会履行承诺
携带一个push ID,标识被取消的server push
|
|
Settings
settings 0x04 传递配置参数 比如对等行为的偏好和约束,应用于整个HTTP3连接而不是整个流
settings frame只能在control stream上发送
|
|
忽略所有不理解的标识符参数
PUSH_PROMISE
push_promise 0x05 用于在请求流中携带从服务器到客户端的被承诺的请求报头片段
|
|
encoded field section被承诺响应的QPACK请求报头字段
客户端绝对不能发送PUSH_PROMISE frame
GOAWAY
GOAWAY frame 0x07 用于启动HTTP3连接的任意断点的优雅关闭
|
|
只能在控制流上传输,应用于整个连接
MAX_PUSH_ID
max_push_id 0x0d 被客户端用来控制服务器可以发起的服务器推送数量。
只能在控制流上发送,server不能发送,server在收到MAX_PUSH_ID前不能推送,frame携带一个边长整数用来标识server可以使用的推送ID最大值。
|
|
Reserved Frame Types
RFC9204
可以在quic-go源码中看到其handleConn中第一步就是qpack.NewDecoder(nil)
创建一个qpack decoder
其使用的库是"github.com/marten-seemann/qpack",而qpack也是RFC9204规定的HTTP/3的字段压缩方式
Abstract
该规范定义了 QPACK:一种用于有效表示将在 HTTP/3 中使用的 HTTP 字段的压缩格式。这是 HPACK 压缩的一种变体,旨在减少行头阻塞(HOL blocking)
2、压缩过程概述
与HPACK相似,QPACK使用两个表(第三章的Static table和Dynamic table)将字段行(“头”)与索引相关联。Static table
是预定义的,包含常见的头字段行(其中一些是空值)。Dynamic table
动态表是在连接过程中建立的,Encoder
编码器可以使用它来索引编码字段段中的头字段行和尾字段行。
QPACK定义了单向流,用于将指令从编码器发送到解码器,反之亦然。
2.1、Encoder
编码器通过为列表中的每个字段行发出索引或文字表示,将头部或尾部部分转换为一系列表示;参见4.5节。索引表示通过用静态或动态表的索引替换文字名称和可能的值来实现高压缩。对静态表和文字表示的引用不需要任何动态状态,而且永远不会有行首阻塞的风险。如果编码器没有收到表示该条目在解码器中是可用的确认信息,则引用动态表可能会导致行首阻塞
编码器可以在它选择的动态表中插入任何条目,不局限于他在压缩的字段行。
QPACK保留了每个字段部分中字段行的顺序。编码器必须按照字段在输入字段部分中出现的顺序发出字段表示。
QPACK的设计目的是将可选状态跟踪的负担放在编码器上,从而产生相对简单的解码器
2.1.1.动态表插入的限制
2.1.2.阻塞流
2.1.3.避免流控制死锁
2.1.4.已知接收计数
已知接收计数是解码器确认的动态表插入和重复的总数。编码器跟踪已知的接收计数,以便识别哪些动态表条目可以被引用,而不会潜在地阻塞流。解码器跟踪已知的接收计数,以便能够发送插入计数增量指令。
Acknowledgment指令(章节4.4.1)意味着解码器已经收到了解码字段Section所需的所有动态表状态。如果确认字段部分的“所需插入计数”大于当前的“已知接收计数”,则“已知接收计数”将更新为“所需插入计数”值。(Known Received Count/Required Insert Count)
插入计数递增指令(第4.4.3节)通过其递增参数递增已知接收计数。
2.2、Decoder
与HPACK一样,解码器处理一系列表示并发出相应的字段部分。它还处理从编码器流接收到的修改动态表的指令。请注意,编码字段部分和编码器流指令在不同的流上到达。这与HPACK不同,在HPACK中,编码字段部分(头块)可以包含修改动态表的指令,并且没有HPACK指令的专用流。
解码器必须按字段表示在编码字段部分中出现的顺序发出字段行。
2.2.1.阻塞解码
2.2.2.状态同步
2.2.3.无效参考
3、Reference Tables
与HPACK不同,QPACK静态表和动态表中的条目是分别寻址的。下面的部分描述了每个表中的条目是如何寻址的
3.1、Static Table
静态表由预定义的字段行列表组成,每条字段行都有一个随时间变化的固定索引。
静态表中的所有条目都有一个名称和一个值。但是,值可以为空(即长度为0)。每个条目由唯一的索引标识
QPACK从0开始索引 HPACK从1开始索引
3.2、Dynamic Table
动态表由一个以先进先出的顺序维护的字段行列表组成。QPACK编码器和解码器共享一个最初为空的动态表。编码器将条目添加到动态表中,并通过编码器流上的指令将它们发送给解码器
可以有重复条目,表项可以有空值。
3.2.1. Dynamic Table Size
size = len(name) + len(value) + 32
3.2.2. Dynamic Table Capacity and Eviction
编码器设置动态表的容量作为其最大值 初始容量为0 编码器发送一个具有非零值的Set Dynamic Table Capacity指令开始使用动态表。
大小不够插入就移出一个表项
新条目可以引用之前的表项,要注意移出的时候避免移出被引用表项。
3.2.3. Maximum Dynamic Table Capacity
约束解码器内存需求,解码器限制编码器允许为动态表容量设置的最大值。由解码器发送的SETTING_QPACK_MAX_TABLE_CAPACITY决定,编码器不能设置超过这个最大值。
3.2.4. Absolute Indexing
每个条目都拥有一个绝对索引,该索引在条目的生命周期内是固定的。插入的第一个条目的绝对索引为0;每插入一次,索引就增加1
3.2.5. Relative Indexing
相对索引从零开始,向与绝对指数相反的方向增长。确定哪个条目的相对索引为0取决于引用的上下文
在编码器指令(章节4.3)中,0的相对索引指的是动态表中最近插入的值。注意,这意味着在解释编码器流上的指令时,给定相对索引引用的条目将发生变化。
|
|
动态表索引-编码器流
与编码器指令不同的是,字段行表示中的相对索引相对于被编码的字段行开头的Base;看到4.5.1节。这确保了即使编码字段部分和动态表更新被乱序处理,引用也是稳定的
在字段行表示中,相对索引为0指的是绝对索引等于Base - 1的条目
|
|
动态表索引示例-表示中的相对索引
3.2.6. Post-Base Indexing
4、Wire Format
4.2. Encoder and Decoder Streams
QPACK定义了两个单项流
- 0x02 encoder stream carries an unframed sequence of encoder instructions
- 0x03 decoder stream carries an unframed sequence of decoder instructions
每个端点必须也仅能启动一个encoder stream和decoder stream
4.3. Encoder 指令
4.3.1. 设置Dynamic table capacity
001开头
|
|
4.3.2. Insert with Name Reference
|
|
插入字段行 Indexed name
4.3.3. Insert with Literal Name
|
|
插入字段行 New name
4.3.4. Duplicate
|
|
重复
4.4. Decoder 指令
4.4.1. Section Acknowledgment
在处理了一个Required Insert Count不为0的编码字段时回复ACK
|
|
4.4.2. Stream Cancellation
当流被重置或放弃读取时,发送取消指令
|
|
4.4.3. Insert Count Increment
该指令通过Increment参数的值增加已知接收计数(章节2.1.4)。解码器应该发送一个Increment值,将已知接收计数增加到到目前为止处理的动态表插入和重复的总数
|
|
4.5. Field Line Representations
4.5.1. Encoded Field Section Prefix
|
|
4.5.1.1. Required Insert Count
|
|
|
|
|
|
4.5.1.2. Base
|
|
4.5.2. Indexed Field Line
|
|
4.5.3. Indexed Field Line with Post-Base Index
|
|
4.5.4. Literal Field Line with Name Reference
|
|
4.5.5. Literal Field Line with Post-Base Name Reference
|
|
4.5.6. Literal Field Line with Literal Name
|
|
Literal Field Line with Literal Name