RFC9000 QUIC:一种基于UDP的安全多路复用传输协议
前言
本文是QUIC传输层协议的网络规范文档译文,尚未完成翻译,欢迎指正。
摘要
本文定义了QUIC传输协议的核心。QUIC是能支持应用程序进行有流量控制的多流结构化通信机制,支持低延迟连接建立和网络迁移。QUIC自带机密、完整的安全措施,能广泛支持各种各样的部署。有关文档描述了QUIC如何将TLS的密钥协商、丢包检测及众多拥塞控制算法整合。
备忘状态
本文是互联网标准追踪文档。
本文产自互联网工程任务组(IETF),已接受公开审查,并由互联网互联网工程指导委员会(IESG)批准出版。更多互联网标准相关信息详见RFC 7841第2章。
关于本文当前状态、勘误及反馈方式等相关信息请移步https://www.rfc-editor.org/info/rfc9000。
版权声明
版权所有(c)2021 IETF信托及确认为文档作者的个人。保留所有权利。
本文遵守BCP 78及在本文发布之日起生效的IETF信托涉及IETF文档的法律条文(https://trustee.ietf.org/license-info)。请仔细阅读相关条文,因为其描述了你对本文所有的权利及限制。从本文中摘录的代码组件必须包含信托法律条文第4.e章的简版BSD License文件,并且不附带任何该文件所描述的保证。
1. 综述
QUIC是一个安全通用传输协议。本文定义了第1版QUIC,支持《QUIC不变量》所定义的独立版本功能。
QUIC是一个面向连接的协议,在客户端及服务端之间建立有状态的交互。
QUIC握手由密钥协商及传输参数协商组成。QUIC集成了TLS握手《TLS13》,同时以自定义的帧保护数据包。更多关于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重定向——后,这个设计使连接仍然能够继续下去而不会断开。
有多种方式可以关闭连接。应用程序可以平滑关闭连接;双端可以协商一个超时时间段在超时后关闭连接;触发错误能够立即断开连接;一端失去状态后也能通过一种无状态机制关闭连接。
1.1. 文档结构
1.2. 术语及定义
本文中的关键字“必须(MUST)”、“必须不(MUST NOT)”、“需要(REQUIRED)”、“强烈要求(SHALL)”、“强烈要求不(SHALL NOT)”、“应该(SHOULD)”、“不应该(SHOULD NOT)”、“推荐(RECOMMENDED)”、“不推荐(NOT RECOMMENDED)”、“可以(MAY)”,以及“可选(OPTIONAL)”应理解为BCP 14 《RFC2119》《RFC8174》所描述的,当且仅当它们像本段一样以斜体加粗方式出现的时候。
本文常用术语列述如下:
QUIC:本文描述的传输协议。QUIC是名称,不是首字母缩写。
终端(Endpoint):一个能够以创建、接收及处理QUIC数据包参与QUIC连接的实体。QUIC终端有两种类型:客户端(client)及服务端(server)。
客户端:初始化QUIC连接的终端。
服务端:接收QUIC连接的终端。
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发送及接收数据的实体。
本文使用术语“QUIC数据包”、“UDP报文”及“IP数据包”表示相应协议的传输单元。换言之,一个或多个QUIC包被封装到一个UDP报文里,最终封装到一个IP数据包中。
1.3. 标准规范
本文数据包及帧的图解使用一种定制格式,目的是总结而不是定义协议元素。文中定义了完整的语义及详细的结构。
复杂的字段被命名后,由紧随命名的一个以一对花括号括起来的字段列表描述,列表中的字段以逗号分隔。
单个字段包括长度信息、带正号的定值、可选值或本字段的副本。单个字段使用下述标准规范,且所有长度都以比特为单位:
x (A)
: 表示x
是A
比特长度
x (i)
: 表示x
是一个使用第16章描述的变量长度编码的整型值
x (A..B)
: 表示x
的长度可以是从A
到B
的所有值,省略A
表示最小零位,并且省略B
表示没有设置上限。这种格式的值总是以字符边界结束。
x (L) = C
: 表示x
有一个定值C
,且x
的长度为L
,L
可以用上述任何长度格式
x (L) = C..D
: 表示x的值介于C
到D
之间,包括边界值C
和D
,L
表示长度,同上
[x (L)]
: 表示x
是一个可选的值,长度为L
x (L) ...
: 表示x
重复0次或以上次数,且每个实例长度为L
本文使用网络字节序(也就是大端)值。字段每个字节的各个比特从高位到低位排列。
约定单个字段通过使用复合字段的名称引用复合字段。
2. 流
2.1. 流类型及标识符
流可以是单向或双向的。单向流往一个方向传输数据:从流发起端向对端发送;双向流允许双端向对端发送数据。
在连接中,流以一个数字值标识,称为流ID。一个流ID是一个62比特的整数(0
-262-1
),且与同连接中其他流的流ID严格区分。流ID编码为一个可变长度整型,详见第16章。一个QUIC终端必须不能在同一个连接的不同流上重复使用同一个数值作为流ID。
流ID的最小有效位(0x01)标识流的发起者。客户端发起的流的ID是偶数(该位被置为0),服务端发起的流的ID是奇数(该位被置为1)。
流ID的次小有效位(0x02)标识流是双向流(该位被置为0)抑或单向流(该位被置为1)。
也就是说,流ID的最小两个有效位用来标识一条流是总共四种流类型中的哪一种,总结在如下表1中:
位 | 流类型 |
---|---|
0x00 | 客户端创建的双向流 |
0x01 | 服务端创建的双向流 |
0x02 | 客户端创建的单向流 |
0x03 | 服务端创建的单向流 |
每种流类型的流空间从其最小值开始(依次从0x00到0x03);每种流的每个流ID根据创建顺序依次线性递增。如果不按顺序地使用了一个流ID,将导致相同流类型的所有具有更小的流ID的流都被开启。
2.2. 数据发送与接收
流帧(第19.8章)封装应用层发送的数据。终端使用流帧的流ID及偏移字段整理数据。
终端必须将流数据以一个有序字节流传递给应用层。传递一个有序字节流需要终端缓存任何接收到的乱序数据,直到到达了建议的流量控制限制的上限。
QUIC对于传递流的无序数据并没有做额外的打算。然而,实现上可以选择支持传递无序数据给应用层接收。
终端可以从一条流的同一个偏移位置多次接收数据。如果数据已经被接收过了,就会直接被丢弃。处在任何偏移位置的数据如果被重复发送,其必须不能更改。终端可以将在流的同一偏移处收到不同数据的情况视为PROTOCOL_VIOLATION类型(违反协议)的连接错误。
流是一个抽象的有序字节流,QUIC不感知除此以外的任何结构。流帧的边界在数据被传输、丢包后重传以及被传递给应用层接收者后,不会继续保留。
终端必须不能在对端设置的流量控制限制之外发送任何数据。流量控制详见第4章。
2.3. 流优先级
如果资源以正确的优先级分配给多条流,那么流的多路复用可以对应用性能产生显著影响。
QUIC并不支持交换优先级信息,相反,其依赖从应用层接收的优先级信息。
QUIC在实现上应该提供某种方法使得应用层能够因此确定各条流的相对优先级。一种实现是使用应用层提供的信息决定分配多少资源来激活流。
2.4. 流操作
本文没有定义QUIC API,而是定义了一系列流操作相关的函数可以用于应用层协议的构建。应用层协议可以假定QUIC有关实现提供了本章描述的操作对应的接口。为一个特定应用层协议设计实现的QUIC协议可能仅仅提供该协议需要的这些操作。
在流的发送部分,应用层协议可以:
- 写数据,只有当流量控制给数据写出留足空间(第4.1章)才能成功写出;
- 结束流(清理并关闭),发送一个设置FIN位为1的流帧(第19.8章);
- 重置流(中止并关闭),当流未处在终止状态时发送一个RESET_STREAM帧(第19.4章)。
在流的接收部分,应用层协议可以:
- 读数据,以及
- 中止读取流数据并请求关闭流,该操作可能需要发送STOP_SENDING帧(第19.5章)。
应用层协议也可以请求在流状态改变的时候收到通知信息,包括当对端开启或重置流、对端中止流数据读取、有新数据可以读取、以及数据可以写出或因流控不能写出。
3. 流状态
本章描述流的发送及接收相关组件。有两个状态机需要描述:一个是关于终端传输数据(第3.1章)的流,另一个是关于终端接收数据(第3.2章)的流。
单向流用到发送或接收的其中一个状态机,取决于流类型及终端角色。双向流双端都会用到两个状态机。在极大程度上,不论单向流还是双向流,在使用这两个状态机上是没有区别的。相对而言打开一条双向流会稍微复杂一些,因为同时打开发送和接收端意味着在两个方向上同时打开流。
本章展示的状态机极具信息量。本文使用流状态描述不同类型帧在何时以何种方式发送的相关规则,以及当收到不同类型的帧时应作出的反应。即使这些状态机目的在于指导如何实现QUIC协议,但其并不意味着限制QUIC实现的方式。一个QUIC实现完全可以定义不同的状态机,只要其行为与本文所述状态机的具体实现一致即可。
注意:在某些情况下,单个事件或操作可能导致多个状态转换。例如,对于流的发送端,如果发送一个FIN置位的流帧可能导致两个状态转换:从“就绪”状态转到“发送”状态,然后从“发送”状态转到“数据发送完毕”状态。
3.1. 流发送状态
图2展示了通过流向对端发送数据的状态。
终端发起的流的发送部分(客户端发起的流类型是0和2,1和3则是由服务端发起的)由应用层打开。用于发送的流数据可能被缓存起来。
发送首个流帧(STREAM)或流阻塞帧(STREAM_DATA_BLOCKED)使流的发送部分进入“发送”状态。QUIC实现可以选择延迟分配流ID直到其发送首个流帧并进入“发送”状态,这样可以实现更好的流优先级。
由对端发起的双向流的发送部分(服务端发起的流类型是0,客户端发起的是1)在接收部分创建时就已处于“就绪”状态,并从此状态开始。
在“发送”状态,终端通过流帧传输——必要时重传——流数据。终端遵从其对端设置的流量控制限制,同时持续接收与处理最大流数据量帧(MAX_STREAM_DATA)。当发送过程被流的流量控制限制(第4.1章)所阻塞时,处在“发送”状态的终端会生成流阻塞帧。
在应用层表示所有应用数据及一个带FIN置位的流帧都发送完成后,流的发送部分进入“发送完成”状态。从这个状态开始,终端只会做必要的数据重传。在对端收到最终流下标前,都有可能收到最大流数据量帧。处于此状态的终端可以忽略任何其收到的最大流数据量帧,这样是安全的。
当所有流数据都被成功确认后,发送部分进入“接收完成”状态,这是一个最终状态。
处在“就绪”、“发送”及“发送完成”状态中的任何一个状态时,应用层都可以提出取消发送流数据。相应地,终端也可能从其对端收到一个停止发送帧(STOP_SENDING)。不论哪种情况,终端都会发送流重置帧(RESET_STREAM),随后流进入“重置发送”状态。
终端可以发送一个流重置帧作为流的首个帧,这会导致该流的发送部分开启然后立即转到“重置发送”状态。
一旦一个包含流重置帧的数据包被确认,流的发送部分即进入“重置接收”状态,这也是一个最终状态。
3.2. 接收流状态
图3展示了通过流从对端接收数据的状态。流的接收部分只会反映对端流发送部分的一些状态。流的接收部分不能追踪发送部分那些不可见的状态,例如“就绪”状态。相反,流的接收部分追踪向应用层传递的数据,其中一些数据对发送端不可见。
由对端发起的流(客户端发起的流类型是1和3,服务端发起的是0和2)的接收部分在收到该流的第一个流帧、流阻塞帧或流重置帧时创建。对于由对端创建的双向流,发送部分收到最大流数据量帧或停止发送帧时也会创建该流的接收部分。流的接收部分的初始状态是“接收”状态。
由本端发起的双向流(客户端发起的流类型是0,服务端发起的是1),当其发送部分进入“就绪”状态时,其接收部分进入“接收”状态。
终端在收到最大流数据量帧或停止发送帧后打开一条双向流。接收到一条未开启的流的最大流数据量帧意味着对端已经开启了该流,并开始支持流量控制额度。而接收到一条未开启流的停止发送帧意味着对端不会再从该流接收数据。无论这两种帧的哪一种都可能先于流帧或流阻塞帧到达本端,原因是包丢失或乱序。
在一条流创建前,所有数值小于该流ID的同类型流都必须被创建。这样能确保双端流的创建次序保持一致。
在“接收”状态,终端接收流帧和流阻塞帧。传入数据将被缓存,并可以按照正确顺序重组以便递给应用层。随着应用层不断消耗数据,缓冲区重新空出来,终端发送最大流数据量帧告知对端可以发送更多数据。
当收到一个带FIN置位的流帧时,数据的最终大小确定下来,详见第4.5章。流的接收部分随后转到“数据量确认”状态。在此状态,终端不再需要发送最大流数据量帧,只需要接收重传数据即可。
一旦收完了一条流的所有数据,流的接收部分转入“接收完成”状态。这可能发生在收到导致转入“数据量确认”状态的同一个流帧后。在所有数据都收完后,可以丢弃该流的任何流帧或流阻塞帧。
秋航注:意思是说,在收到流帧后,接收方可能转入“接收量确认”状态;紧接着,状态再次流转到“接收完成”状态。中间这句原译为:
在接收到导致状态转换到“接收量确认”的流帧后,可能同时导致转入“接收完成”状态。
“接收完成”状态会一直持续直到数据全部传递到应用层。一旦流数据传递完成,即转到“读取完成”状态,这是一个最终状态。
如果在“接收”或“数据量确认”状态收到一个流重置帧,将导致流转到“重置接收”状态。这可能打断流数据传递到应用层。
流重置帧也有可能在所有流数据都收完后才收到(即在“接收完成”阶段)。同样地,也可能在收完流重置帧后收到了剩下的流数据(即在“重置接收”状态)。在QUIC的具体实现上,可以自由选择如何处理这两种情况。
发送流重置帧意味着终端不能保证流数据的传输。然而,并没有要求在收到流重置帧后不传输流数据。QUIC实现可以打断流数据的传输,丢弃任何没有被消耗的数据,并通知对端收到了流重置帧。如果流数据完成了接收并被缓存起来以供应用层读取,此时收到流重置帧信号,则该信号可能被抑制或扣留。如果流重置帧被抑制,流的接收部分仍然维持在“接收完成”状态。
一旦应用层收到流被重置的信号,流的接收部分转到“重置读取”阶段,这是一个最终状态。
3.3. 允许的帧类型
流的发送端发送的帧只有三种能同时影响发送端和接收端状态:流帧(第19.8章)、流阻塞帧(第19.13章),以及流重置帧(第19.4章)。
发送端必须不在最终状态(“接收完成”或“重置接收”)发送任何这些帧。当流处在“重置发送”或任何最终状态时——也就是说,在发送完一个流重置帧后,发送端必须不发送流帧或流阻塞帧。接收端可以在任何状态接收这三种帧,原因在于被延迟的包可能携带这些帧。
流的接收端发送最大流数据量帧(第19.19章)及停止发送帧(第19.5章)。
接收端只可以在“接收”状态发送最大流数据量帧。接收端可以在任何尚未收到过流重置帧的状态——也就是除“重置接收”、“重置读取”之外的状态,发送一个停止发送帧。然而,在“接收完成”状态发送停止发送帧意义不大,因为所有流数据都已经收到了。由于可能存在数据包延迟,因此发送端可以在任何状态接收这两种帧。
3.4. 双向流状态
双向流同时包括接收部分和发送部分。QUIC的实现可以将双向流的状态表示成发送及接收流状态的组合状态。在最简单的模型里,当发送和接收部分均处在非最终状态时,表示流处于“打开”状态;当两者均处于最终状态时,表示流处于“关闭”状态。
表2展示了一个更复杂的双向流状态映射,粗略对应HTTP/2(HTTP2)中定义的流状态。这表明流的发送或接收部分多个状态映射到同一个组合状态。注意这仅仅是一个可能的映射,这种映射需要数据全部被确认后才能转入“关闭”或“半关闭”状态。
发送部分 | 接收部分 | 组合状态 |
---|---|---|
无流、就绪 | 无流、接收① | 空闲 |
就绪、发送、发送完成 | 接收、数据量确认 | 打开 |
就绪、发送、发送完成 | 接收完成、读取完成 | 半关闭(对端) |
就绪、发送、发送完成 | 重置接收、重置读取 | 半关闭(对端) |
接收完成 | 接收、数据量确认 | 半关闭(本端) |
重置发送、重置接收 | 接收、数据量确认 | 半关闭(本端) |
重置发送、重置接收 | 接收完成、读取完成 | 关闭 |
重置发送、重置接收 | 重置接收、重置读取 | 关闭 |
接收完成 | 接收完成、读取完成 | 关闭 |
接收完成 | 重置接收、重置读取 | 关闭 |
注意(①):如果一条流尚未被创建或其接收部分处于“接收”状态而尚未收到任何帧,则流处于“空闲”状态。
3.5. 请求状态转换
如果应用层不再需要流接收到的数据,应用层可以中止流的读取,并指定一个应用层错误码。
如果流处在“接收”或“数据量确认”状态,传输层应该发送一个停止发送帧通知对端及时在反方向关闭流。这通常表明接收方应用层不再读取流接收到的数据,但这并不意味着传入的数据一定会被忽略。
在发送完停止发送帧后收到的流帧仍然会被连接及流级别的流量控制统计,即使这些帧可能在接收时即被丢弃。
停止发送帧请求接收到它的那一端发送一个流重置帧。如果流处在“就绪”或“发送”状态,收到停止发送帧的一端必须发送一个流重置帧。如果流处在“发送完成”状态,终端可以推迟发送流重置帧直到含有未发完数据的数据包被确认或明确地被丢弃。如果任何未发完数据得到明确被丢弃了,终端应该发送一个流重置帧而非重传数据。
终端应该从停止发送帧复制错误码到要发出的流重置帧,但是其可以使用任何应用层错误码。发送停止发送帧的终端可以忽略任何随后被流接收到的流重置帧里的错误码。
停止发送帧应该只能在流尚未被对端重置前发送出去。停止发送帧主要在流的“接收”或“数据量确认”状态使用。
如果包含前一个停止发送帧的数据包丢失了,终端应当发送另一个停止发送帧。然而,一旦流收到了所有流数据或一个流重置帧——也就是说流处在“接收”或“数据量确认”之外的状态——发送停止发送帧就不再必要了。
如果双向流的一端想要将流的两个方向同时关闭,那么其可以通过发送一个流重置帧关闭一个方向,并发送一个停止发送帧促使相反方向也迅速得到关闭。
4. 流量控制
接收方需要限制缓存数据量以防发送方速度太快造成冲击或被恶意发送方消耗大量内存。为了让接收方能够限制连接的内存占用,不仅每条流有单独的流量控制,所有流也作为一个整体在连接层面有统一的流量控制。QUIC接收方控制发送方在一条流上以及任何时刻在所有流上可以发送的最大数据量,详见第4.1章或第4.2章。
同样地,为了限制连接并发,QUIC终端控制对方可以同时开启的最大流数量,详见第4.6章。
通过加密帧发送的数据不像流数据那样受流量控制制约。QUIC依赖于加密协议的实现来避免这些数据被过量缓存,详见《QUIC-TLS》。为了防止在多个层次过量缓存数据,QUIC实现应该为加密协议实现提供一套接口以供其交流缓存区限制。
4.1. 数据流量控制
QUIC使用一个基于限制的流量控制模型,接收者给出其准备在给定流或整个连接上准备接收的总字节数的上限。这使得QUIC中存在两层数据流量控制:
- 流的流量控制:通过限制每条流可以发送的数据量,防止单条流耗尽一条连接的全部接收缓冲区;
- 连接流量控制:通过限制所有流经由流帧可以发送的数据量,防止发送方超过连接接收方的缓冲区容量。
发送方发送数据必须不能超过上述任何一个限制。
接收方在握手过程中(第7.4章)通过传输参数为所有流设置初始的流接收缓存区上限。随后,接收方发送最大流数据量帧(第19.10章)或最大数据量帧(第19.9章)以告知对方提高流接收缓存区上限。
接收方可以通过发送一个含相关流ID的最大流数据量帧告知对方,它提高了流接收缓存区上限。最大流数据量帧表示流的最大绝对字节偏移量。接收方可以根据当前流消耗数据的偏移量确定后续发出的流量控制的偏移量。
接收方可以通过发送一个最大数据量帧告知对方,它提高了连接的接收缓存区上限,亦即所有流绝对字节偏移量之和的上限。接收方维护一个在所有流上累计接收数据的总字节数,用以检查是否超过了连接或流流量控制上限。接收方可以基于在所有流上消耗数据总字节数确定后续发出的最大数据偏移量。
接收方发布连接或流的流量控制上限若低于之前的上限,这不是错误,只是这个更低的上限不会生效而已。
如果发送方违反了推荐的连接或流的流量控制上限,接收方必须以FLOW_CONTROL_ERROR类型错误关闭连接,更多错误处理相关细节详见第11章。
发送方必须忽略任何不会提高流量控制上限的最大流数据量帧或最大数据量帧。
如果发送方发送数据达到了流量控制上限,其将不能再发送新数据,且应认为其被阻塞住了。发送方应该发送一个流数据阻塞帧或数据阻塞帧来告知接收方其有数据要写出但是被流量控制所阻塞。如果发送方被阻塞的时间超过空等超时时间(第10.1章),接收方可以关闭连接,即便发送方有可传输的数据。为了保持连接不被关闭,在没有可引发ACK的数据包处于传输中时,被流量控制限制所阻塞的发送方应该定期发送一个流数据阻塞帧或数据阻塞帧。
4.2. 提高流量控制限制
QUIC实现决定什么时候以什么额度通过最大流数据量帧及最大数据量帧提高流量控制限制,但是本章提出了一些注意事项。
为了避免阻塞发送方,接收方可以在一个往返时间(RTT)内多次发送一个最大流数据量帧或最大数据量帧,或者尽可能早地发送,从而为帧的丢失及随后的恢复留出时间。
控制帧也会引入连接开销。也就是说频繁发送最大流数据量帧及最大数据量帧做极其微小的调整是不可取的。另一方面,如果更新不够频繁,每次更新时就要对接收方上限做更大幅度的提升以防发送方被阻塞,使得接收方耗费需要更多资源。因此,确定决定接收方推荐上限需要权衡资源耗费与连接开销。
接收方可以使用一个常见于TCP实现的基于往返时间及接收数据应用层消耗速率的自动调谐机制来调整推荐接收上限的频率和增量。作为优化,终端只有在有其他帧要发送时才可以发送流量控制相关帧,以确保流量控制不会导致额外的数据包发送。
被阻塞发送方不一定发送流阻塞帧或数据阻塞帧。因此,接收方必须不能在发送最大流数据量帧及最大数据量帧前等待接收流阻塞帧或数据阻塞帧,否则可能导致发送方在连接的其余部分被阻塞。即使发送方发送了这些帧,等待它们也会导致发送方至少被阻塞一个完整的往返周期。
当终端在阻塞期间收到可发送数据额度时,它可能会回复大量数据,造成短暂的拥塞,详见《QUIC恢复》第7.7章有关终端如何避免这类拥塞的讨论。
秋航注:这里将credit翻译为额度、增量等,指的是最大流数据量帧及最大数据量帧所带来的接收方新流量控制上限相对之前的提升量,这个提升量是发送方后续可发送新数据的空间。
4.3. 流量控制性能
如果终端不能确保其对端始终在该连接上有大于对端带宽时延积的流量控制额度,其接收吞吐量将被流量控制限制。
包丢失会导致接收缓冲区出现空隙,从而阻碍应用层消耗数据并释放接收缓冲空间。
及时发送流量控制上限更新能提高性能。发送只包含流量控制更新的数据包会增加网络负载,对性能产生不利影响。将流量控制更新与其他帧一起发出,例如如ACK帧,可以降低此类更新带来的消耗。
4.4. 处理流取消
终端之间最终必须在每条流消耗的流量控制额度上达成一致,从而能够计算出连接级流量控制的字节数。
收到流重置帧后,终端就会关闭相应流的状态,并忽略后续从该流上收到的数据。
流重置帧会立即中止流的一个方向。对于一条双向流,流重置帧不会影响另一个方向的数据流。双端必须给一条流尚未关闭的方向维持流量控制状态直到该方向转入终止状态。
秋航注:“中止”与“终止”的区别在于“中止”强调突然地、异常地结束一个过程,而“终止”没有这种强调意味。可以认为,“终止”包含流程的正常结束和异常“中止”两种情况。
4.5. 流的最终数据量
流的最终数据量是流消耗的流量控制额度的总量。假设流的每个连续的字节都被发送了一次,那么其最终数据量就是发出的总字节数。更一般地来说,这比流上发送的偏移最高的字节的偏移值高1
,若无字节发送则为0
。
不管流是如何终止的,发送方始终试图将流的最终数据量可靠地发送给接收方。最终数据量是 带有FIN置位的流帧的Offset(下标)和Length(长度)字段值的总和,注意这些字段可能是隐式的。或者,流重置帧的Final Size字段也可以携带最终数据量值。这保证了双端发送方在该流上消耗的流量控制额度上达成一致。
终端在流的接收部分转入“数据量确认”或“重置接收”状态(第3章)后将得知最终数据量。接收方必须根据流的最终数据量在其连接层流量控制上统计该流发送的字节数。
终端必须不能在大于或等于最终数据量的流上发送数据。
一旦流的最终数据量得到确认,就不能再更改。如果收到流重置帧或流帧表示要修改流的最终数据量,终端应该回复一个FINAL_SIZE_ERROR类型的错误,更多细节详见第11章有关错误处理部分。接收方应该将收到达到或超过最终数据量数据的情况视为FINAL_SIZE_ERROR类型的错误,即使是在关闭后收到的。并不强制要求生成这些错误,因为终端若要做到这一点往往意味着终端需要给已关闭流维持一个最终数据量状态,也就意味着需要增加一个重要的状态确认。
4.6. 并发控制
终端限制对端累计可以开启的流的数量。只有流ID小于(max_streams * 4 + first_stream_id_of_type)
的流可以被开启,详见表1。初始限制由传输参数设置,详见第18.2章。随后的限制由最大流帧推出,详见第19.11章。对于单向流和双向流的限制值是相互独立的。
如果经由max_streams
传输参数或最大流帧收到的值大于260,这将使得最大流ID不能表示为变长整数,详见第16章。如果收到了这两者的其中一个,必须立即关闭连接:如果这个值是通过传输参数收到的,就以TRANSPORT_PARAMETER_ERROR错误关闭连接;如果是通过帧收到的,就以FRAME_ENCODING_ERROR错误关闭连接,详见第10.2章。
终端必须不能触达对端设置的流数量限制。终端收到一个有个触达其设置的流数量上限的流ID的帧的情况必须视为STREAM_LIMIT_ERROR类型的连接错误,更多有关错误处理的细节详见第11章。
一旦接收方通过最大流帧推荐了一个流数量上限,再推荐一个更小的上限将不再生效。必须忽略不会提高流数量上限的最大流帧。
正如流及连接层流量控制,本文让实现者来决定通过最大流帧发布流数量上限的时机及数值。QUIC实现可以选择在流关闭时提高流数量上限,从而保持对端可以使用的流的数量大体对等。
终端因对端设置的流数量上限而不能再开启新流时,应该发送一个流阻塞帧(第19.14章)。这个信号对于调试很有帮助。终端必须不能在告知额外额度前等待这个信号,因为这样做的话意味着对端会被阻塞至少一个往返周期,且若对端选择不发流阻塞帧时则等待可能会是无限期的。
5. 连接
QUIC连接在客户端与服务端之间共享状态。
QUIC连接从握手开始。在握手的过程中,双端使用加密握手协议(QUIC-TLS)创建一个共享密钥并协商应用协议。握手过程(第7章)确认双端的交流意愿(第8.1章),并确立连接的各项参数(第7.4章)。
应用层协议可以在连接握手阶段使用连接,但这种使用是有所限制的。0-RTT准许客户端在收到服务端回复前发送应用数据,但是没有抵御回放攻击的措施,详见《QUIC-TLS》第9.2章。服务端也可以在收到客户端的最终加密握手信息前发送应用数据,此时服务端尚未确认客户端的身份,也未确认客户端是否存活。应用层协议可以在安全性与降低延迟之间权衡,以决定是否使用这些功能。
连接ID(第5章)的存在使得连接可以迁移到新的网络通道上,这不仅可以是终端的主动选择,也可以是在网络中间件发生变更后的被动选择。第9章描述了如何缓解连接迁移带来的安全和隐私问题。
对于不再需要的连接,有几种方法可以让客户端和服务端将其关闭,详见第10章。
5.1 连接ID
每条连接拥有一组连接标识符,也就是连接ID,每个连接ID都能标识这条连接。连接ID是由终端独立选择的,每个终端选择连接ID供对端使用。
连接ID的主要功能是确保底层协议(UDP、IP及更底层的协议栈)发生地址变更时不会导致一个QUIC连接的数据包被传输到错误的QUIC终端上。终端使用特殊实现的(且可能特殊部署的)方式选择连接ID,这将使得具有该连接ID的数据包能够路由回终端,并在接收时能够正确识别。
终端使用多个连接ID从而使观察者无法在没有终端协助的情况下将发送的数据包识别为同一个连接的数据包,详见第9.5章。
连接ID必须不能包含任何能够被外部观察者(这里指的是不会与发信者合作的人)用于将其关联到该连接的其他连接ID的信息。举个简单的例子,这意味着同一个连接ID不能在同一个连接上被多次发送。
长包头数据包包含源连接ID和目标连接ID字段,这些字段用来设置新连接的连接ID,更多细节详见第7.2章。
短包头数据包(第17.3章)只包含目标连接ID并省略显式长度。目标连接ID字段的长度被认为已经为对端所知。使用基于连接ID进行路由调度的负载均衡器的终端可以在连接ID的固定长度或使用编码上与负载均衡器达成一致。在一个固定的位置可以编码一个显式的长度,从而可以根据长度安置整个连接ID,使其仍可被负载均衡器使用。
一个版本协商(第17.2.1章)包回显客户端选择的连接ID,不仅为了确保到客户端的路由是正确的,也为了证明这个数据包是对客户端发送的数据包的回应。
当一个连接ID不需要路由到正确的终端上时,可能使用一个零长度的连接ID。然而,多个连接复用同一个本地IP地址和端口时,如果使用零长度的连接ID,将在存在对端连接迁移、NAT重绑定及客户端端口复用的情况下导致失败,除非可以肯定没有使用这些协议特性。
当一个终端使用一个非零长度连接ID,它必须确保对端有一定量的连接ID可供选择给发往本端的数据包使用。这些连接ID通过NEW_CONNECTION_ID帧提供(第19.15章)。
5.1.1 发布连接ID
当新连接ID帧
或撤销连接ID帧
指向同一个值时,每个连接ID都有一个相关联的序列号用于协助检测。一个终端发出的初始连接ID是通过握手阶段(第17.2章)的长包头的源连接ID字段发送的。初始连接ID的序列号是0。如果发送了传输参数preferred_address
(推荐地址),发出的连接ID的序列号就是1。
额外的连接ID通过新连接ID帧
(第19.15章)传输给对方。新发出的连接ID的序列号必须递增1。客户端不会为它发出的第一个目标连接ID字段的连接ID及重试包的连接ID分配序列号。
只要一个终端发出了某个连接ID,那么在连接存活,且对端尚未通过撤销连接ID帧
(第19.16章)取消的期间,它必须接受携带该连接ID的数据包。连接ID被发出且没有被取消,即是活跃的;任何活跃的连接ID在连接任何时间、对其任何类型的数据包都可以有效使用。这包括被服务端通过推荐地址传输参数发出的连接ID。
终端应该确保其对端有足够数量可用且未使用的连接ID。终端使用active_connection_id_limit
(活跃连接ID限制)传输参数指定它们想要维持的活跃连接ID的数目。一个终端必须不能提供超过对端限制数目的连接ID。如果新连接ID帧
也要求终止任何多余的连接ID,终端可以通过在“停用至”字段包含一个足够大的值来发送临时超过对端限制数量的连接ID。
终端可以根据新连接ID帧
的“停用至”字段内容添加一些活跃连接ID并终止其他连接ID。在处理完一个新连接ID帧
,添加及撤销一些活跃连接ID后,如果活跃连接ID数量仍然超过active_connection_id_limit
传输参数建议的值,终端必须以CONNECTION_ID_LIMIT_ERROR
(连接ID限制)错误关闭连接。
当对端撤销一个连接ID后,终端应该提供一个新的连接ID。如果终端提供的连接ID数目小于对端active_connection_id_limit
值,则其可以在收到一个包含之前撤销的连接ID的数据包时提供一个新的连接ID。终端可以限制为每条连接发布的连接ID总数来避免连接ID耗尽的风险,见第10.3.2章。终端也可以限制连接ID的发布以降低其维护的通道层面的状态量,例如通道验证状态,因为对端可能有多少连接ID就用多少通道与其交互。
启动连接迁移且需要非零长度连接ID的终端应该确保其连接ID池对端可得,从而使对端能在连接迁移时用新的连接ID,因为如果连接ID池耗尽,对端将无法回复。
在握手阶段选择零长度连接ID的终端不能发布新的连接ID。通过任何通道发往一个这样的终端的所有包使用零长度目标连接ID字段。
5.1.2 消耗及撤销连接ID
终端可以在连接期间任何时候将其与对端交互的连接ID更改为另一个可用的连接ID。在对端进行连接迁移时,终端发布的连接ID会被消耗,更多详见第9.5章。
终端维护一个接收自对端的连接ID集合,其中每个ID均可用于发送数据包。当终端想要停止使用其中的某个连接ID,其可以发送一个撤销连接ID帧
给对端。发送一个撤销连接ID帧
意味着将不会再次使用该连接ID,同时请求对端通过新连接ID帧
换一个新的连接ID。
如第9.5章所述,终端对连接ID的使用作出了限制,同一个连接ID只能用于从同一本地地址向同一目标地址发送数据包。当使用连接ID的本地或目标地址不再活跃的时候,终端应该撤销这些连接ID。
在特定场合,终端可能需要停止接收先前发布的连接ID。这种终端会通过发送带有值已增加的“停用至”字段的新连接ID帧
使对端停用这些连接ID。终端应该继续接收先前发布的连接ID直到它们被对端撤销。如果终端不能再处理指定的连接ID,其可以关闭连接。
一旦收到一个值增加的“停用至”字段,对端必须停止使用相关连接ID,并在添加新提供的连接ID到活跃连接ID集合之前,通过撤销连接ID帧
停用这些连接ID。这么安排使得终端能够替换全部活跃连接ID,而不会出现对端没有可用连接ID的情况,也不会使活跃连接ID数超过对端传输参数active_connection_id_limit
设下的限制,详见第18.2章。无法停止使用连接ID可能导致连接错误,因为发布端可能不能在当前连接上继续使用这些连接ID。
如果撤销连接ID帧
尚未被确认,那么终端应该限制本地已撤销连接ID的数量。终端应该允许发送中及可追踪的撤销连接ID帧
数量至少是传输参数active_connection_id_limit
值的两倍。终端必须不在没有撤销一个连接ID前忘记它,不过它可以将存在某连接ID亟待撤销,但是待撤销连接ID的总数超过上述限制的情况视为一个CONNECTION_ID_LIMIT_ERROR
类型的连接错误。
在收到撤销由先前的“停用至”字段指定的全部连接ID的撤销连接ID帧
前,终端不应该发布更新“停用至”字段。
5.2 数据包与连接如何匹配
入包在接收时进行分类。数据包可能被关联到一个已存在的连接,也可能(在服务端)创建一个新连接。
终端尝试将数据包与现有连接关联起来。如果数据包有关联到一个已存在的连接的非零目标连接ID,QUIC将在相应连接上处理该数据包。注意一条连接可以对应不止一个连接ID,详见第5.1章。
如果一个数据包的目标连接ID是零长度的,而且其地址信息与终端用来标识零长度连接ID的连接的地址信息一致,则QUIC将该数据包作为该连接的一部分处理。终端可以使用目标IP和端口或同时使用源地址和目标地址作为标识,不过这样会使连接像第5.1章描述的那样变得脆弱。
对任何不属于已存在连接的数据包,终端都可以发送一个无状态重置(第10.3章)。无状态重置使对端能更快地识别连接变得不可用的情况。
初始、重试或版本协商这种缺乏强完整性保护的无效包可以被丢弃。如果终端在发生错误前处理了这些包的内容,终端必须产生一个连接错误,或完全恢复处理期间所做的所有变更。
5.2.1 客户端数据包处理
发往客户端的有效数据包往往包含一个与客户端选择的值匹配的目标连接ID。选择接收零长度连接ID的客户端可以使用本地地址和端口识别一个连接。不能与已存在连接匹配的数据包——基于目标连接ID或目标连接ID为零长度时基于本地IP地址和端口——将被丢弃。
由于数据包乱序或丢失,客户端可能收到使用尚未算出的密钥加密的数据包。客户端可以丢弃这些包,也可以缓存起来以备后续数据包使其可以计算出密钥。
如果客户端收到一个包使用了与初始化阶段选择不同的QUIC版本,其必须丢弃该包。
5.2.2 服务端数据包处理
如果服务端收到一个来自不支持版本的数据包而其尺寸又足够初始化某个支持版本的新连接,那么服务端应该发送一个版本协商包,详见第6.1章。服务端可以限制其回复的版本协商包的数量。服务端必须丢弃属于不支持版本且尺寸不到上述要求的数据包。
一个不支持版本的第一个包可以对任何与具体版本有关的字段使用不同语义和编码。尤其是不同的版本可能使用不同的数据包保护密钥。不支持一个特定版本的服务端不太可能解密数据包的有效负载或正确解释其内容。当数据报文足够长时,服务端应该回复一个版本协商包。
有一个支持版本或没有版本字段的数据包通过连接ID或——对于零长度连接ID的数据包——本地地址和端口与一个连接匹配,这些包在该连接上处理;否则,服务端继续做如下处理。
如果数据包是完全符合规范的初始化包,则服务端继续进行握手(第7章)。服务端将按照客户端选择的版本来进行握手。
如果服务端拒绝接收一个新连接,它应该发送一个带连接关闭帧
的初始化包给客户端,其中连接关闭帧
的错误码设置为CONNECTION_REFUSED
(连接拒绝)。
如果数据包是一个0-RTT包,服务端可以进行有限数量的缓存,因为后续预期会收到初始化包。由于客户端不可能先于收到服务端回复发送握手包,所以服务端应该忽略任何这样的包。
服务端必须丢弃任何其他场合收到的数据包。
5.2.3 关于简单负载均衡
服务端部署可以仅仅使用源与目的IP地址和端口在服务器之间进行负载均衡。改变客户端IP地址或端口可能导致数据包被转发到错误的服务器上。这种服务端部署可以从如下方法中选择一个用于在客户端地址变换时维持连接。
- 服务端可以使用带外机制,根据连接ID将数据包转发到正确的服务器;
- 如果服务器可以使用固定的IP地址或端口,且不同于客户端初始建联时访问的那个,它们可以使用传输参数
preferred_address
(推荐地址)请求客户端迁移到那个固定的地址上。 注意客户端可以选择不使用推荐地址。
服务端的一台服务器如果不支持在客户端改变地址时维持连接,其应该通过传输参数disable_active_migration
(关闭活动迁移)告知对端当前不支持连接迁移。在客户端预先拿到preferred_address
参数后,传输参数disable_active_migration
将不能阻止客户端进行连接迁移。
应用本简单形式负载均衡进行部署后,服务端必须避免创建无状态重置指示,详见第21.11章。
5.3 连接操作
本文没有定义QUIC的API,而是定义了一系列有关QUIC连接的函数,用于应用层协议进行依赖。应用层协议可以假设一个QUIC的实现提供了一个包括本章所述操作的接口。针对一个特定应用层协议而设计的实现可能只提供该协议用到的那些操作。
当实现客户端时,应用层协议可以:
- 创建一个连接,开始进行第7章描述的交互过程;
- 如果支持,启用早期数据功能;
- 当早期数据被服务端接受或拒绝时,收到通知。
当实现服务端时,应用层协议可以:
- 监听传入的连接,准备进行第7章描述的交互过程;
- 如果支持早期数据,在发送给客户端的TLS恢复ticket中嵌入应用层控制数据;
- 如果支持早期数据,从接收自客户端的恢复ticket中恢复应用层控制数据,并根据该信息接受或拒绝早期数据。
无论哪种情况,应用层协议都可以:
6. 版本协商
6.1 发送版本协商包
如果客户端选择的版本服务端不接受,服务端会响应一个版本协商包,详见第17.2.1章。版本协商包包含一个服务端支持的版本的列表。终端必须不能给一个版本协商包回应一个版本协商包。
这个系统允许服务端处理不支持的数据包而不保持状态。即使作为响应发出的初始包或版本协商包都可能丢失,客户端也会发新的包直到收到回复或放弃建连尝试。
服务端可以限制其发送版本协商包的数量。例如,能识别0-RTT包的服务端可能会选择不发送版本协商包来响应0-RTT包,而是期望最终能收到初始包。
6.2 处理版本协商包
设计版本协商包的目的是为了让QUIC能够给未来定义的功能协商QUIC版本用于连接。未来标准追踪规范可能改变支持多版本QUIC对于收到的用于回复给试图使用这个版本建立连接的版本协商包进行应对的实现方式。
仅仅支持这个版本的客户端如果收到一个版本协商包,其必须放弃当前建联尝试,除非是下述两种例外:如果已经收到且成功处理任何其他类型的包,客户端必须忽略任何版本协商包;客户端必须忽略包含其选择的QUIC版本的版本协商包。
如何执行版本协商留作由未来标准追踪规范定义的未来工作。特别是,未来工作将确保在抵御版本降级攻击时具有健壮性,详见第21.12章。
6.3 使用保留版本
7. 加密与传输握手
QUIC通过将加密与传输握手组合最小化连接建立的延迟。QUIC使用加密帧(详见第19.6章)传递加密握手信息。本文所定义的QUIC版本标识为0x00000001
,使用《QUIC-TLS》描述的TLS,而不同的QUIC版本可能表明其使用了不同的加密握手协议。
QUIC可靠、有序地传递加密握手数据。QUIC数据包保护用于尽可能多地对握手协议进行加密。加密握手必须具备如下属性:
- 密钥交换是认证的,需要
- 服务端总是经过认证的,且
- 客户端是否经过认证是可选的,且
- 每个连接生成不同且不相关的密钥,且
- 密钥材料对0-RTT和1-RTT数据包均可提供保护。
- 双端传输参数值的交换是认证的,且对服务端传输参数提供机密性保护。
- 应用层协议的认证协商(为此,TLS使用的是应用层协议协商——ALPN)。
加密帧可以在不同数据包号空间发送(详见第12.3章)。在每个数据包号空间,加密帧用来确保加密握手数据有序传递的偏移量都是从0开始的。
图4展示了一个简化的握手及用于推动握手进程的数据包与帧的交换。如果可以的话,尽量在握手期间交换应用层数据,并用星号("*")表示。一旦完成握手,终端就能自由交换应用层数据。
终端可以使用握手期间发送的数据包来测试是否支持显式拥塞通知(ECN),详见第13.4章。终端通过观察用于确认其发送的首个数据包的ACK帧是否携带ECN计数,验证是否支持ECN,更多细节详见第13.4.2章。
终端必须明确地协商应用层协议。这避免了对使用中的协议还存在分歧的情况。
7.1. 握手流程示例
关于TLS如何与QUIC整合的更多细节详见《QUIC-TLS》,但在此举出一些范例。用以支持客户端地址验证的这种交换的一个扩展详见第8.1.2章。
一旦完成任何地址验证交换,加密握手将用于协商密钥。加密握手由初始包(第17.2.2章)和握手包(第17.2.4章)携带。
图5展示了1-RTT握手过程的概况。每行展示一个QUIC数据包,首先展示其包类型和包号,紧接着展示这类包通常包含的帧。例如首包通常是初始包,包号是0,且包含一个携带ClientHello内容的加密帧。
多个QUIC包——甚至包类型各不相同——可以合并成一个UDP报文,详见第12.2章。因此,这次握手最少可以仅由4个UDP报文组成(受限于协议固有的诸如拥塞控制和反放大机制)。例如,服务端的首个报文包含初始包、握手包及包含“0.5-RTT数据”的1-RTT包。
图6是一个0-RTT握手连接的示例,并且其中一个数据包携带0-RTT数据。注意,如第12.3章所述,服务端在1-RTT包中对0-RTT数据进行确认,并且客户端发送的1-RTT包处于同一个包号空间。
7.2. 协商连接ID
连接ID用于确保数据包稳定路由,详见第5.1章。长包头包含两个连接ID:目标连接ID由接收方指定并用于提供稳定路由;源连接ID供对端设置为目标连接ID。
在握手期间,带有长包头(详见第17.2章)的数据包用于设置双端所使用的连接ID。每端的源连接ID会作为发往该端的数据包的目标连接ID。在处理完首个初始数据包后,每端将后续发送数据包的目标连接ID字段值设置为其接收的源连接ID字段值。
当客户端发送初始包之前未从服务端收到过初始数据包或重试数据包,则生成不可预测值填充发送的初始包的目标连接ID字段。目标连接ID的长度必须至少8字节。客户端在一条连接上必须使用同一个目标连接ID,直到收到服务端发来的数据包为止。
客户端发送的首个初始数据包的目标连接ID字段用于确定初始数据包的包保护密钥。这些密钥在收到重试数据包后变更,详见QUIC-TLS的第5.2章。
客户端选择一个值填充源连接ID字段,并设置源连接ID长度字段以标识其长度。
客户端发送的首个0-RTT数据包使用与其发送的首个初始数据包一致的源连接ID和目标连接ID。
在首次收到从服务端发来的初始数据包或重试数据包后,客户端将服务端提供的源连接ID作为后续发送数据包的目标连接ID,包括任何0-RTT包。这意味着客户端可能需要在连接建立阶段将目标连接ID字段的值变更两次:一次是响应服务端发来的重试数据包,一次是响应服务端发来的初始数据包。一旦客户端收到一个从服务端发来的有效的初始数据包,则其必须丢弃在该连接上后续接收到的任何带有不同源连接ID值的数据包。
客户端必须在收到首个初始数据包或首个重试数据包后,更改其后续发送数据包时的目标连接ID值。服务端必须基于收到的首个初始数据包设置发送数据包的目标连接ID。任何针对目标连接ID的后续改动,只允许通过新连接ID帧携带的值进行;如果后续的初始数据包包含不同的源连接ID,则必须将该包丢弃。这避免了对多个具有不同源连接ID的初始数据包进行无状态处理可能导致的不可预测的结果。
7.3. 验证连接ID
终端在握手期间所选择的连接ID会被包含在传输参数中受到认证,详见第7.4章。于是所有用于握手的连接ID都会经由加密握手得到认证。
终端将其发出的首个初始数据包的源连接ID字段值包含在initial_source_connection_id
传输参数中,详见第18.2章。服务端将其接收到的首个初始数据包中的目标连接ID值包含在original_destination_connection_id
传输参数中;如果服务端发送了一个重试数据包,则其对应的是发送该包之前收到的首个初始数据包。如果发送重试数据包,服务端也会将重试数据包的源连接ID字段包含在retry_source_connection_id
传输参数中。
来自对端的传输参数中的值必须与终端在其发送的(对于服务端,则同时包括其接收到的)初始数据包的目标连接ID和源连接ID字段所使用的值保持一致。终端必须验证其接收到的传输参数匹配接收到的连接ID值。将连接ID值包含在传输参数中并认证,从而确保攻击者不能在握手期间通过插入携带有攻击者选择的连接ID的数据包影响连接ID的选择。
终端必须将任意终端缺失initial_source_connection_id
传输参数以及服务端缺失original_destination_connection_id
传输参数视为TRANSPORT_PARAMETER_ERROR
类型的连接错误。
终端必须将下述情况视为TRANSPORT_PARAMETER_ERROR
或PROTOCOL_VIOLATION
类型的连接错误:
- 从服务端接收到了重试数据包,但缺失
retry_source_connection_id
传输参数, - 未从服务端接收到重试数据包,但存在
retry_source_connection_id
传输参数,或 - 从对端接收到的传输参数中的值与发送的初始数据包中相应目标连接ID或源连接ID字段不一致。
如果选择了一个零长度连接ID,则相应的传输参数包含一个零长度值。
图7展示了一个完整握手过程中连接ID的使用(其中DCID表示目标连接ID,SCID表示源连接ID)。图中展示了初始数据包如何交换,以及随后1-RTT数据包的交换,其中包括握手期间创建的连接ID。
图8展示了包含重试数据包的握手过程。
在上述两种情况(图7和图8)下,客户端设置其initial_source_connection_id
传输参数值为C1
。
当握手过程没有出现重试数据包(图7),服务端设置original_destination_connection_id
为S1
(注意该值是客户端选择的),设置initial_source_connection_id
为S3
。在这种情况下,服务端不包含retry_source_connection_id
传输参数。
当握手过程出现了重试数据包(图8),服务端设置original_destination_connection_id
为S1
,设置retry_source_connection_id
为S2
,设置initial_source_connection_id
为S3
。
7.4. 加密与传输握手
连接建立期间,双端对各自的传输参数作出验证声明。终端必须遵守每个参数定义的约束规范,每个参数的描述包括其处理规则。
传输参数由每个终端单方面声明。每个终端可以独立选择传输参数的值而不依赖于对端的选择。
传输参数的编码详见第18章.
QUIC在加密握手阶段包含编码的传输参数。一旦握手完成,由对端声明的传输参数就生效了。每个终端都需要验证对端提供的参数值。
每个传输参数的具体定义详见第18.2章。
终端必须将收到无效的传输参数值的情况视为一个TRANSPORT_PARAMETER_ERROR
类型连接错误。
终端必须不能在一个特定的传输参数扩展中传输同一个参数超过1次。终端应该将收到重复的传输参数的情况视为一个TRANSPORT_PARAMETER_ERROR
类型连接错误。
终端使用传输参数认证握手期间的连接ID协商,详见第7.3章。
ALPN(详见ALPN)允许客户端在连接建立期间提供多个应用协议。客户端包含的传输参数对客户端提供的所有应用协议生效。应用协议可以指定传输参数值,例如初始流控上限。然而,应用协议对传输参数值设置限制,可能导致因为限制冲突使得客户端不能提供多种应用协议支持。
7.4.1. 0-RTT传输参数
是否使用0-RTT取决于客户端和服务端使用的传输参数是否由同一个旧连接协商的。为了开启0-RTT,终端需要保存服务端传输参数的值及在连接上收到的任何会话票据(session ticket)。终端也需要保存其他任何应用协议或加密握手所需要的信息,详见《QUIC-TLS》的第4.6章。保存的传输参数的值在使用会话票据尝试进行0-RTT建连时使用。
保存的传输参数适用于新连接,直到握手完成,客户端开始发送1-RTT数据包为止。一旦握手完成,客户端就启用握手过程中创建的传输参数。不是所有的传输参数都需要保存,因为部分参数不会在后续连接中生效,或是在0-RTT建连期间不起作用。
定义新的传输参数(详见第7.4.2章)时,必须明确说明其作为0-RTT参数进行保存是否是强制性的、可选的,或必须禁止的。客户端不需要保存一个其不能处理的传输参数。
客户端必须不使用下述参数的保存值:ack_delay_exponent
、max_ack_delay
、initial_source_connection_id
、original_destination_connection_id
、preferred_address
、retry_source_connection_id
,以及stateless_reset_token
。客户端必须使用握手期间服务端给出的新值。如果服务端没有提供新的值,就使用默认值。
试图发送0-RTT数据的客户端必须保存所有其他服务端需要使用且可以处理的传输参数。服务端可以保存这些传输参数,或在会话票据中保存这些参数值的一份具备完整性保护的备份,并在收到0-RTT数据后恢复其具体信息。
如果服务端收到了0-RTT数据,服务端必须不能放宽任何限制,或采用任何其他可能与客户端发来的0-RTT数据产生冲突的值。尤其需要注意的是,服务端在接收0-RTT数据后,必须不可为下述参数(详见第18.2章)设置一个低于保存值的值:
active_connection_id_limit
initial_max_data
initial_max_stream_data_bidi_local
initial_max_stream_data_bidi_remote
initial_max_stream_data_uni
initial_max_streams_bidi
initial_max_streams_uni
删除特定传输参数,或为之设置一个零值,可能导致启用0-RTT数据但却不可用。允许发送应用数据的传输参数的适当子集应该被设置为非零值,以支持0-RTT。这包括:initial_max_data
和(1)initial_max_streams_bidi
及initial_max_stream_data_bidi_remote
,或(2)initial_max_streams_uni
及initial_max_stream_data_uni
。
服务端可能给流设置的初始流控上限大于保存的0-RTT数据包中由客户端设置的值。一旦握手完成,客户端必须根据更新后的initial_max_stream_data_bidi_remote
及initial_max_stream_data_uni
的值更新所有发送流的流控上限。
服务端可以保存并恢复先前发送的max_idle_timeout
、max_udp_payload_size
及disable_active_migration
参数的值,以及在选择更小值后拒绝0-RTT。当接收0-RTT数据可能同时降低连接性能时,调低这些参数的值。特别是调低max_udp_payload_size
可能导致丢包,导致性能与直接拒绝0-RTT数据相比变得更差。
当服务端不支持恢复后的值时,其必须拒绝0-RTT数据。
当在0-RTT数据包中发送帧时,客户端必须只使用保存的传输参数;更重要的是,它必须不能使用服务端更新后的传输参数,或使用从1-RTT数据包接收到的帧。在握手过程中传输参数更新的值只适用于1-RTT数据包。例如,保存的传输参数中的流控上限值适用于所有的0-RTT数据包,即使这些值通过握手或通过1-RTT数据包发送的帧调大了。服务端可以将在0-RTT数据中使用更新后的传输参数视为一个PROTOCOL_VIOLATION
类型的连接错误。
7.4.2. 新传输参数
7.5. 加密信息缓存
QUIC实现需要维护一个用于保存乱序接收到的加密数据的缓存区。由于加密帧没有流量控制,终端有可能强制对端缓存无限量的数据。
QUIC实现必须支持缓存至少4096字节从乱序加密帧收到的数据。终端可以选择允许在握手期间缓存更多的数据。更大的缓存上限意味着可以交换更大的密钥或证书。终端的缓存区大小不必在整个连接生命周期内保持不变。
握手期间无法缓存加密帧可能导致连接失败。如果终端的缓存区在握手期间溢出,则其可以通过暂时扩大缓存空间确保握手完成。如果终端不扩大其缓存,则其必须以错误码CRYPTO_BUFFER_EXCEEDED
关闭连接。
一旦握手完成,如果终端不能缓存加密帧中的全部数据,那么其可以丢弃该帧及其随后收到的所有加密帧,也可以以错误码CRYPTO_BUFFER_EXCEEDED
关闭连接。必须确认包含被丢弃的加密帧的数据包,因为该数据包已经被接收且被传输层处理了,即使是以丢弃加密帧的方式处理的。
8. 地址验证
8.1 连接建立期间的地址验证
连接建立为两侧终端隐式提供了地址验证。具体来说,接收到一个受握手密钥保护的数据包证实了对端已成功处理了初始数据包。一旦终端成功处理了来自对端的握手数据包,它就能将对端地址认定为已验证。
除此之外,如果对端使用了一个由终端选择的连接ID并且连接ID包含至少64位的熵,那么终端可以将对端地址认定为已验证。
对客户端来说,它的首个初始数据包中目标连接ID字段的值允许它有机会验证服务器的地址,只要服务器能成功处理任一数据包。来自服务器的初始数据包会受到从那个目标连接ID字段的值衍生的密钥的保护(详见《QUIC-TLS》的第5.2章)。另一方面,这个值会被服务器在版本协商包(详见第6章)中被回显,或在重试数据包的完整性标签中被使用(详见《QUIC-TLS》的第5.8章)。
在验证客户端地址前,服务器发送的字节数必须不超过已接收字节数的三倍。这就限制了所有能用伪造源地址的方式实施的放大攻击的量级。出于在地址验证前避免放大攻击的目的,服务器必须统计在数据报中接收的载荷的字节数,每个数据报都是归属于唯一的某条连接的。这里的数据报既包括了含有被成功处理的数据包的数据报,也包括了含有被全部丢弃的数据包的数据报。
客户端必须确保包含初始数据包的UDP数据报含有至少1200字节的UDP载荷,不够就加填充帧。发送扩充过的数据报的客户端允许服务器在完成地址验证前发送更多数据。
如果来自服务器的初始数据包或握手数据包遭遇丢包,并且客户端没有发送额外的初始数据包或握手数据包,那么这会引发死锁。当客户端已经接收到了所有它发出的数据的确认但服务器抵达了抗放大上限,那么死锁就会发生。在这种情况下,当客户端没有理由发送额外的数据包时,服务器将无法发送更多数据,因为它还没有验证过客户端的地址。为了避免这种死锁,客户端必须在一个探测包超时(PTO)的时候发送一个数据包;详见《QUIC恢复》的第6.2章。具体来说,如果客户端没有握手密钥,那么它必须用包含至少1200字节的UDP数据报发送一个初始数据包,否则发送的是握手数据包。
服务器可能想要在开始加密握手前验证客户端地址。QUIC使用一个包含在初始数据包的令牌以在完成握手前提供地址验证。这个令牌是在连接建立期间通过重试数据包(详见第8.1.2章),或在之前的连接中通过新令牌帧(详见第8.1.3章),分发给客户端的。
除了在地址验证通过前被施加发送限制之外,服务器还在能发送的内容上被拥塞控制器所设置的上限所限制。客户端则仅受拥塞控制器的限制。
8.1.1 令牌构建
构建在新令牌帧或重试数据包中发送的令牌时,必须使用一种能够令服务器识别出令牌提供给客户端的途径的方法。这些令牌被携带于相同字段中但是需要服务器进行不同的处理。
8.1.2 使用重试数据包进行地址验证
当接收到客户端的初始数据包时,服务器可以通过发送包含令牌的重试数据包(详见第17.2.5章)来请求地址验证。接收到重试数据包之后,客户端必须在为那条连接发送的所有初始数据包中重复这个令牌。
作为处理包含了曾在重试数据包中提供的令牌的初始数据包的回应,服务器不能再发送重试数据包;它只能拒绝那条连接或允许连接继续进行。
只要不存在攻击者为它自己的地址生成一个有效令牌(详见第8.1.4章)的可能性,并且客户端有能力将那个令牌返回给服务器,就能证实客户端接收到了令牌。
服务器还能使用重试数据包来推迟连接建立所需的状态数据和处理的成本。要求服务器提供一个不同的连接ID,以及在第18.2章中定义的传输参数original_destination_connection_id
(原始目标连接ID),能强制服务器表明它,或和它一起工作的实体,从客户端接收到了原始的初始数据包。提供一个不同的连接ID还确保服务器对后续数据包如何路由有一定的控制能力。这可以被用来将连接指向不同的服务器实例。
如果服务器接收到了一个除了无效的重试令牌字段外都合法的客户端初始数据包,那么它能肯定客户端不会接收另一个重试令牌。服务器可以丢弃这样的数据包,并且允许客户端以超时的形式检测到本次握手的失败,但这会对客户端施加强烈的延迟惩罚。取而代之的是,服务器应该使用错误INVALID_TOKEN
(无效令牌)立即关闭(详见第10.2章)连接。注意,服务器在这个时候还没有为连接建立任何状态数据所以不会进入到关闭状态。
图9展示的是关于重试数据包的使用的流程图。
8.1.3 为将来的连接进行地址验证
在一条连接进行期间,服务器可以向客户端提供一个可以被用于后续连接的地址验证令牌。地址验证对于0-RTT特别重要,因为服务器响应0-RTT数据时可能向客户端发送大量的数据。
服务器使用新令牌帧(详见第19.7章)来向客户端提供一个可以被用于验证将来的连接的地址验证令牌。在将来的连接中,客户端将这个令牌包含在初始数据包中来提供地址验证。客户端必须将令牌包含在所有它发送的初始数据包中,除非有重试数据包将令牌替换为新的值。客户端必须不在将来的连接中使用重试数据包里提供的令牌。服务器可以丢弃任何没有携带期望的令牌的初始数据包。
不像为重试数据包创建的令牌要被立即使用掉,在新令牌帧中发送的令牌即使经过了一段时间也能使用。因此,令牌应该具有一个过期时间,它既可以是一个显式的过期时间,也可以是一个可以被用来动态计算过期时间的签发时间戳。服务器可以将过期时间存储起来或将它以加密的形式包含在令牌中。
使用新令牌帧签发的令牌必须不包含会令观察者将之关联到被签发令牌所在的连接上的信息。例如,它不能包含先前的连接ID或地址信息,除非这些值是经过加密的。除了因为先前发送的新令牌帧遭遇丢包而重新发送的那些帧外,服务器必须确保它发送的任何新令牌帧在所有客户端间都是唯一的。允许服务器区分令牌是来自重试数据包还是新令牌帧的信息还会被并非服务器的实体访问到。
客户端的端口号在两条不同的连接间保持相同是不太可能的;因此验证端口不太可能成功。
如果与某服务器间的连接具有权威性(例如,服务器的名称是被包含在证书中的),那么在新令牌帧中接收到的令牌应该适用于这台服务器。当客户端连接到一台服务器且它对此服务器仍保有对此服务器适用的且未使用的令牌时,它应该在初始数据包的令牌字段中使用令牌。使用令牌可以允许服务器不需要额外花费往返时间就验证客户端地址。客户端必须不在连接到服务器时使用不适用于此服务器的令牌,除非客户端知道签发令牌的服务器和它正在连接的服务器是共享令牌的管理机制的。客户端可以使用来自任何先前与那台服务器的连接的令牌。
令牌允许服务器将令牌签发时所在的连接和任何它被使用时所在的连接间的网络活动关联起来。想要打破在某个服务器上的身份连续性的客户端可以丢弃使用新连接帧提供的令牌。相反,在重试数据包中获取的令牌必须在连接尝试期间被立即使用且不能被用于后续的连接尝试。
客户端不应该在不同的连接尝试间重用来自新令牌帧的令牌。重用令牌使得连接会被网络路径上的实体关联起来;详见第9.5章。
客户端可能在单条连接上接收到数个令牌。只要记得防止可关联性,任何令牌可以被用于任何连接尝试。服务器可以发送额外的令牌既可以为多次连接尝试提供地址验证,又可以取代可能会变为无效的旧令牌。对于客户端来说,这种不明确的目的意味着发送最近的未使用的令牌是最有可能有效的。尽管保存并使用更旧的令牌没有负面影响,但是客户端可以将更旧的令牌视为更不太可能会对此服务器的地址验证有效。
当服务器接收到具有地址验证令牌的初始数据包时,它必须尝试验证令牌,除非它已经完成了地址验证。如果令牌是无效的,那么服务器应该表现得就好像客户端的地址未经验证一样,这包括在必要时发送重试数据包。使用新令牌帧和重试数据包提供的令牌能够被服务器区分(详见第8.1.1章),并且后者会被更严格地验证。如果验证通过,服务器随后应该允许握手继续进行。
注意:将客户端地址视为未经验证的而不是丢弃数据包的原因是,客户端可能是在之前的连接中从新令牌帧中获得令牌的,并且如果服务器丢失过状态,它就可能无法验证令牌,此时若丢弃数据包则会导致连接失败。
在无状态的设计中,服务器可以使用经过加密和认证的令牌来将信息传递给客户端,随后从中恢复信息并用它来验证客户端的地址。令牌没有被整合进加密握手,所以它们是未经认证的。这意味着,客户端将可以重用令牌。为了避免利用这一属性的攻击,服务器可以将令牌中的信息限制为仅仅验证客户端地址时所需要的那些信息。
客户端可以将从一条连接中获取的令牌用于任何使用相同版本的连接尝试。当选择使用的令牌时,客户端不需要考虑正在尝试的连接的其他属性,包括应用协议、会话票证或其他连接属性的可能的选择。
8.1.4 地址验证令牌完整性
地址验证令牌必须是难以猜测的。在令牌中包含一个有着至少128位的熵的随机值会是足够的,但是这需要服务器记录它发送给客户端的值。
基于令牌的方案允许服务器将关于验证的状态数据带来的负载转移到客户端上。要使这种设计正常工作,令牌必须被能够抵御客户端修改或伪造的完整性保护所覆盖。要是没有完整性保护,恶意的客户端就能生成或猜测会被服务器接收的令牌值。只有服务器需要访问令牌的完整性保护密钥。
对令牌制定单一的定义良好的格式是没有必要的,因为服务器即使生成者又是消费者。在重试数据包中发送的令牌应该包含允许服务器验证客户端数据包中的源IP地址和端口是否保持不变的信息。
在新令牌帧中发送的令牌必须包含允许服务器验证客户端的IP地址是否和令牌签发时的值一致的信息。服务器可以使用来自新令牌帧的令牌来做出不发送重试数据包的决定,即使客户端地址已经改变。如果客户端的IP地址已经改变,那么服务器必须遵守抗放大上限;详见第8章。注意,由于NAT的存在,这项要求可能不足以保护其他共享NAT的主机免于放大攻击。
攻击者可能在DDoS攻击中重放令牌来将服务器用作放大器。为了抵御这种攻击,服务器必须确保令牌的重放是被阻止或限制的。服务器应该确保在重试数据包中发送的令牌仅会在一段较短时间内被接受,因为它们会被客户端立即返回。在新令牌帧(详见第19.7章)中提供的令牌应该有更长的有效时间但是不应该能被多次接受。建议服务器尽可能只允许令牌被使用一次;令牌可以包含关于客户端的额外信息来进一步缩减适用范围或重用机会。
8.2 路径验证
两侧终端在连接迁移期间(详见第9章)都会使用路径验证以在地址发生变化后验证可达性。在路径验证中,终端会测试特定的本地地址和特定的对端地址间的可达性,这里的地址指的是IP地址和端口的二元组。
路径验证测试的是在一条路径上发送给对端的数据包有没有被对端接收到。使用地址验证是为了确保从正在迁移的对端接收到的数据包携带着的源地址不是伪造的。
地址验证并不验证对端能够在返回的方向上发送数据。确认不能被用于返回路径的验证,因为它们携带的熵不够并且可能是伪造的。不同终端独立判断一条路径在各自方向上的可达性,因此返回方向的可达性只能由对端建立。
任一终端都可以在任意时间使用路径验证。例如,某个终端可能想在对端沉默了一段时间后检查对端是否还位于原来的地址上。
路径验证不是被作为NAT遍历机制设计出来的。尽管这里描述的机制可能对创建支持NAT遍历的NAT绑定也是有效的,但我们期望的是某个终端在还没有在一条路径上发送过数据包时就能够接收到数据包。有效的NAT遍历需要额外的同步机制,但这里没有提供。
终端可以在地址验证时将通道挑战帧和回复通道帧与其他类型的帧一起使用。特别是,终端可以在路径最大传输单元发现(PMTUD)中一起使用填充帧和通道挑战帧;详见第14.2.1章。终端还可以在发送回复通道帧时带上自己的通道挑战帧。
终端在从新的本地地址发送探测包时使用新的连接ID;详见第9.5章。当探测新路径时,终端得确保对端有一个可以用来响应的未使用的连接ID。只要对端的active_connection_id_limit
(活跃连接ID上限)允许,在同一个数据包中同时发送新连接ID帧和通道挑战帧可以确保对端在发送响应时有一个未使用的连接ID。
终端可以选择同时探测数条路径。同时探测的路径数量受到对端目前已提供的额外连接ID数量的限制,因为每次为探测包使用新的本地地址时都需要一个未曾使用的连接ID。
8.2.1 发起地址验证
要发起路径验证,终端在要被验证的路径上发送一个包含不可预测的载荷的通道挑战帧。
终端可以发送多个通道挑战帧以抵御数据包丢包。然而,终端不应该使用同一个数据包发送这些通道挑战帧。
在用包含通道挑战帧的数据包探测新路径时,终端不应该发送得比初始数据包还要频繁。这确保连接迁移不会比建立新连接施加更多负载给新路径。
终端必须在每个通道挑战帧中使用不可预测的数据以使得它能将对端的响应与对应的通道挑战帧关联起来。
终端必须将包含通道挑战帧的数据报扩充至至少1200字节,这个在允许的最大数据报尺寸中的最小值,除非那条路径的抗放大上限不允许发送那么大的数据报。发送这个尺寸的UDP数据报确保终端与对端间的网络路径能够承载QUIC数据包;详见第14章。
当终端因为抗放大上限而不能将数据报扩充至1200字节时,路径的MTU将不会被验证。为了确保路径的MTU足够大,终端必须通过在至少1200字节的数据报中发送通道挑战帧的方式进行第二次路径验证。这次额外的验证可以在回复通道帧被成功接收到之后再进行,也可以等到在路径上收到了足够数量的字节而使得发送一个大一点的数据包也不会超过抗放大上限的时候再进行。
不像其他需要扩充数据报的情况,如果数据报包含了通道挑战帧或回复通道帧,那么终端必须不丢弃看起来过小的数据报。
8.2.2 路径验证的响应
当接收到通道挑战帧时,终端必须响应回复通道帧,并在其中回显通道挑战帧里的数据。除非被拥塞控制限制,终端必须不推迟传输包含回复通道帧的数据包。
回复通道帧必须在接收到通道挑战帧的网络路径上发送。这确保对端的路径验证仅当路径在两个方向上同时可用时才通过。这项要求必须不被发起路径验证的终端强制执行,因为这会使得针对迁移的攻击变为可能;详见第9.3.3章。
终端必须将包含回复通道帧的数据报扩充至至少1200字节,这个在允许的最大数据报尺寸中的最小值。这样就能验证这条路径在两个方向上都能够承载这个尺寸的数据报。然而如果产生的数据超过了抗放大上限,那么终端必须不扩充包含回复通道帧的数据报。这种情况应只会发生在接收到的通道挑战帧没有用扩充过的数据报发送时。
终端必须不在响应单个通道挑战帧时发送超过一个回复通道帧;详见第13.3章。对端应当根据需要发送更多通道挑战帧来唤起额外的回复通道帧。
8.2.3 地址验证通过
当接收到的回复通道帧包含着曾在通道挑战帧中发送的数据时,地址验证通过。在任何网络路径上接收到的回复通道帧所验证的路径都是发送通道挑战帧时使用的那条。
如果终端在一个未扩充至至少1200字节的数据报中发送通道挑战帧,并且它的响应验证了对端地址,那么该路径就是验证通过的,但这不包括路径的MTU。于是,终端现在可以发送超过已接收数据量三倍的数据。然而,终端必须使用经扩充的数据包发起另一次路径验证,来验证这条路径支持所需的MTU。
接收到对包含通道挑战帧的数据包的确认这一事实并不能作为合适的验证方法,因为确认可以是被恶意对端伪造出来的。
8.2.4 地址验证失败
仅当发起地址验证的终端放弃了验证地址的尝试时,地址验证失败。
终端应该基于倒计时放弃地址验证。在设置倒计时时,QUIC实现要注意新路径可能有着比原来的路径更长的往返时间。推荐使用的值是当前PTO和新路径PTO(使用《QUIC恢复》中定义的kInitialRtt
)中的较大值的三倍。
这个倒计时使得在地址验证失败前可以经过数个PTO,于是一两个通道挑战帧或回复通道帧的丢包不会引起整个路径验证的失败。
注意,终端可能在新路径上接收到包含其他类型的帧的数据包,但是要使得路径验证通过,具有正确数据的回复通道帧是必需的。
当终端放弃地址验证时,它将这条路径认定为不可用。这不一定会暗示连接的失败——终端可以通过其他合适的路径继续发送数据包。如果没有路径可用,终端可以等待出现一条新的可用路径或关闭连接。没有通往对端的有效网络路径的终端可以用连接错误NO_VIABLE_PATH
(无可行通道)来发送信号,不过这只有在有网络路径存在但是不支持所需的MTU(详见第14章)的情况下才可行。
地址验证可能因为除了失败外的其他原因而被放弃。这主要会发生在对旧路径进行验证期间发起了前往新路径的连接迁移时。
9. 连接迁移
连接ID的使用使得连接能经受住终端地址(IP地址和端口)的变化,例如由终端迁移至新的网络环境而引起的变化。本章描述了终端迁移至新地址的过程。
QUIC的设计要求终端在握手过程中保持稳定的地址。在握手确认前,终端必须不发起连接迁移,详见《QUIC-TLS》的第4.1.2章。
如果对端发送了传输参数disable_active_migration
(禁止活跃迁移),终端在握手过程中还必须不从不同于当前地址的本地地址发送数据包(包括探测数据包,详见第9.1章),除非终端已经对来自对端的传输参数preferred_address
(首选地址)作出反应。如果对端违反了这项要求,终端必须要么丢弃来自那条路径的传入数据包而不创建无状态重置,要么用路径验证来应对并且允许对端迁移。创建无状态重置或关闭连接将允许网络中的第三方用伪造或操作被观察的流量的方式使得连接被关闭。
不是所有的对端地址变化都是有意的,或活跃的,迁移。对端可能经历NAT重绑定:一种因中间设备,通常是NAT,为数据流分配新的传出端口或甚至新的出口IP地址而引起的地址变化。如果终端检测到任何与对端地址相关的变化,那么它必须进行地址验证(详见第8.2章),除非它先前已经验证过那个地址。
当终端没有经过验证的路径来发送数据包时,它可以丢弃连接的状态数据。在丢弃连接的状态数据前,有能力进行连接迁移的终端可以等待新的可用地址。
本文档限制连接迁移为仅客户端可以发起,除非是在第9.6章中描述的情况。客户端负责发起所有迁移。服务器在它们从某客户端地址接收到非探测数据包(详见第9.1章)前,不会向那个地址发送非探测数据包。如果客户端从一个未知的服务器地址接收到了数据包,那么客户端必须丢弃这些数据包。
9.1 探测新路径
在将连接迁移到新的本地地址前,终端可以使用地址验证(详见第8.2章)从新的本地地址探测对端的可达性。地址验证的失败仅仅意味着新路径对此条连接来说无法使用。地址验证的失败不会引起连接被关闭,除非不存在其他可替代的有效路径。
通道挑战帧、回复通道帧、新连接ID帧和填充帧都属于“探测帧”,而其他类型的帧都是“非探测帧”。仅仅包含探测帧的数据包是“探测数据包”,而包含了其他类型的帧的数据包就是“非探测数据包”。
9.2 发起连接迁移
终端可以通过从新的本地地址发送包含非探测帧的数据包的方式来迁移连接。
每个终端都会在连接建立期间验证对端的地址。因此,正在迁移的终端可以在知道对端在其当前地址上愿意接收数据的前提下向对端发送数据。于是,终端不需要先验证对端的地址就可以迁移至新的本地地址。
为了在新的路径上建立可达性,终端在新路径上发起路径验证(详见第8.2章)。终端可以推迟路径验证,直到对端向它的新地址发送下一个非探测帧之时。
在迁移时,新路径可能不支持终端当前的发送速率。因此,终端如第9.4章所描述的那样重置它的拥塞控制器和RTT预估。
新的路径可能不具有相同的ECN功能。因此,终端如第13.4章所描述的那样验证ECN功能。
9.3 响应连接迁移
从新的对端地址接收到一个包含非探测帧的数据包表明对端已经迁移到那个地址。
如果接收方承认这次迁移,那它必须改向新的对端地址发送后续数据包,并且,如果还没有发起,则必须发起路径验证(详见第8.2章)来验证对端对那个地址的所有权。如果接收方没有来自对端的尚未使用的连接ID,那么在对端提供一个之前它将无法在新路径上发送任何数据,详见第9.5章。
终端仅在响应具有最大数据包号的非探测数据包时改变他将数据包发向的地址。这确保终端不会在接收到乱序数据包时将数据包发向旧的对端地址。
终端可以向未经验证的对端地址发送数据,但它必须抵御如第9.3.1章和第9.3.2章所述的潜在攻击。如果对端地址最近被见到过,那么终端可以跳过验证。特别是,如果终端在检测到某些形式的虚假迁移后返回先前验证过的路径,那么跳过地址验证并恢复丢包检测和拥塞控制状态可以减少由攻击带来的性能影响。
终端在改变它将非探测数据包发向的地址后,它可以放弃为其他地址进行的路径验证。
接收到一个来自新对端地址的数据包可能是对端遭遇NAT重绑定的结果。
在验证新的客户端地址后,服务器应该向客户端发送新的地址验证令牌(详见第8章)。
9.3.1 来自对端的地址伪造
9.3.2 来自路径上设备的地址伪造
在路径上的攻击者可以通过拷贝并转发具有伪造地址的数据包并使它比原始数据包更早到达的方式引发虚假的连接迁移。具有伪造地址的数据包将被视为来自连接迁移,而原始数据包将被视为重复并被丢弃。在一次虚假迁移之后,对源地址的验证将会失败,因为在源地址上的实体没有读取或响应发给它的通道挑战帧所需的加密密钥,哪怕它真的想这么做。
为了保护连接免于因为这样的虚假迁移而失败,当对于新对端地址的验证失败后,终端必须回退并使用最后一个经验证的对端地址。除此之外,从合法对端地址接收到具有最大数据包号的数据包,将触发另一次连接迁移。这将使得对虚假迁移的地址的验证被放弃,其中正包括由注入单个数据包的攻击者发起的迁移。
如果终端没有关于最后一个经验证的对端地址的状态,它必须以丢弃所有此连接的状态数据的方式静默地关闭连接。这会造成这条连接上的新数据包被以常规方式处理。例如,终端可以在响应将来的传入数据包时发送无状态重置。
9.3.3 来自非路径上设备的数据包转发
不在路径上的但能观察数据包的攻击者可以将真实数据包的副本转发到终端上。如果数据包副本比真实数据包更早到达,这就会表现得像是NAT重绑定一样。任何真实数据包都会因为重复而被丢弃。如果攻击者有能力继续转发数据包,它就有可能使得迁移的路径经过攻击者。这会使得攻击者位于路径上,给予它观察或丢弃任何后续数据包的能力。
这种形式的攻击要求攻击者使用一条有着和终端间的直连路径近乎相似的特征的路径。如果发送的数据包相对较少,或数据包丢包与计划的攻击同时发生,那么这种攻击的成功率就更加可靠。
在原始路径上接收到的会增加最大的已接收到的数据包号的非探测数据包将使得终端移动回那条路径。在这条路径上引发数据包增加了攻击失败的可能性。因此,这种攻击的抵御依赖于触发数据包交换。
在响应一个看起来很像的迁移时,终端必须使用通道挑战帧验证先前的活跃路径。这能在那条路径上引入新数据包的发送。如果那条路径不再可用,那么验证路径的尝试会超时并失败;如果路径时可用的但是不再被需要,验证就会成功但是仅仅使得那条路径上被多发送了几个探测数据包。
在活跃路径上接收到通道挑战帧的终端应该在发送非探测数据包作为响应。如果这个非探测数据包比任何攻击者创建的副本到达得都要早,这就能使得连接被迁移回原始路径。任何后续去往另一条路径的迁移都会重启上述整个过程。
这种防御是不完美的,但是它不被认为是一个严肃的问题。如果在尝试过数次原始路径的情况下,通过攻击者的路径还总是比原始路径更快,那么这很难说究竟是一次攻击,还是对路由的优化。
终端还可以使用启发式的方法来提升对这种形式的攻击的检测。例如,如果最近在旧路径上接收到过数据包,那么NAT重绑定是不太可能发生的;类似地,重绑定在IPv6路径上是很少见的。终端也可以寻找有没有重复的数据包。或者换个思路,连接ID上的改变更有可能表明这是一次有意的迁移,而不是一次攻击。
9.4 丢包检测和拥塞控制
新路径上的可用功能可能和旧路径上的不一样。在旧路径上发送的数据包必须不被计入新路径的拥塞控制或RTT预估中。
当承认对端对它新地址的所有权时,终端必须立即将新路径的拥塞控制器和往返时间预估器重置为初始值(详见《QUIC恢复》的附录A.3和附录B.3),除非对端地址的改变仅仅是换了个端口。因为仅改变端口通常是NAT重绑定或其他中间设备活动的结果,终端在这种情况下可以沿用拥塞控制状态和往返时间预估,而不是将它们重置为初始值。在来自旧路径的拥塞控制状态被沿用至具有完全不同特征的新路径上时,发送方一开始可能传输得过于激进,直到拥塞控制器和RTT预估器适应下来为止。一般来说,建议QUIC实现在沿用旧值至新路径时要谨慎。
在迁移期间,当终端发送数据和探测包自/至不同地址时,接收方一侧可能出现很明显的乱序,这是因为使用两条不同路径会有不同的往返时间。来自不同路径上的数据包的接收方仍然要为所有接收到的数据包发送ACK帧。
尽管在连接迁移期间会用到多条路径,但是最少只要使用单个拥塞控制上下文和单个丢包恢复上下文(如《QUIC恢复》中所述)就足够应对了。比如,终端可以推迟切换至新的拥塞控制上下文,直到它确认不再需要旧路径(就像第9.3.3章中描述的情形一样)。
发送方可以将探测数据包视作特例,以使得它们的丢包检测是独立的且不会使得拥塞控制器过度地抑制它的发送速率。当发送通道挑战帧时,终端可以设置一个单独的倒计时,并当相应的回复通道帧被接收到时取消倒计时。如果倒计时在回复通道帧前归零,终端可以发送一个新的通道挑战帧并且启动一个时间更长的倒计时。这个倒计时应该如《QUIC恢复》的第6.2.1章描述的那样被设置,并且必须不变得越来越严格。
9.5 连接迁移对隐私的影响
在不同网络路径上使用相同的连接ID将允许被动观察者从那些路径上将网络活动互相关联起来。在不同网络间移动的终端可能不希望它们的网络活动被不是对端的任何实体给关联起来,所以当从不同的本地地址发送时,会使用不同的连接ID,如第5.1章所讨论的那样。为了使这个方法有效,终端应该确保它们提供的那些连接ID不会被任何其他实体找出相关性。
终端随时可以改变传输时所用的目标连接ID为一个从未在别的路径上使用过的值。
当发送自不止一个本地地址时——例如在如第9.2章所描述的发起连接迁移时或在如第9.1章所描述的探测新网络路径时——终端必须不重用连接ID。
类似地,当发送至不止一个目标地址时,终端必须不重用连接ID。由于不受对端控制的网络条件变化,终端可能从一个新的源地址接收到具有相同目标连接ID字段值的数据包,这时它可以在使用新的远程地址时继续使用当前连接ID,但是仍然从相同的本地地址发送。
以上有关重用连接ID的要求仅适用于数据包发送,因为数据包发送时的连接ID不变但是数据包传输的路径发生意想不到的变化的情况是有可能的。例如,在网络经过一段时间的沉默后,客户端恢复发送时,NAT重绑定可能使得数据包在一条新路径上被发送。终端如第9.3章所描述的那样响应这类事件。
为每条新网络路径上双向发送的数据包使用不同的连接ID消除了连接ID对于从不同网络路径中的相同连接间将数据包互相关联的作用。而头部保护确保了数据包号不能被用来关联网络活动。但这无法阻止数据包的其他属性,例如计时和尺寸,被用于关联网络活动。
终端不应该在对端申请了零长度连接ID时发起迁移,因为经过新路径的流量与经过旧路径的会很容易地被关联起来。如果服务器有能力将具有零长度连接ID的数据包与正确的那条连接关联起来,那么这就意味着服务器正使用其他信息来解除数据包的多路复用。举个例子,服务器可以给每个客户端提供唯一的地址——比如通过使用HTTP替代服务(详见《ALTSVC》)。这类能允许不同网络路径上的数据包被正确路由的信息同样能允许非对端的实体关联这些路径上的网络活动。
在沉默一段时间后,客户端在发送流量时可能希望通过切换至新连接ID、源UDP端口或IP地址(详见《RFC8981》)的方法减少网络活动的可关联性。改变用来发送数据包的地址同时会使得服务器检测到连接迁移。这确保了用于支持迁移的机制即使是对没有经历NAT重绑定或实际迁移的客户端来说也是用得上的。改变地址会使得对端重置它的拥塞控制状态(详见第9.4章),所以地址应该仅偶尔地被改变。
耗尽可用连接ID的终端既不能探测新路径或发起迁移,也不能响应探测包或对端发起的迁移。为了确保迁移可行并且在不同路径上发送的数据包不会被关联,终端应该在对端迁移前提供新连接ID,详见第5.1.1章。如果对端可能已经耗尽了可用的连接ID,正在迁移的那个终端可以在所有在新网络路径上发送的数据包中包含一个新连接ID帧。
9.6 服务器的首选地址
QUIC允许服务器在一个IP地址上接受连接,然后在握手之后立刻尝试将这些连接转移到一个更优先使用的地址。这在客户端起初连接到由多个服务器共享的地址但想优先使用一个单播地址来确保连接稳定性时尤其有用。本节描述了这种将连接迁移到首选服务器地址的协议。
在这份文档制定的QUIC规范版本中,在连接途中迁移到新服务器地址是不受支持的。如果客户端在还没有发起一次前往新服务器地址的迁移的情况下就从那个地址接收到了数据包,那么客户端应该丢弃这些数据包。
9.6.1 传达首选地址
服务器通过在TLS握手中包含传输参数preferred_address
的方式传达首选地址。
服务器可以为每一种地址族(IPv4和IPv6)传达首选地址,以允许客户端选择一个最适合它们网络连接的地址。
一旦握手被确认,客户端就应该选择客户端提供的两个地址中的一个并发起地址验证(详见第8.2章)。客户端从传输参数preferred_address
或新连接ID帧中获取并使用任何之前没用过的活跃连接ID来构建数据包。
只要路径验证通过,客户端就应该开始使用新连接ID向新服务器地址发送所有将来的数据包,并且不再使用旧服务器地址。如果地址验证失败,那么客户端必须将所有将来的数据包继续发到服务器的原始IP地址。
9.6.2 迁移至首选地址
迁移至首选地址的客户端必须在迁移前验证它选择的地址,详见第21.5.3章。
服务器可能在接受一条连接后的任何时间接收到一个发到它的首选IP地址的数据包。如果这个数据包包含了通道挑战帧,那么服务器就要像第8.2章所描述的那样发送回复通道帧。服务器必须保持从它的原始地址发送非探测数据包,直到它在首选地址上接收到了来自客户端的非探测数据包并且它对新路径完成了验证。
服务器必须从它的首选地址探测通向客户端的路径。这有助于抵御由攻击者发起的虚假迁移。
一旦服务器完成了路径验证并且在首选地址上收到了具有新的最大数据包号的非探测数据包,它就开始仅仅从首选IP地址向客户端发送非探测数据包。如果服务器在旧IP地址上接收到了属于这条连接的更加新的数据包,那么服务器应该丢弃它们。服务器可以继续处理在旧IP地址上接收到的被延误的数据包。
服务器在传输参数preferred_address
中提供的地址仅对提供地址时所处的连接有效。客户端必须不将它们用于其他连接,包括从当前连接恢复的连接。
9.6.3 客户端迁移与首选地址的交互
客户端可能想要在迁移到服务器的首选地址前先进行一次连接迁移。在这种情况下,客户端应该从它的新地址同时对原始的和首选的服务器地址进行路径验证。
如果对服务器首选地址的路径验证通过了,那么客户端必须放弃对原始地址的验证并且迁移至服务器的首选地址。如果对服务器首先地址的路径验证失败了但是对服务器原始地址的的验证成功了,那么客户端可以迁移到它的新地址,并且继续向服务器的原始地址发送数据。
如果在服务器的首选地址接收到的数据包有着与在握手期间观测到的值不同的客户端源地址,那么服务器必须如第9.3.1章和第9.3.2章描述的那样保护自己免受潜在的攻击。除了有意的同时迁移,这种情况还会因为客户端的网络在访问服务器首选地址时使用了不同的NAT绑定而发生。
服务器应该在接收到来自不同地址的探测数据包时向客户端的新地址发起路径验证,详见第8章。
迁移到新地址的客户端应该使用与服务器正使用的地址族一致的首选地址。
由传输参数preferred_address
提供的连接ID不是特定于参数中提供的地址的。提供这个连接ID是为了客户端有一个可以用来迁移的连接ID,但客户端可以在任何路径上使用这个连接ID。
9.7 迁移与IPv6流标签的使用
10. 连接终止
10.1 空闲超时
如果任一终端在其传输参数(详见第18.2章)中指定了max_idle_timeout
(最大空闲超时时间),且某个连接持续空闲的时间超过了两个终端宣告的max_idle_timeout
中的较小值,那么这个连接就会被静默关闭,并且它的状态会被丢弃。
每个终端都宣告max_idle_timeout
,但是只取两个值中较小的那一个(或只取有效的那个值,如果只有一个终端宣告了非零值)作为终端使用的有效值。通过宣告max_idle_timeout
,终端承诺,如果它要在空闲时间还未达到上述有效值时就放弃一条连接,那么它会发起立即关闭(详见第10.2章)。
当成功接收且处理了一个来自对端的数据包时,终端重启它的空闲计时器。当正要发送ACK触发包时,如果自上一次接收并处理数据包后还没有发送过任何ACK触发包,那么终端也会重启它的空闲计时器。在发送数据包时重启这个计时器确保新的活动被发起后连接不会被马上关闭。
为了避免过小的空闲超时时间,终端必须提高空闲超时时间至少到当前探测包超时时间(PTO)的三倍。这使得在空闲超时前有充足的时间供数个探测包被发送和丢失。
10.1.1 存活确认
10.1.2 推迟空闲超时
如果终端正在等待响应数据但是没有或无法发送应用数据,那么终端可能需要发送ACK触发包以避免空闲超时。
QUIC实现可以向应用提供选项来推迟空闲超时。当应用希望避免丢失已经和一条连接关联的状态但不希望花时间重新交换应用数据,那么就可以使用这个选项。开启这个选项时,终端可以定期发送一个Ping帧(详见第19.2章),这会使得对端重置自己的空闲超时定时器。如果这包含Ping帧的数据包是自上一次接收到数据包后发送的第一个ACK触发包,那么发送这个包还会重置当前终端的空闲超时定时器。发送Ping帧会使得对端用确认来响应,这也会重置当前终端的空闲超时定时器。
使用QUIC的应用协议应该提供有关合适的推迟空闲超时的时机的指导。不必要地发送Ping帧可能对性能有负面影响。
如果一条连接上在一段超过用传输参数max_idle_timeout
协商的时间内,没有数据包被发送或接收,那么这条连接会超时,详见第10章。然而,网络中间设备的状态可能比那个时间更早地超时。尽管《RFC4787》中的第5条要求推荐了每2分钟的超时间隔,但是经验表明至少要每30秒发送一次数据包才能避免大多数中间设备丢失有关UDP流量的状态,详见《GATEWAY》。
10.2 立即关闭
终端发送连接关闭帧(详见第19.19章)来立即终止连接。连接关闭帧使得所有流都被立即关闭;开放的流可以被假定为被隐式重置。
在发送连接关闭帧之后,终端立即进入关闭状态,详见第10.2.1章。在收到连接关闭帧之后,终端进入排空状态,详见第10.2.2章。
对协议的违背将引发立即关闭。
在应用协议计划关闭一条连接后,可以使用立即关闭。这时可能是应用协议协商了一次正常关闭。应用协议可以交换用于两侧应用终端协商关闭连接的消息,在那之后请求QUIC关闭连接。当QUIC因此关闭连接后,带着由应用提供的错误码的连接关闭帧会被用于向对端发送关闭的信号。
关闭和排空状态用于确保连接被干净地关闭,以及延误的和乱序的数据包被正确地丢弃。这些状态的持续时间应该至少为当前PTO间隔(定义于《QUIC恢复》)的三倍大小。
在退出关闭或排空状态前就处理连接的状态数据,可能导致终端在收到被延误的数据包时不必要地发起无状态重置。有一些备用手段来确保被延误的数据包不会引起响应的终端,比如那些有能力关闭UDP套接字的终端,可以更早地结束连接的状态以更快地恢复可用资源。始终保持一个套接字开放以接受新连接的服务器不应该过早结束关闭或排空状态。
一旦终端的关闭或排空状态结束,它就应该丢弃所有连接状态数据。终端在响应任何将来的属于这条连接的传入数据包时,可以发送无状态重置。
10.2.1 连接的关闭状态
终端发起立即关闭后进入关闭状态。
在关闭状态下,终端仅保留足够用于创建包含连接关闭帧的数据包和用于区分数据包是否属于此条已关闭连接的信息。处于关闭状态的终端发送包含连接关闭帧的数据包来响应任何属于这条连接的传入数据包。
终端应该限制它在关闭状态下创建数据包的速率。比如,终端可以在响应接收到的数据包前等待足够的数据包或时间,且等待的数据包数量或时间长短是逐渐增长的。
终端选择的连接ID和QUIC版本是区分数据包是否属于一条关闭状态的连接的充分信息;终端可以丢弃所有其他连接状态数据。处于关闭状态的终端没有必要处理任何接收到的帧。终端可以为传入数据包保留数据包保护密钥用于读取和处理连接关闭帧。
当进入关闭状态时,终端可以丢弃数据包保护密钥,然后在响应任何接收到的UDP数据报时都发送包含连接关闭帧的数据包。然而,丢弃了数据包保护密钥的终端没有能力区分并丢弃不合法的数据包。为了避免被用于放大攻击,这样的终端必须限制它发出的数据包尺寸总和不超过它接收到的且属于这条连接的数据包尺寸总和的三倍。为了最小化终端为关闭状态的连接所维护的状态数据,终端可以在响应任何接收到的数据包时发送完全相同的数据包。
注意:允许在关闭状态下重传完全相同的数据包是“每个数据包都必须使用新的数据包号”要求的一个特例,详见第12.3章。发送新的数据包号主要是用于丢包恢复和拥塞控制,但这两者都和一个已关闭的连接关系不大。重传这最后一个数据包需要的状态数据更少。
当处于关闭状态时,终端可能从新的源地址接收到数据包,这可能是连接迁移的结果;详见第9章。处于关闭状态的终端必须要么丢弃从未经验证的地址接收到的数据包,要么限制它发向未经验证的地址的数据包尺寸总和,使其小于它从那个地址接收到的数据包的尺寸总和。
处于关闭状态的终端不应处理密钥更新(详见《QUIC-TLS》的第6章)。密钥更新可能阻止终端从关闭状态转移到排空状态,因为终端将失去处理后续接收到的数据包的能力,但这没有什么影响。
10.2.2 连接的排空状态
一旦终端接收到连接关闭帧,就会进入排空状态,这表明对端已处于关闭或排空状态。虽然在其他方面都和关闭状态一致,不过处于排空状态的终端必须不发送任何数据包。一旦连接处于排空状态,就没有必要再保留数据包保护密钥了。
接收到连接关闭帧的终端可以在进入排空状态前发送一个包含连接关闭帧的数据包,并且,如若合适,使用NO_ERROR
(无错误)代码。除此之外,终端必须不发送更多数据包。不这么做会导致两个终端间保持不断交换连接关闭帧的状态,直到其中一个终端退出关闭状态。
如果终端接收到了连接关闭帧,那么它可以从关闭状态进入排空状态,这表明对端也处于关闭或排空状态。在这种情况下,排空状态会在原来关闭状态应该结束时结束。换句话说,终端使用相同的状态结束时机但是不再在这条连接上传输数据包。
10.2.3 在握手期间立即关闭
发送连接关闭帧时,要确保对端会处理这个帧。通常来说,这意味着将这个帧放到具有最高级别的数据包保护的数据包中发送,以避免数据包被丢弃。在握手确认后(详见《QUIC-TLS》的第4.1.2章),终端必须在1-RTT数据包中发送任何连接关闭帧。然而,在确认握手前,更高级的数据包保护密钥有可能在对端那还不可用,所以可以使用具有较低数据包保护级别的数据包多发送一个连接关闭帧。具体来说:
-
客户端总是知道服务器有没有握手密钥(详见第17.2.2.1章),但是服务器不确定客户端有没有握手密钥。在这种条件下,服务器应该用握手数据包和初始数据包各发送一个连接关闭帧来确保它们中的至少一个是客户端可以处理的。
-
在0-RTT数据包中发送连接关闭帧的客户端不能确信服务器会同意0-RTT的使用。用初始数据包发送连接关闭帧使得服务器更有可能接收到关闭的信号,哪怕应用错误码可能不会被接收到。
-
在确认握手前,对端可能无法处理1-RTT数据包,所以终端应该用握手数据包和1-RTT数据包各发送一个连接关闭帧。服务器则应该用初始数据包发送连接关闭帧。
用初始数据包或握手数据包发送类型为0x1d
的连接关闭帧会暴露应用状态或被用于更改应用状态。用初始数据包或握手数据包发送时,必须将类型为0x1d
的连接关闭帧替换为类型为0x1c
的连接关闭帧。否则,有关应用状态的信息可能被泄露。终端在转换连接关闭帧的类型到0x1c
时必须清除原因语句字段的值,并且应该使用APPLICATION_ERROR
(应用错误)代码。
用不同数据包类型发送的连接关闭帧可以被合并至单个UDP数据报中;详见第12.2章。
终端可以用初始数据包发送连接关闭帧。这时可能是在响应接收自初始数据包或握手数据包的未通过认证的信息。这样的立即关闭行为可能将合法的连接暴露在拒绝服务攻击之下。QUIC对于握手过程中来自路径上设备的攻击并不提供防御措施;详见第21.2章。然而,如果牺牲发向合法对端的关于错误的反馈,并且终端直接丢弃不合法的数据包而不是用关闭连接帧终止连接,那么部分形式的拒绝服务攻击对于攻击者来说会变得更难实施。出于这个原因,如果在缺乏认证的数据包中检测出了错误,那么终端可以丢弃数据包而不是选择立即关闭。
尚未建立状态的终端,例如在初始数据包中检测到错误的服务器,并不会进入关闭状态。在发送连接关闭帧时,没有关于连接的状态的终端不会进入关闭或排空状态。
10.3 无状态重置
如果终端无法访问连接的状态数据,那么无状态重置将是其最后手段。崩溃或中断可能造成对端持续向一个没有正常地维持连接的终端发送数据。终端可以在接收到一个它无法关联到某个活跃连接的数据包时发送无状态重置作为响应。
无状态重置不适合用来表明在活跃连接中出现的错误。想要传达致命的连接错误这一消息的终端在有能力的情况下必须使用连接关闭帧。
为了支持无状态重置,终端会签发一个无状态重置令牌,它是一个难以猜测的16字节长的值。如果对端后续收到了无状态重置,也就是一个以那个无状态重置令牌结尾的UDP数据报,那么对端将立即结束这条连接。
每个无状态重置令牌都是特定于某个连接ID的。终端签发无状态重置令牌时,将它的值包含在新连接ID帧的无状态重置令牌字段中。服务器还可以在握手期间签发传输参数stateless_reset_token
(无状态重置令牌),它会被应用到服务器在握手期间选择的连接ID上。这些信息交换是受到加密保护的,所以只有客户端和服务器知道它们的值。注意,客户端不能使用传输参数stateless_reset_token
,因为它们的传输参数没有可信度保护。
当令牌关联的连接ID被停用连接ID帧(详见第19.16章)停用时,令牌将失效。
如果终端接收到了它无法处理的数据包,那么它会以这种形式发送一个数据包:
无状态重置 {
固定比特位 (2) = 1,
不可预测的比特位 (38..),
无状态重置令牌 (128),
}
这种设计确保无状态重置——尽可能地——无法和常规的短包头数据包区分。
无状态重置使用从数据包头部的最前面两个比特位开始的一整个UDP数据报。首个字节的剩余部分和在它之后任意数量的字节都应该被设置为无法分辨是不是随机的值。数据报的最后16个字节包含了无状态重置令牌。
对于不是意图的接收方的实体来说,无状态重置看起来就是一个短包头数据包。为了让无状态重置表现得像一个合法QUIC数据包,不可预测的比特位应该使用至少38比特的数据(或者说5字节并去掉两个固定比特位)。
如果接收方要求使用连接ID,那么那作为结果的至少21个字节并不保证无状态重置很难与其他数据包区分。为了做到这一点,终端应该确保所有它发送的数据包都比它要求对端在数据包中包含的最小的连接ID的长度要长至少22字节,不够就加填充帧。这么做能确保所有对端发出的无状态重置都无法和发送给当前终端的合法数据包区分。在发送无状态重置来响应一个不超过43字节的数据包时,终端应该发送一个比它要响应的那个数据包要短一个字节的无状态重置。
以上数值假设了无状态重置令牌和数据包保护AEAD的最小扩充量具有相同长度。如果终端协商了具有更大的最小扩充量的数据包保护方案,那么就需要额外的不可预测字节。
终端发送的无状态重置尺寸必须不是它接收到的数据包的三倍或更大,以免被用于放大攻击。第10.3.3章描述了无状态重置尺寸的额外限制。
终端必须丢弃因过小而不合法的QUIC数据包。举个例子,在使用《QUIC-TLS》中定义的AEAD函数组时,小于21字节的短包头数据包永远不可能合法。
终端必须将无状态重置以短包头数据包的格式发送。然而,终端必须将任何以有效无状态重置令牌结尾的数据包视作无状态重置,因为其他QUIC版本可能允许使用长包头。
终端可以在响应长包头数据包时发送无状态重置。在无状态重置令牌在对端处可用前发送无状态重置是没有效果的。在本QUIC版本中,只能在连接建立期间使用长包头数据包。由于无状态重置令牌只有在连接建立完成或临近完成时才可用,忽略长包头数据包的效果和发送无状态重置的效果可能是一样的。
终端不能从短包头数据包中判断源连接ID;因此,它不能在无状态重置中设置目标连接ID。于是目标连接ID会和之前发送的数据包中使用的值不一样。随机的目标连接ID能使连接ID表现得像是迁移至由新连接ID帧提供的新连接ID的结果;详见第19.15章。
使用随机的连接ID会有两个问题:
-
数据包可能无法抵达对端。如果目标连接ID在路由至对端时起关键作用,那么该数据包可能被错误地路由。作为响应,这可能会触发另一个无状态重置;详见第10.3.3章。没有被正确路由的无状态重置使得错误检测和恢复机制失去效果。在这种情况下,终端需要依靠其他方法——比如计时器——来检测连接已失败。
-
随机生成的连接ID可以被不是对端的其他实体识别为可能的无状态重置。偶尔使用不同的连接ID的终端可以对此引入一些不确定性。
这样的无状态重置设计是特定于QUIC版本1的。支持多个QUIC版本的终端创建的无状态重置应该可以被支持所有当前终端所支持的版本(或所有在当前终端丢失状态前可能支持的版本)的对端接受。新QUIC版本的设计者应当注意上述这一点,并且要么(1)重用当前设计,要么(2)使用数据包除了最后16个字节之外的某个部分来携带数据。
10.3.1 检测无状态重置
终端使用UDP数据报末尾的16个字节检测可能的无状态重置。终端记录所有无状态重置令牌,这些令牌关联着连接ID和终端最近发送的数据报的目标远程地址。这些令牌来自新连接ID帧的无状态重置令牌字段和服务器的传输参数,但不包含那些与未使用的或已停用的连接ID关联的无状态重置令牌。终端通过比较数据报的最后16个字节和所有与发送数据报的远程地址关联的无状态重置令牌,来将接收到的数据报识别为无状态重置。
这种比较可以为每个传入数据报进行。如果来自某个数据报的数据包被成功处理了,那么终端可以跳过这个检查。然而,如果一个传入数据报的首个数据包不能与一条连接关联上,或不能被解密,那么必须进行这种比较。
终端必须不为任何关联到它尚未使用或已停用的连接ID上的无状态重置令牌做检查。
在将数据报和无状态重置令牌的值比较时,终端必须在不泄露令牌的值的前提下进行比较。比如,用固定时间进行比较能保护无状态重置令牌免于利用计时侧信道的信息泄露。另一种方法是存储并比较经转换的无状态重置令牌值而不是原始的令牌值,这种转换指的是使用密钥并在密码学上安全的伪随机函数(例如数据块加密算法和散列消息认证码(HMAC,详见《RFC2104》))。终端不必保护有关数据包是否被成功解密和有效的无状态重置令牌数量的信息。
如果数据报的最后16个字节与无状态重置令牌的值一致,那么终端必须进入排空状态,并且在此连接上不再发送数据包。
10.3.2 计算无状态重置令牌
无状态重置令牌必须难以被猜测。为了创建无状态重置令牌,终端可以为每一个它创建的连接随机生成(详见《RANDOM》)一个秘密值。然而,当集群中有多个实例时它会带来协调上的问题,以及在终端丢失状态时带来存储上的问题。无状态重置是为了处理状态丢失的情况而专门设计的,因此这个方法不是最理想的。
通过使用以固定密钥和由终端选择的连接ID(详见第5.1章)作为输入的伪随机函数来生成令牌,单个固定密钥可以被用于与同一个终端有关的所有连接。终端可以使用HMAC(详见《RFC2104》)(例如,HMAC(固定密钥, 连接ID)
)或基于HMAC的密钥衍生函数(HKDF,详见《RFC5869》)(例如,使用固定密钥作为输入密钥材料,使用连接ID作为盐)。这个函数的输出被截断至16字节来作为连接的无状态重置令牌。
丢失状态的终端可以使用相同的方法来生成有效的无状态重置令牌。连接ID来自终端接收到的那个数据包。
这种设计要求对端总是在数据包中发送连接ID,这样终端才可以使用来自数据包的连接ID来重置连接。使用这种设计的终端必须要么为所有连接使用相同长度的连接ID,要么将连接ID长度编码进连接ID以使得它可以在没有状态的情况下恢复。除此之外,它不能提供零长度连接ID。
泄露无状态重置令牌将允许任何实体终止连接,所以一个令牌值只能被使用一次。这种选择无状态重置令牌的方法意味着相同连接ID和固定密钥的组合必须不被用于另一个连接。如果共享同一个固定密钥的实例还使用了相同的连接ID,或者攻击者可以使数据包被路由到没有状态但是使用相同固定密钥的实例,那么就有可能发起拒绝服务攻击;详见第21.11章。如果一个连接ID所在的连接已通过使用无状态重置令牌的方法重置过,那么在共享相同固定密钥的节点上必须不将这个连接ID用于新的连接。
同一个无状态重置令牌必须不被用于多个连接ID。终端不需要将新的令牌值与所有先前的值进行比较,但是重复的值可能被视作类型为PROTOCOL_VIOLATION
(协议违反)的连接错误。
注意,无状态重置不受到任何加密保护。
10.3.3 死循环
无状态重置的设计使得在不知道无状态重置令牌的情况下无法将它与合法的数据包区分开来。举例来说,如果一个服务器往另一个服务器发送了无状态重置,那么作为响应它可能收到另一个无状态重置,这会导致无尽的数据包交换。
终端必须确保每一个它发出的无状态重置都比触发它的数据包要小,除非终端维护着足以避免死循环的状态数据。在循环出现的情况下,这使得数据包最终变得过小而不能触发响应。
终端可以记录它已发送的无状态重置数量并且一旦到达限制数量就不再创建新的无状态重置。为不同的远程地址使用单独的限制可以确保其他对端或连接已到达限制时还能够使用无状态重置来关闭连接。
取决于对端的连接ID长度,小于41字节的无状态重置可能被观察者识别为无状态重置。反过来,对小型数据包不响应无状态重置可能导致无状态重置不能有效地检测连接损坏但是只有小型数据包被发送的情况;这样的失败可能只能被其他方法检测出来,例如计时器。
11. 错误处理
检测到错误的终端应该向对端发送出现了这个错误的信号。传输层和应用层的错误都会影响整条连接,详见第11.1章。只有应用层的错误可以被隔离到单条流上,详见第11.2章。
在发送信号的帧中应该使用最合适的错误码(详见第10章)。这份规范中在描述错误情形的地方会定义这时候使用的错误码;尽管这些定义被称作”要求“,但是不同的实现策略可能导致最终报告不同的错误。特别是,终端在检测到错误情形时,可以使用任何适用的错误码;通用的错误码(比如PROTOCOL_VIOLATION
(协议违背)或INTERNAL_ERROR
(内部错误))总是可以用在其他错误码可以用的地方。
对于可以使用连接关闭帧或流重置帧来发送信号的错误,不适合使用无状态重置(第10.3章)。具有足够的状态数据来在连接上发送帧的终端必须不使用无状态重置。
11.1 连接错误
如果出现的错误会导致连接变得不可用,比如违背了协议语义或损坏了可能影响整条连接的状态数据,那么必须使用连接关闭帧(详见第19.19章)来发送信号。
与应用相关的协议的错误出现时,使用帧类型为0x1d
的连接关闭帧来发送信号。与传输相关的错误,包括所有在这份文档中描述的错误,都用帧类型为0x1c
的连接关闭帧来携带。
携带连接关闭帧的数据包可能会遭遇丢包。终端应该准备好当在一条已终止的连接上收到更多数据包时,重传包含连接关闭帧的数据包。限制重传的次数和发送这个最终的数据包的时机能够限制在已终止的连接上投入的资源。
选择不重传连接关闭帧的终端需要承担对端错失首个包含连接关闭帧的数据包的风险。在一条已终止的连接上继续接收数据的终端,其唯一可用的机制是启动无状态重置的流程(第10.3章)。
鉴于初始数据包的AEAD不提供强力的认证,终端可以丢弃不合法的初始数据包。即使在本规范要求产生连接错误的地方,丢弃初始数据包也是允许的。只有终端不能处理某个数据包中的帧或回退所有已处理部分产生的副作用时,它才能丢弃这个数据包。丢弃不合法的初始数据包可以被用来减少拒绝服务产生的风险,详见第21.2章。
11.2 流错误
如果一个应用层错误仅影响了单个流且它所在的连接处于可以恢复的状态,那么终端可以发送一个使用适当的错误码的流重置帧(详见第19.4章)来仅仅终止受影响的流。
对流进行重置而不通知应用协议会使得应用协议进入不可恢复的状态。流重置帧必须只能由使用QUIC的应用协议发起。
在流重置帧中携带的应用错误码的语义是由应用协议定义的。只有应用协议有能力终止一条流。应用协议的本地实例直接调用API,远程实例则可以使用会自动触发流重置帧的停止发送帧。
应用协议应该定义规则来处理被任一终端提前取消的流。
12. 数据包与帧
12.1 受保护的数据包
根据数据包类型不同,QUIC数据包具有不同级别的加密保护。有关数据包保护的细节,详见《QUIC-TLS》;本节对这种保护做了概述。
版本协商数据包没有加密保护,详见《QUIC不变量》。
重试数据包使用AEAD函数(带有关联数据的认证加密,详见《AEAD》)来保护数据包免于意外修改。
初始数据包也使用AEAD函数,函数的密钥是用一个在传输中对外界可见的值衍生出来的。因此初始数据包不具有有效的可信度保护。使用初始数据包保护是为了确保数据包发送方是存在于当前网络路径上的。任何接收到来自客户端的初始数据包的实体都能重建出一个密钥,这个密钥既能让它们读取数据包的内容,也可以让它们创建出能被任一终端认证成功的初始数据包。AEAD函数还保护初始数据包免于意外修改。
其他所有数据包都受衍生自加密握手的密钥保护。加密握手确保了只有正在交流的终端才能够接收到用于握手数据包、0-RTT数据包和1-RTT数据包的相应密钥。受0-RTT和1-RTT密钥保护的数据包具有强力的可信度和完整性保护。
某些数据包类型中出现的数据包号字段具有可替代的可信度保护,这种保护被用作头部保护的一部分,详见《QUIC-TLS》的第5.4章。在给定的数据包号空间中,受保护的数据包号会随着数据包的发送而增加,详见第12.3章。
12.2 合并数据包
初始数据包(详见第17.2.2章)、0-RTT数据包(第17.2.3章)和握手数据包(第17.2.4章)包含可以用来判断数据包末尾位置的长度字段。这个长度既包括数据包号字段也包括载荷字段,这两个字段都被可信地保护着的,一开始它们的长度都是未知的。但只要头部保护被移除,就能知道载荷字段的长度。
使用长度字段,发送方可以将数个QUIC数据包合并至单个UDP数据报中。这可以减少为了完成加密握手并开始发送数据所需的UDP数据报数量。这也可以被用于构造路径最大传输单元(PMTU)探测包,详见第14.4.1章。接收方必须有能力处理被合并的数据包。
用加密级别递增(初始、0-RTT、握手、1-RTT;详见《QUIC-TLS》的第4.1.4章)的顺序合并数据包更有可能使接收方有能力一次性处理所有数据包。具有短包头的数据包并不包含长度,所以它只能作为最后一个数据包被包含进UDP数据报中。如果多个帧将以相同的加密级别发送,那么终端应该将它们包含在单个数据包中,而不是合并多个相同加密级别的数据包。
接收方可以根据UDP数据报包含的第一个数据包的信息来做路由。发送方必须不将具有不同连接ID的QUIC数据包合并至单个UDP数据报中。如果后续的某个数据包的目标连接ID字段与数据报中第一个数据包的值不同,那么接收方应该忽略这个数据包。
任何被合并至单个UDP数据报的QUIC数据包都是独立且完整的。被合并的QUIC数据包的接收方必须独立处理每个QUIC数据包并单独确认它们,就好像它们是作为不同UDP数据报的载荷被接收的。例如,如果对一个数据包的解密失败了(因为密钥出于某个理由而不可用),那么接收方可以要么丢弃这个数据包要么缓存它以供将来处理,并且必须尝试处理余下的数据包。
重试数据包(详见第17.2.5章)、版本协商数据包(第17.2.1章)和具有短包头的数据包(第17.3章)并不包含长度字段,因此在同一个UDP数据报中不能有其他数据包跟在它们后面。还要注意,不存在将重试数据包和版本协商数据包与另一个数据包合并的情况。
12.3 数据包号
数据包号是范围在0
至262-1
中的整数。这个数值被用来为数据包保护决定加密随机值。每个终端为发送和接收分别维护单独的数据包号。
数据包号被限制在这个范围内是因为它们需要在ACK帧(详见第19.3章)的最大确认数字段中被完整表示。然而在长包头或短包头中表示时,数据包号被截断并被编码至1至4字节中,详见第17.1章。
版本协商数据包(详见第17.2.1章)和重试数据包(第17.2.5章)并不包含数据包号。
在QUIC中,数据包号被划分到三个空间里:
- 初始空间(Initial space):
-
所有初始数据包(详见第17.2.2章)都在这个空间中。
- 握手空间(Handshake space):
-
所有握手数据包(详见第17.2.4章)都在这个空间中。
- 应用数据空间(Application data space):
如《QUIC-TLS》所述,每种数据包类型使用不同的保护密钥。
概念上,数据包号空间是数据包被处理和确认的上下文。初始数据包只能使用初始数据包保护密钥发送,也只能在初始数据包中被确认。类似地,握手数据包使用握手加密级别发送,且只能在握手数据包中被确认。
这样,在不同数据包号空间中发送的数据的加密得到了强制隔离。每个空间中的数据包号都从0
开始。在相同数据包号空间中发送的后续数据包必须将数据包号增加至少1
。
0-RTT数据和1-RTT数据出现在相同数据包号空间,是为了使得丢包检测算法在这两种数据包类型间更好实现。
在一条连接的同一个数据包号空间中,QUIC终端必须不重用数据包号。如果发送用的数据包号到达了262-1
,发送方必须关闭连接但不发送任何连接关闭帧或更多数据包;终端可以发送一个无状态重置(详见第10.3章)以响应将来它接收到的数据包。
除非接收方确信它没有处理过另一个具有相同数据包号空间和数据包号的数据包,否则它必须丢弃眼前刚去除保护的数据包。出于在《QUIC-TLS》的第9.5章中所述的原因,必须在移除数据包保护后进行这个去除重复的步骤。
为了检测重复的目的而追踪一个个数据包的终端会面临状态数据不断扩容的风险。检测重复所需的数据可以通过维护一个最小的数据包号来限制,低于这个数据包号的数据包都会被立即丢弃。使用任何最小的数据包号时,都要考虑往返时间可能发生的大幅度变化,原因之一是对端可能用更大的往返时间来探测网络路径,详见第9章。
有关数据包号在发送方的编码和在接收方的解码,详见第17.1章。
12.4 帧和帧类型
如图11所示,移除数据包保护后的QUIC数据包,其载荷由一系列完整的帧组成。版本协商数据包、无状态重置数据包和重试数据包中不包含帧。
数据包载荷 {
帧 (8..) ...,
}
包含帧的数据包载荷必须包含至少1个帧,可以包含多个帧和多种帧类型。终端必须将接收到不包含帧的数据包的情况视作类型为PROTOCOL_VIOLATION
(协议违背)的连接错误。帧总是能被放进单个QUIC数据包中且不能横跨多个数据包。
任何帧的开头都是表明其类型的帧类型字段,后面跟着额外的与类型相关的字段:
帧 {
帧类型 (i),
与类型相关的字段 (..),
}
表3罗列并概述了有关本规范中定义的各种帧类型的信息。表后是对于这份概述的描述。
类型值 | 帧类型名称 | 定义 | 数据包类型 | 特殊规则 |
---|---|---|---|---|
0x00 | 填充帧 | 第19.1章 | IH01 | NP |
0x01 | Ping帧 | 第19.2章 | IH01 | |
0x02-0x03 | ACK帧 | 第19.3章 | IH_1 | NC |
0x04 | 流重置帧 | 第19.4章 | __01 | |
0x05 | 停止发送帧 | 第19.5章 | __01 | |
0x06 | 加密帧 | 第19.6章 | IH_1 | |
0x07 | 新令牌帧 | 第19.7章 | ___1 | |
0x08-0x0f | 流帧 | 第19.8章 | __01 | F |
0x10 | 最大数据量帧 | 第19.9章 | __01 | |
0x11 | 最大流数据量帧 | 第19.10章 | __01 | |
0x12-0x13 | 最大流帧 | 第19.11章 | __01 | |
0x14 | 数据阻塞帧 | 第19.12章 | __01 | |
0x15 | 流数据阻塞帧 | 第19.13章 | __01 | |
0x16-0x17 | 流阻塞帧 | 第19.14章 | __01 | |
0x18 | 新连接ID帧 | 第19.15章 | __01 | P |
0x19 | 撤销连接ID帧 | 第19.16章 | __01 | |
0x1a | 通道挑战帧 | 第19.17章 | __01 | P |
0x1b | 回复通道帧 | 第19.18章 | ___1 | P |
0x1c-0x1d | 连接关闭帧 | 第19.19章 | ih01 | N |
0x1e | 握手完成帧 | 第19.20章 | ___1 |
在第19章中对各种帧类型的格式和语义进行了更详细的解释。本节的剩余部分对重要的和通用的信息做了概述。
ACK帧、流帧、最大流帧、流阻塞帧和连接关闭帧中的帧类型字段额外携带着与帧相关的标志量。对其他帧来说,帧类型字段仅仅被用来区分帧类型。
表3中的”数据包类型“一栏用以下字符表示每种帧类型可能出现在哪些数据包类型中:
- I:
-
初始数据包(详见第17.2.2章)。
- H:
-
握手数据包(详见第17.2.4章)。
- 0:
-
0-RTT数据包(详见第17.2.3章)。
- 1:
-
1-RTT数据包(详见第17.3.1章)。
- ih:
-
只有类型为
0x1c
的连接关闭帧能够出现在初始数据包或握手数据包中。
有关以上限制的细节,详见第12.5章。注意所有类型的帧都可以出现在1-RTT数据包中。终端必须将在某个类型的数据包中接收到该类型并不允许的帧的情况视作类型为PROTOCOL_VIOLATION
的连接错误。
表3的”特殊规则“一栏用以下字符表示了处理和创建各种类型的帧时遵循的特殊规则:
- N:
-
如果数据包中包含的帧全都具有这个标记,那么这不是一个触发ACK的数据包,详见第13.2章。
- C:
-
如果数据包中包含的帧全都具有这个标记,那么这个数据包不会被计入出于拥塞控制目的而计算的正在传输的字节数中,详见《QUIC恢复》。
- P:
-
如果数据包中包含的帧全都具有这个标记,那么这个数据包可以在连接迁移期间被用于探测新的网络路径,详见第9.1章。
- F:
-
具有这种标记的帧的内容会受到流量控制,详见第4章。
表3的”数据包类型“和”特殊规则“并不出现在IANA注册表的QUIC帧类型中,详见第22.4章。
终端必须将接收到未知类型的帧的情况视作类型为FRAME_ENCODING_ERROR
(帧编码错误)的连接错误。
本QUIC版本中所有类型的帧都是幂等的。也就是说,一个合法的帧即使被接收到多次,也不会引起意外的副作用或错误。
帧类型字段使用可变长度整型编码(详见第16章),但有一个例外。为了确保帧解析的实现简单且高效,帧类型必须使用最短的编码长度。对于这份文档中定义的帧类型来说,这意味着哪怕将这些值编码到双字节、四字节甚至八字节可变长度整型都是可行的,也要使用单字节编码。例如,尽管0x4001
是值1
在双字节下合法的可变长度整形编码,Ping帧的帧类型也总是被编码到值为0x01
的单个字节。这条规则适用于所有目前的和将来的QUIC帧类型。如果终端收到的帧类型使用了比最低限度所需要长的编码,那么终端可以将此情况视作类型为PROTOCOL_VIOLATION
的连接错误。
12.5 帧和数据包号空间
有一些类型的帧在某些数据包号空间中是被禁止的。这里的规则概述了TLS的规则,即与建立连接有关的帧通常能出现在任何数据包号空间的数据包中,而与传输数据有关的帧只能出现在应用数据空间中。
-
填充帧、Ping帧和加密帧可以出现在任何数据包号空间中。
-
标志着QUIC层错误(类型为
0x1c
)的连接关闭帧可以出现在任何数据包号空间中。标志着应用错误(类型为0x1d
)的连接关闭帧必须只能出现在应用数据空间中。 -
ACK帧可以出现在任何数据包号空间中,但是只能确认在同一个数据包号空间中的数据包。然而,如下文所述,0-RTT数据包不能包含ACK帧。
-
所有其他类型的帧必须只能出现在应用数据空间中。
注意,不管出于什么理由,都不能在0-RTT数据包中发送以下类型的帧:ACK帧、加密帧、握手完成帧、新令牌帧、回复通道帧和撤销连接ID帧。服务器可以将在0-RTT数据包中接收到以上帧的情况视作类型为PROTOCOL_VIOLATION
的连接错误。
13. 分包与可靠性
发送方可以在一个QUIC数据包中发送一个或多个帧,详见第12.4章。
发送方可以通过在每个QUIC数据包中包含尽可能多的帧来最小化每个数据包消耗的带宽和计算资源。在发送一个被未完全利用的数据包前,发送方可以短暂等待一会,多收集几个帧,以避免发送出大量的小型数据包。QUIC实现可以使用有关应用发送习惯的知识或启发式的方法来决定要不要等和等多久。这一等待时间的长短是一个由QUIC实现做的决策,同时实现应该小心地保守地进行等待,因为任何等待都有可能增加应用所察觉到的延迟量。
流的多路复用是通过将来自多个流的流帧交错至一个或多个QUIC数据包中来实现的。单个QUIC数据包可以包含来自一个或多个流的数个流帧。
使用QUIC的益处之一是能避免复数流间的队头阻塞问题。当出现丢包时,只有那个数据包中的数据所属的流会被阻塞住并等待收到重传的数据,而其他流能够继续传输。注意,当单个QUIC数据包中包含来自多个流的数据时,丢失那个数据包将阻塞所有有关的流的传输。建议QUIC实现在发出去的数据包中包含尽可能少的流,来避免降低这些未完全利用的数据包的传输效率。
13.1 数据包处理
在数据包保护被成功移除且所有包含在数据包中的帧都被处理之前,必须不确认这个数据包。对于流帧,这个时机指其中的数据已经被置入队列,且准备被应用协议接收之时,但它不要求数据被交付和消费。
一旦数据包已经被完全处理,接收方通过发送一个或多个ACK帧来确认这个数据包的接收,这些ACK帧中包含被接收到的这个数据包的数据包号。
如果终端有能力检测出收到了一个它未发送过的数据包的确认的情况,它应该将这种情况视作一种类型为PROTOCOL_VIOLATION
(协议违背)的错误。有关如何做到这件事,详见第21.4章。
13.2 生成确认
终端对所有它们接收到和处理过的数据包进行确认。然而,只有ACK触发包要求在最大的确认延迟之内发送一个ACK帧。对于不触发ACK的数据包,只有出于其他原因发送ACK帧时,才会确认它们。
当出于任何原因发送数据包时,如果最近没有发送过ACK帧,那么终端应该尝试包含一个这种帧。这么做有助于对端进行及时的丢包检测。
通常来说,如果一个接收方对每一个ACK触发包都响应一个ACK帧,那么这些来自接收方的频繁反馈可以改善丢包和拥塞响应,但这必须与它产生的额外负载相平衡。下文提供的指导旨在取得这种平衡。
13.2.1 发送ACK帧
所有数据包都应该被确认至少一次,ACK触发包必须在终端使用传输参数max_ack_delay
(最大ACK延迟)沟通的最大延迟时间内被确认至少一次,详见第18.2章。max_ack_delay
声明了一个显式的约定:终端承诺不会故意拖延ACK触发包的确认以致延迟时间超过那个约定值。如果它这么做了,那么超出量会积累到RTT预估值上,并且导致来自对端的无效的或延迟的重传。发送方使用接收方的max_ack_delay
值来决定基于计时器的重传的超时时间,详见《QUIC恢复》的第6.2章。
除了以下例外,终端必须立即确认所有引发确认的初始数据包和握手数据包,并且在它宣告的max_ack_delay
之内确认所有引发确认的0-RTT数据包和1-RTT数据包。终端如果在握手确认前就收到了握手数据包、0-RTT数据包和1-RTT数据包,那么它可能还没有用来解密这些数据包的数据包保护密钥。因此它可以缓存这些数据包,等得到了必要的密钥再确认它们。
由于仅包含ACK帧的数据包不受拥塞控制,终端在响应一个ACK触发包时,必须不发送超过一个这样的数据包。
终端在响应非ACK触发包时,必须不发送非ACK触发包,即便在接收到的数据包前存在数据包空档。这避免了用确认响应确认的无限循环,避免连接无法进入空闲状态。非ACK触发包最终会在终端响应其他事件而发送的ACK帧中被确认。
一味发送ACK帧的终端不会从对端收到确认,除非这些帧和ACK触发帧一起被包含在数据包中。当有新的ACK触发包要确认时,终端应该将这些ACK帧一块发送。当只有非ACK触发包要确认时,终端可以选择等接收到一个ACK触发包时再将这些ACK帧一块发送。
只发送非ACK触发包的终端可以选择偶尔添加一个ACK触发帧到这些数据包中以确保它能收到确认,详见第13.2.4章。在那种情况下,终端必须不在所有本来不引发确认的数据包中都添加ACK触发帧,以避免用确认响应确认的无限循环。
为了辅助发送方的丢包检测,在以下情况中若收到一个ACK触发包,终端应该立即生成并发送ACK帧:
-
当接收到的数据包的数据包号小于另一个已经被接收到的ACK触发包的时;
-
当接收到的数据包的数据包号大于已接收到的ACK触发包的数据包号最大值,且在它们之间有缺失的数据包时。
类似地,标记了IP头部的ECN拥塞预警(CE)码点的数据包应该被立即确认,以减少对端对拥塞事件的响应时间。
对没有遵守以上指导的接收方,《QUIC恢复》中的算法应当是能适应的。即使如此,QUIC实现也应该在谨慎地考虑任何变更会对性能的影响之后,再违背这些要求,既要考虑对终端建立的连接的性能影响,也要考虑对网络中其他用户的性能影响。
13.2.2 确认频率
作为ACK触发包的响应而发送的确认,其频率是由接收方决定的。这个决策包含了一种权衡。
终端依靠及时的确认以检测丢包,详见《QUIC恢复》的第6章。基于窗口的拥塞控制器,例如《QUIC恢复》的第7章中描述的那个,依靠确认来管理它们的拥塞窗口。在这两种情况中,延迟确认都会对性能产生不利影响。
另一方面,降低仅携带确认的数据包的频率能同时减少两个终端在传输和处理上的资源消耗。它能提高在极端不对称的链路上的连接吞吐量,并使用返回路径的容量来减少因确认而产生的流量,详见《RFC3449》的第3章。
接收方在接收到至少两个ACK触发包后应该发送一个ACK帧。这一推荐做法本质上是通用的,并且和TCP终端行为(《RFC5681》)的推荐做法是一致的。有关网络条件的知识、有关对端拥塞控制器的知识或将来的研究和实验可能建议替代本做法的有着更好的性能特征的确认策略。
接收方可以在决定是否要发送ACK帧作为响应前先处理掉几个可用的数据包。
13.2.3 管理ACK块
被发送的ACK帧中包含一个或多个已确认数据包的块。包含对更早期的数据包的确认减少了因之前发送的ACK帧丢包而引起无效重传的可能性,代价是ACK帧会变得更大。
ACK帧应该始终确认最近接收到的数据包,同时接收到的数据包越是乱序,尽快发送更新后的ACK帧就越是重要,以避免对端将某个数据包认定为丢包而对其中包含的帧进行无效的重传。一个ACK帧应当能被放进单个QUIC数据包中。如果不能,那么更早期的块(有着最小的数据包号的那些)会被省略。
接收方限制它记录的ACK块(详见第19.3.1章)的数量和在ACK帧中发送的ACK块的数量,这既是为了限制ACK帧的尺寸也是为了避免耗尽资源。在接收到对一个ACK帧的确认后,接收方应该停止追踪那些在帧中被确认的ACK块。发送方可以期待绝大多数数据包都被确认,但是QUIC不保证接收方处理的所有数据包的确认都被对端接收到。
因保留了许多ACK块而使得ACK帧变得过大是有可能的。接收方可以丢弃未被确认的ACK块以限制ACK帧的尺寸,代价是来自发送方的更多重传。如果一个ACK帧大到放不进一个数据包中,那么就有必要这么做。接收方还可以进一步限制ACK帧的尺寸来为其他帧腾出空间,或为了限制确认所消耗的数据包容量。
除非接收方能确信它后续接收到的数据包号不会落在某个ACK块中,否则它必须保留那个块。使用最少的状态来做到这一点的方法之一是维护一个最小的数据包号,它会随着块被丢弃而增大。
接收方可以丢弃所有ACK块,但是必须保留最大的已被成功处理的数据包号,因为它被用于从后续数据包中恢复数据包号,详见第17.1章。
接收方应该在所有ACK帧中包含最大的已接收到的数据包号。这个最大确认数字段被用于在发送方一侧进行的ECN验证,使用一个比先前发送的ACK帧中的更小的值会造成ECN被不必要地禁用,详见第13.4.2章。
第13.2.4章描述了一个如何决定在各个ACK帧中分别确认哪些数据包的示例方法。尽管这个算法的目标是为每个已处理的数据包生成一个确认,但是确认依旧有可能遭遇丢包。
13.2.4 通过追踪ACK帧来限制块
当发送包含某个ACK帧的数据包时,可以记录那个帧中的最大确认数。当接收到包含某个ACK帧的数据包的确认时,如果某个数据包的数据包号小于等于为那个ACK帧记录的最大确认数,那么接收方就可以不再确认这个数据包。
只发送非ACK触发包,例如ACK帧,的接收方可能很长一段时间都接收不到确认。这会导致接收方需要为大量ACK帧的状态维护很长一段时间,同时它发送的ACK帧会不必要地变得特别大。在这种情况下,接收方可以偶尔,比如每经过一轮往返时间就进行一次,发送一个Ping帧或其他小型的ACK触发帧,来引发来自对端的ACK帧。
在ACK帧没有遇到丢包的情况下,这个算法在每至少1RTT内提供一次重排ACK块的机会。在ACK帧遇到丢包和乱序的情况下,这个方法并不能保证所有确认都在它们不再被包含进ACK帧前就被发送方接收到。数据包还有可能在接收时已经被打乱顺序,或者所有后续包含它们的数据包号的ACK帧都遇到丢包。在这种情况下,丢包恢复算法可能引起无效的重传,但是发送方会继续进行有效的传输。
13.2.5 测量和报告主机延迟
从具有最大数据包号的数据包被接收到的那一刻起,到对于它的确认被发送时,这期间的时间会作为一个被有意引入的延迟而被终端测量。终端将这个确认延迟编码在ACK帧的ACK延迟字段,详见第19.3章。这使得这个ACK帧的接收方能够获知这些有意引入的延迟并作出调整,当确认出现延误时这一调整对更好地评估路径RTT是很重要的。
在得到处理前,数据包可能被暂留在操作系统内核或主机的其他什么地方。在填写ACK帧的ACK延迟字段时,终端必须不在值中包括不受它控制的延迟。然而,终端应该在值中包含由缺少解密密钥造成的缓存延迟,因为这些延迟可以变得很大,而且有可能是非重复出现的。
当测量到的确认延迟比max_ack_delay
大时,终端应该报告这个测量到的延迟。这个信息在握手期间延迟可能会很大的情况下是特别有用的,详见第13.2.1章。
13.2.6 ACK帧和数据包保护
ACK帧必须被携带于一个和正在确认的数据包的数据包号空间相同的数据包中,详见第12.1章。举个例子,受1-RTT密钥保护的数据包必须在同样被1-RTT密钥保护的数据包中被确认。
客户端发送的受0-RTT数据包保护的数据包必须被服务器在受1-RTT密钥保护的数据包中确认。这意味着如果服务器的加密握手消息被延误或丢失,则客户端有可能无法使用这些确认。注意,同样的限制还适用于服务器发送的受1-RTT密钥保护的其他数据。
13.2.7 填充帧消耗拥塞窗口
包含填充帧的数据包会被纳入拥塞控制的考量,详见《QUIC恢复》。而仅仅包含填充帧的数据包虽然会消耗拥塞窗口但是不会引发确认来恢复拥塞窗口。为了避免死锁,发送方应该确保每隔一段时间会发送一次除填充帧外的其他帧,来引发来自接收方的确认。
13.3 信息重传
被认定丢包的QUIC数据包不会被整个重传。被丢包的数据包中包含的帧也是这样。取而代之的是这些帧中可能携带的信息会按照需要在新的帧中被再次发送。
携带被认定为丢失的信息时,使用新的帧和数据包。一般来说,信息会在携带它的数据包被认定为丢包时被再次发送,同时当携带那个信息的数据包被确认时发送即停止。
-
在加密帧中的数据按照《QUIC恢复》中的规则重新传输,直到所有数据都已被确认。当有关数据包号空间的密钥被弃用时,初始数据包和握手数据包的加密帧中的数据会被丢弃。
-
原本在流帧中发送的应用数据会在新的流帧被重传,除非终端已经为那条流发送了流重置帧。一旦终端发送了流重置帧,就不再需要发送流帧。
-
ACK帧携带最近的一系列确认和来自最大已确认数据包的确认延迟,如第13.2.1章所述。延误包含ACK帧的数据包的传输或重发旧的ACK帧会使得对端生成过高的RTT样本或不必要地禁用ECN。
-
流传输的中止信息只有在携带它的流重置帧被确认前且在所有流数据都被对端确认(也就是流的发送方进入“重置接收”或“接收完成”状态)前才能被发送。当再次发送时必须不改变流重置帧的内容。
-
类似地,被编码在停止发送帧帧中的取消流传输的请求,只有在流的接收方进入“接收完成”或“重置接收”状态前才能被发送,详见第3.5章。
-
当数据包丢包被检测到时,连接关闭的信号,包括那些包含连接关闭帧的数据包,不会被再次发送。有关重发这些信号,详见第10章。
-
当前连接的最大数据量是使用最大数据量帧发送的。如果包含最近发送的最大数据量帧的数据包被认定丢包或终端决定更新限制,那么更新后的值还是使用最大数据量帧发送。由于限制可以被频繁提高而造成大量不必要的最大数据量帧被发送出去,必须小心以避免过于频繁地发送这种帧,详见第4.2章。
-
当前最大流数据偏移量是使用最大流数据量帧发送的。像最大数据量帧一样,如果包含最近的最大流数据量帧的数据包遭遇丢包或限制被更新,那么就要发送更新后的值,并且小心以避免过于频繁地发送此类帧。当流的接收方进入“数据量确认”或“重置接收”状态时,终端应该不再发送最大流数据量帧。
-
对给定类型的流的限制是使用最大流帧发送的。像最大数据量帧一样,如果对于给定的流类型,包含其最近的最大流帧的数据包被认定丢包或限制被更新,那么就要发送更新后的值,并且小心以避免过于频繁地发送此类帧。
-
阻塞信号是在流阻塞帧、流数据阻塞帧和流阻塞帧中被携带的。流阻塞帧是连接层面上的,流数据阻塞帧是流层面上的,而流阻塞帧是在某种特定的流层面上的。当包含某一层面上的最近的上述帧的数据包遭遇丢包且仅当终端被阻塞于相应限制时,才发送一个新的帧。这些帧总是包含那个引起阻塞的限制在这些帧被传输时的值。
-
使用通道挑战帧的存活确认或路径验证检查每隔一段时间被发送一次,直到接收到与之匹配的回复通道帧或已经没有必要再做存活确认或路径验证检查。每次发送通道挑战帧时都使用不同的载荷。
-
响应路径验证时使用回复通道帧且仅发送一次。对端应当根据需要发送更多通道挑战帧来唤起额外的回复通道帧。
-
新的连接ID是使用新连接ID帧发送的,如果包含它们的数据包遭遇丢包,依旧用这种帧重传。这种帧重传时携带相同序列数值。类似地,撤销连接ID的信息是在撤销连接ID帧中被发送的,如果包含它们的数据包遭遇丢包,依旧用这种帧重传。
-
新令牌帧在包含它们的数据包遭遇丢包时会被重传。除了直接比较帧的内容之外,没有提供特殊的方法来检测乱序和重复的新令牌帧。
-
Ping帧和填充帧不包含信息,因此丢失的这两种帧不需要修复。
-
握手完成帧必须被重传,除非它已经被确认了。
终端应该使数据重传优先于新数据发送,除非应用指定的优先级表明了相反的策略,详见第2.3章。
尽管鼓励发送方每次发送数据包时都使用最新的信息来装配帧,但是重传从丢包的数据包中拷贝来的帧数据这一行为并未被禁止。重传帧拷贝的发送方需要解决可用载荷大小上的降低,这是由数据包号长度、连接ID长度和路径MTU上的变化引起的。如果一个数据包包含了过时的帧,比如一个携带着比先前数据包中的值更小的最大数据量的最大数据量帧,那么接收方必须接收它们。
只要数据包已经被确认,发送方就应该避免重传来自这些数据包的信息。这包含数据包在被认定为丢包后又被确认的情况,这种情况会因为网络重排序而发生。这么做需要发送方在数据包被认定为丢包后保留有关它们的信息。发送方可以在丢弃此信息前保留一段足以允许重排序的时间,例如一个PTO(详见《QUIC恢复》的第6.2章),或基于其他事件做决定,例如到达内存限制。
在丢包检测时,发送方必须采取合适的拥塞控制措施。有关丢包检测和拥塞控制的细节,详见《QUIC恢复》。
13.4 显式拥塞通知
13.4.1 报告ECN计数
要使用ECN,要求接收数据的终端从IP数据包读取ECN字段,但这不是在所有平台上都能做得到的。如果一个终端没有实现对ECN的支持或访问不了接收到的ECN字段,它就不会为它接收到的数据包报告ECN计数。
即使终端不会在它发送的数据包中设置ECN字段,它也必须为它接收到ECN标记提供反馈,除非访问不了。不报告ECN计数会使得发送方为这条连接禁用ECN。
当接收到一个具有ECT(0)
、ECT(1)
或ECN-CE
码点的IP数据包,一个启用ECN的终端访问ECN字段并增加相应的ECT(0)
、ECT(1)
或ECN-CE
计数。这些ECN计数会被包含在后续ACK帧中,详见第13.2章和第19.3章。
每个数据包号空间维护单独的确认状态和单独的ECN计数。被合并的QUIC数据包(详见第12.2章)共享相同的IP头部,所以每个被合并的QUIC数据包对应的ECN计数都要增加。
举例来说,若将QUIC的初始数据包、握手数据包和1-RTT数据包各一个合并至单个UDP数据报中,那么这三个数据包号空间的ECN计数都要分别基于同一个的IP头部的ECN字段来增加。
ECN计数仅当来自接收到的IP数据包的QUIC数据包被处理时才增加。于是,重复的QUIC数据包不会被处理所以就不会增加ECN计数;有关安全考量,详见第21.10章。
13.4.2 ECN验证
故障的网络设备可能损坏或错误地丢弃携带非零ECN码点的数据包。为了在有这类设备存在的前提下确保连接,终端为每条网络路径验证ECN计数,并当检测到错误时在那条路径上禁用ECN。
要为一条新路径进行ECN验证:
-
对于在一条新路径上发向对端的出站数据包,终端在早期的几个数据包的IP头部设置一个
ECT(0)
码点。
如果终端怀疑一个故障的网络设备正在丢弃具有ECT
码点的IP数据包,那么它可以为路径上仅前十个出站数据包设置ECT
码点,或以每三个PTO(详见《QUIC恢复》的第6.2章)一次的间隔设置。如果所有标记为非零ECN码点的数据包都接连遭遇丢包,那么它就可以基于标记行为会引发丢包的假设来禁用标记。
就这样,终端尝试为每一条新连接使用ECN并做验证,这包括切换到服务器的首选地址时和从活跃连接迁移到新路径时。附录A.4描述了一种可行的算法。
可能存在其他探测路径是否支持ECN的方法以及不同的标记策略。QUIC实现可以使用定义在RFC中的其他方法,详见《RFC8311》。使用了ECT(1)
码点的实现需要用报告的ECT(1)
计数进行ECN验证。
13.4.2.1 接收具有ECN计数的ACK帧
被网络错误地使用的ECN-CE
标记会造成连接性能低下。因此接收到具有ECN计数的ACK帧的终端在使用前要验证它们。终端用比较新接收到的计数和那些在最近成功处理了的ACK帧中的计数的方法进行这项验证。任何在ECN计数上的增加都会被验证,这项验证基于被应用到最近在ACK帧中被确认的原始数据包上的ECN标记。
如果一个ACK帧最近确认了一个由终端发送的数据包且这个数据包被设置了ECT(0)
或ECT(1)
码点,同时相应的ECN计数并未在这个ACK帧中出现,那么ECN验证就会失败。这项检查能够检测出将ECN字段归零的网络元素或并不报告ECN标记的对端。
如果ECT(0)
计数和ECN-CE
计数的增加量的总和小于被新确认的且发送时具有ECT(0)
标记的数据包的数量,那么ECN验证也会失败。类似地,如果ECT(1)
计数和ECN-CE
计数的增加量的总和小于被新确认的且发送时具有ECT(1)
标记的数据包的数量,那么ECN验证也会失败。这些检查可以检测网络重设ECN-CE
标记的行为。
在ACK帧有可能丢失的情况下,终端允许忽略对某个数据包的确认。因此ECT(0)
、ECT(1)
和ECN-CE
的增加量的总和大于被一个ACK帧新确认的的数据包数量是有可能的。这就是为什么允许ECN计数大于被确认的数据包的总数。
为经过重排序的ACK帧验证ECN计数,其结果是不准确的。终端必须不因为处理一个没有增加最大已确认数据包号的ACK帧而将ECN验证结果记为失败。
如果接收到的ECT(0)
或ECT(1)
中任意一个的总计数,超过了已发送的具有的相应ECT
码点的数据包总数,ECN验证就会失败。特别是,当终端接收到一个非零的ECN计数但那个相应的ECT
码点从未使用过,验证就应失败。这项检查可以检测网络重设数据包的ECT(0)
和ECT(1)
的行为。
13.4.2.2 ECN验证结果
如果验证失败,那么终端必须禁用ECN。它在发送的IP数据包中不会再设置ECT
码点,就当作网络路径或对端不支持ECN。
即使验证失败,终端也可以在连接中的任意时间对相同的路径重新验证ECN。终端可以继续定期尝试验证。
当验证成功后,终端可以继续在它发送的后续数据包中设置ECT
码点,并认定这条路径是支持ECN的。网络路由和路径上的元素可能在连接中途发生变化;将来如果验证失败,则终端必须禁用ECN。
14. 数据报尺寸
一个UDP数据报可以包含一个或多个QUIC数据包。数据报尺寸指的是携带QUIC数据包的单个UDP数据报的UDP载荷总大小。数据报尺寸包含一个或多个QUIC数据包头部和受保护的载荷,但不包含UDP或IP头部。
最大数据报尺寸被定义为能够经由一条网络路径使用单个UDP数据报发送的最大的UDP载荷大小。如果某条网络路径支持的最大数据报尺寸不能达到1200字节,则必须不使用QUIC。
QUIC假设一个最小的IP数据包尺寸为至少1280字节。这是IPv6数据包的最小尺寸(见《IPv6》),同时被大多数现代IPv4网络所支持。假设使用最小的IP头部,IPv6时为40字节而IPv4时为20字节,以及大小为8字节的UDP头部,则可得到IPv6中的最大数据报尺寸为1232字节,而IPv4中的为1252字节。因此,现代的IPv4网络路径和所有的IPv6网络路径都应该有能力支持QUIC。
注意:如果某条路径仅支持1280字节,即IPv6的最小MTU,那么要求支持1200字节的UDP载荷会限制IPv6扩展头部和IPv4选项的可用空间,前者被限制至32字节,后者被限制至52字节。
任何超过1200字节的最大数据报尺寸都可以使用路径最大传输单元发现(PMTUD)(见第14.2.1章)或数据报分包层PMTU发现(DPLPMTUD)(见第14.3章)。
强制执行传输参数max_udp_payload_size
(第18.2章)可能成为一个额外的对最大数据报尺寸的限制。一旦这个值已知,发送方就可以避免超过这个限制。然而在获知这个传输参数的值之前,如果终端发送了比1200字节,这个在允许的最大数据报尺寸中的最小值,更大的数据报,那么终端就要承担数据报丢失的风险。
UDP数据报必须不在IP层被分段。在IPv4(《IPv4》)中必须尽可能设置不要分段(DF)比特位来防止路径上的分段。
QUIC有时需要数据报不小于一个特定尺寸,比如第8.1章中的例子。然而,数据报的尺寸不会被认证。也就是说,如果终端接收到了某个尺寸的数据报,它不能确定发送方发送的是不是就是那个尺寸的数据报。因此,当终端接收到一个没有满足尺寸限制的数据报时,它必须不关闭那条连接;终端可以丢弃这样的数据报。
14.1 初始数据报尺寸
客户端必须通过向初始数据包增加填充帧或合并初始数据包的方式,对所有携带初始数据包的UDP数据报载荷进行扩充,至少扩充至1200字节,这个在允许的最大数据报尺寸中的最小值,详见第12.2章。初始数据包甚至可以和不合法的数据包合并,后者会被接收方丢弃。相似的,服务器必须对所有携带引发确认的初始数据包的UDP数据报载荷进行扩充,至少扩充至1200字节,这个在允许的最大数据报尺寸中的最小值。
以上述尺寸发送UDP数据报确保网络路径在两个方向上都支持一个合理的路径最大传输单元(PMTU)值。除此之外,扩充初始数据包的客户端有助于减少因服务器响应未经验证的客户端地址而引起的放大攻击的幅度,详见第8章。
如果发送方确信网络路径和对端均支持它选择的数据报尺寸,那么包含初始数据包的数据报可以超过1200字节。
如果一个UDP数据报的载荷小于1200字节,这个在允许的最大数据报尺寸中的最小值,那么服务器必须丢弃这个数据报中的初始数据包。服务器还可以通过发送一个错误码为PROTOCOL_VIOLATION
(协议违背)的连接关闭帧来立即关闭连接,详见第10.2.3章。
服务器还必须限制它在验证客户端地址前发送的字节数,详见第8章。
14.2 路径最大传输单元
PMTU指整个IP数据包的最大尺寸,包括IP头部、UDP头部和UDP载荷。UDP载荷包含一个或多个QUIC数据包头部和受保护的载荷。PMTU可能受路径特征影响因而随时间变化。终端的最大数据报尺寸指的是在某个给定时间,终端能发送的最大UDP载荷大小。
终端应该使用DPLPMTUD(第14.3章)或PMTUD(第14.2.1章)来认定一条通向目的地的路径是否支持一个期望的最大数据报尺寸且无需分段。在缺失这些机制时,QUIC终端不应该发送比允许的最大数据报尺寸中的最小值还要大的数据报。
DPLPMTUD和PMTUD都会发送比当前的最大数据报尺寸更大的数据报,也就是PMTU探测包。所有不是在PMTU探测包中发送的QUIC数据包都应该具有适合最大数据报尺寸的尺寸以避免数据报被分段或丢弃(详见RFC8085)。
如果QUIC终端认定某对本地IP地址和远程IP地址间的PMTU达不到1200字节,这个在允许的最大数据报尺寸中的最小值,那么它必须在受影响的路径上立即停止发送QUIC数据包,除了那些在PMTU探测包中的或包含连接关闭帧的数据包。如果无法找到备选路径,那么终端可以终止连接。
每对本地地址和远程地址可以有不同的PMTU。因此实现了任何一种PMTU发现的QUIC实现应该为每一对本地IP地址和远程IP地址的组合维护一个最大数据报尺寸。
QUIC实现可以更保守地计算最大数据报尺寸以允许未知的隧道开销或IP头部选项/扩展。
14.2.1 使用PMTUD处理ICMP消息
PMTUD(详见《RFC1191》和《RFC8201》)依赖一种ICMP消息(也就是IPv6数据包过大(PTB)消息)的接收,这种消息表明了何时IP数据包由于超过本地路由器MTU而被丢弃。DPLPMTUD可以选择使用这些消息。使用这种ICMP消息潜在地容易受到路径上不能观察数据包但可能成功猜测到地址的实体的攻击。
如果一个ICMP消息宣称PMTU已降低,且降低至QUIC允许的最大数据报尺寸中的最小值以下,那么终端必须忽略它。
生成ICMP消息的要求(详见《RFC1812》和《RFC4443》)指出,被引用的数据包应该包含尽可能多的原始数据包且不超过当前IP版本的最小MTU。被引用的数据包的尺寸实际上可以更小,或信息可以更难以理解,如《DPLPMTUD》的第1.1章所述。
使用PMTUD的QUIC终端应该验证ICMP消息以免于如《RFC8201》和《RFC8085》的第5.2章所述的数据包注入。这种验证应该使用ICMP消息的载荷中提供的被引用数据包,来将这条消息与一条有关的传输连接相关联(详见《DPLPMTUD》的第4.6.1章)。ICMP消息验证必须在一个活跃的QUIC上下文中包含匹配的IP地址和UDP端口(详见《RFC8085》)以及,如果可以的话,连接ID。终端应该忽略所有未通过验证的ICMP消息。
终端必须不基于ICMP消息提高PMTU,详见《DPLPMTUD》的第3章中的第6款。在QUIC的丢包检测算法认定被引用的数据包确实被丢失了之前,任何作为ICMP消息的响应而降低的QUIC最大数据报尺寸都可以是临时的。
14.3 数据报分包层PMTU发现
DPLPMTUD(详见《DPLPMTUD》)依赖对PMTU探测包中携带的QUIC数据包的丢失或确认进行追踪。DPLPMTUD中使用了填充帧的PMTU探测包实现了如《DPLPMTUD》的第4.1章所述的“使用填充数据进行探测”。
终端应该将BASE_PLPMTU
(基本分包层路径最大传输单元)(详见《DPLPMTUD》的第5.1章)的初始值设置为与QUIC允许的最大数据报尺寸中的最小值一致的值。MIN_PLPMTU
(最小分包层路径最大传输单元)就是BASE_PLPMTU
。
实现DPLPMTUD的QUIC终端为每一对本地IP地址和远程IP地址的组合维护一个DPLPMTUD最大数据包尺寸(MPS)(详见《DPLPMTUD》的第4.4章)。它对应着最大数据报尺寸。
14.3.1 DPLPMTUD与初始连接
从DPLPMTUD的视角看,QUIC是个进行过确认的分包层(PL)。因此QUIC发送方当QUIC连接的握手阶段完成时可以进入DPLPMTUD的BASE
(基本)状态。
14.3.2 用DPLPMTUD验证网络路径
14.3.3 使用DPLPMTUD处理ICMP消息
14.4 发送QUIC的PMTU探测包
14.4.1 包含源连接ID的PMTU探测包
依赖目标连接ID字段来路由传入的QUIC数据包的终端,有可能需要连接ID被包含在PMTU探测包中,才能将产生的ICMP消息(详见第14.2.1章)路由回正确的终端。
构建PMTU探测包的方法之一是将一个有长包头的数据包,例如握手数据包或0-RTT数据包(详见第17.2章),与一个短包头数据包合并(详见第12.2章)至单个UDP数据报中。如果产生的PMTU探测包到达了对端,那么具有长包头的数据包会被忽略,但是短包头数据包会被确认。如果PMTU探测包触发了一条ICMP消息,那么探测包的起始部分会被引用进那条消息。如果源连接ID字段在探测包被引用的部分里,它就能被用来路由或验证那条ICMP消息。
注意:使用具有长包头的数据包的目的仅仅是确保ICMP消息中被引用的数据包里包含源连接ID字段。这个数据包并不需要是一个合法的数据包,并且即使当时那种数据包没有用处也可以发送它。
15. 版本
QUIC版本使用一个32位无符号整型值来标识。
版本0x00000000
被保留使用,以表示版本协商。本规范的版本使用数值0x00000001
来标识。
其他QUIC版本的各种属性可能与本版本不同。《QUIC不变量》中描述了保证在协议不同版本间保持一致的一些QUIC属性。
如《QUIC-TLS》所述,版本为0x00000001
的QUIC使用TLS作为加密握手协议。
版本号最高的16个有效位被清除的版本被保留使用,用于将来的IETF共识文档。
遵循0x?a?a?a?a
模式的版本——也就是所有字节的四个低位都是1010
(二进制)的那些版本号——被保留使用,用于强制执行版本协商。客户端或服务器可以宣称自己支持这些保留版本中的任意版本。
保留版本号绝不会表示一个真实的协议;客户端可以使用这些版本号中的一个并期望服务器会发起版本协商;服务器可以宣称自己支持这些版本中的一个,并期望客户端忽略这个值。
16. 可变长度整型编码
QUIC数据包和帧经常对非负整型值使用一种可变长度编码。这种编码确保较小的整型值能被编码到更少的字节中。
QUIC可变长度整型编码占用了首个字节最高的两个有效位,用它们来存储正在编码的整型值的字节长度的以2
为底的对数值。整型值以网络字节序被编码进剩余比特位中。
这意味着被编码进1,2,4或8字节的整型值可以分别编码6,14,30和62位值。表4总结了这种编码的属性。
最高的2个有效位 | 字节长度 | 可用位数 | 可表示范围 |
---|---|---|---|
00 | 1 | 6 | 0-63 |
01 | 2 | 14 | 0-16383 |
10 | 4 | 30 | 0-1073741823 |
11 | 8 | 62 | 0-4611686018427387903 |
在附录A.1中可以找到一种解码算法的例子和一些样例编码。
值并不一定要被编码进正正好好最少的所需字节数,帧类型字段是主要的例外,详见第12.4章。
版本(第15章),头部中发送的数据包号(第17.1章)和长包头中的连接ID长度(第17.2章)使用整型值来描述但不使用本编码。
17. 数据包格式
所有数字值均以网络字节序(也就是大端)编码,并且所有的字段长度都以比特为单位。十六进制标记被用于描述字段的值。
17.1 数据包号编码与解码
数据包号是范围在0
至262-1
中的整数(第12.3章)。当在长包或短包的头部出现时 ,它们被编码在1至4字节中。通过仅使用数据包号的几个最低有效位,用于表示数据包号的比特位数量得以被减少。
经过编码的数据包号是受保护的,如《QUIC-TLS》第5.4章所述。
在接收到对于某数据包号空间的确认前,必须使用完整的数据包号;它不可以如下文所述那样被截断。
在接收到对于某数据包号空间的确认后,发送方必须使用一个足够大的数据包号尺寸,这个尺寸能够表示的范围大小至少是最大已确认数据包号和正在发送的数据包号之差的两倍。接收到该数据包的对端将正确解码数据包号,除非此包在传输过程中被延误得比很多更大编号的数据包还晚地被接收到。终端应该使用一个足够大的数据包号编码,使得即便此数据包比后续发送的数据包更晚到达,其数据包号也能被恢复。
于是,某数据包号编码的尺寸至少要比连续未被确认的数据包号的数量(包括此数据包本身)的以2
为底的对数值大1
比特。在附录A.2中可以找到数据包号编码的伪代码和一个样例。
在接收方一侧,数据包号的保护在恢复完整数据包号前被移除。完整的数据包号随后被重建,重建基于数据包中出现的包号有效比特位的数量和值,以及已接收的且成功认证的数据包中最大的数据包号。恢复完整的数据包号对于成功移除数据包保护很有必要。
一旦头部保护被移除,通过找到与下一个预期数据包更接近的数据包号值即可将数据包号解码。下一个预期数据包就是将最大的已接收数据包编号加上1
。在附录A.3中可以找到数据包号解码的伪代码和一个样例。
17.2 长包头数据包
长包头数据包 {
包头形式 (1) = 1,
固定比特位 (1) = 1,
长数据包类型 (2),
类型特定比特位 (4),
版本 (32),
目标连接ID长度 (8),
目标连接ID (0..160),
源连接ID长度 (8),
源连接ID (0..160),
类型特定载荷 (..),
}
长包头被用于在1-RTT密钥建立前发送的数据包。一旦有了1-RTT密钥,发送方就会改用短包头发送数据包(第17.3章)。长类型包头允许特殊数据包——例如版本协商数据包——以这种统一的固定长度的数据包格式呈现。使用长包头的数据包包含以下字段:
- 包头形式(Header Form):
-
对于长包头,字节0(第一个字节)的最高有效位(
0x80
)被设置为1
。 - 固定比特位(Fixed Bit):
-
字节0中的下一个比特位(
0x40
)被设置为1
,除非该包是一个版本协商数据包。此比特位为0
的数据包表示它不是当前版本的合法数据包且必须被丢弃。此比特位为1
允许QUIC与其他协议共存,详见《RFC7983》。 - 长数据包类型(Long Packet Type):
-
字节0中的后两个比特位(掩码为
0x30
的那两个)包含了数据包类型。表5罗列了数据包类型。 - 类型特定比特位(Type-Specific Bits):
-
字节0中最低的四个比特位(掩码为
0x0f
的那四个)的语义由数据包类型决定。 - 版本(Version):
-
QUIC的版本是一个跟在第一个字节后的32比特长的字段。该字段表明了正在使用的QUIC版本并且决定了如何解释剩下的协议字段。
- 目标连接ID长度(Destination Connection ID Length):
-
跟在版本后面的那个字节包含了这个字节后方的目标连接ID字段的长度。这个长度以字节为单位,且被编码为一个8位无符号整型值。在QUIC版本1中,该值必须不超过20字节。收到版本为
1
的长包头数据包且本字段的值超过20
的终端必须丢弃它。为了正确构造一个版本协商数据包,服务器应该有能力从其他QUIC版本中读取更长的连接ID。 - 目标连接ID(Destination Connection ID):
-
目标连接ID字段跟在目标连接ID长度字段后面,后者指出了本字段的长度。第7.2章更详细地描述了本字段的用法。
- 源连接ID长度(Source Connection ID Length):
-
跟在目标连接ID后面的那个字节包含了这个字节后方的源连接ID字段的长度。这个长度以字节为单位,且被编码为一个8位无符号整型值。在QUIC版本1中,该值必须不超过20字节。收到版本为1的长包头数据包且本字段的值超过
20
的终端必须丢弃它。为了正确构造一个版本协商数据包,服务器应该有能力从其他QUIC版本中读取更长的连接ID。 - 源连接ID(Source Connection ID):
-
源连接ID字段跟在源连接ID长度字段后面,后者指出了本字段的长度。第7.2章更详细地描述了本字段的用法。
- 类型特定载荷(Type-Specific Payload):
-
数据包的剩余部分是类型特定的,如果有的话。
在此版本的QUIC中,定义了以下长包头数据包的类型:
一个长包头数据包的包头形式比特位、目标和源连接ID长度、目标和源连接ID,以及版本字段是版本无关的。第一个字节的其他字段是版本特定的。有关怎样解释不同QUIC版本的数据包的细节,见《QUIC不变量》。
第一个字节的其他字段和载荷的解释方法取决于版本和数据包类型。接下来的章节会描述当前版本下,各种数据包类型对它们的特定语义。在这个版本的QUIC中,一些长包头数据包均包含了这些额外字段:
- 保留比特位(Reserved Bits):
-
字节0的某两个比特位(掩码为
0x0c
的那两个)在多种数据包类型中都被保留使用。这些比特位被头部保护所保护,详见《QUIC-TLS》的第5.4章。在进行保护前,这两个比特位的值必须被设置为0。若在移除数据包保护和头部保护之后发现这些位被设置为非零值,则接收到该数据包的终端必须将该情况视作一个类型为PROTOCOL_VIOLATION
的连接错误。仅在移除头部保护后就丢弃这样的数据包会使终端暴露于攻击之下,详见《QUIC-TLS》的第9.5章。 - 数据包号长度(Packet Number Length):
-
在包含数据包号字段的数据包类型中,字节0最低的两个有效位(掩码为
0x03
的那两个)包含数据包号字段的长度。该长度被编码为一个2位无符号整型值,这个值比数据包号字段的字节长度小1
。也就是说,数据包号字段的长度等于这个字段的值加1
。这些比特位被头部保护所保护,详见《QUIC-TLS》的第5.4章。 - 长度(Length):
-
这是数据包剩余部分(也就是数据包号字段和载荷字段)的字节长度,被编码为一个可变长度整型值。
- 数据包号(Packet Number):
-
这个字段的长度是1至4字节。数据包号被头部保护所保护,详见《QUIC-TLS》的第5.4章。数据包号字段的长度被编码进字节0的数据包号长度比特位,见上文。
- 数据包载荷(Packet Payload):
-
这是数据包的载荷——包含一系列帧——它们被数据包保护所保护。
17.2.1 版本协商数据包
版本协商数据包(Version Negotiation packet)本质上不是版本特定的。客户端收到后,将根据版本字段值为0
将其识别为版本协商数据包。
版本协商数据包是对一个包含了服务器不支持的版本的客户端数据包的响应。它只能被服务器发送。
一个版本协商数据包的结构如下:
版本协商数据包 {
包头形式 (1) = 1,
未使用 (7),
版本 (32) = 0,
目标连接ID长度 (8),
目标连接ID (0...2040),
源连接ID长度 (8),
源连接ID (0...2040),
支持的版本 (32) ...,
}
未使用字段中的值由服务器设置为任意值。客户端必须忽略此字段的值。当QUIC可能被与其他协议多路复用时(详见RFC7983),服务器应该将这个字段的最高有效位(0x40
)设置为1
以使得版本协商数据包看起来具有固定比特位字段。要注意的是其他版本的QUIC不一定做出类似的推荐。
版本协商数据包的版本字段必须被设置为0x00000000
。
服务器在目标连接ID字段中使用的值必须来自接收到的那个数据包的源连接ID字段。源连接ID字段的值必须是从接收到的那个数据包的目标连接ID字段中拷贝来的,这是个来自客户端的随机选择的值。回显两个连接ID可以让客户端确信服务器收到了数据包,并且版本协商数据包不是由没有观察初始数据包的实体生成的。
将来版本的QUIC可以对连接ID的长度有不同要求。特别是,连接ID可能具有更小的最小长度或更大的最大长度。
版本协商数据包的剩余部分是一个列表,包含服务器支持的一个个32位长的版本号。
版本协商包不会被确认。它只会作为一个使用了不受支持的版本的数据包的响应而被发送,详见第5.2.2章。
版本协商数据包不包含在其他使用了长包头形式的数据包中存在的数据包号和长度字段。因此,一个版本协商数据包消耗一整个UDP数据报。
服务器必须不对单个UDP数据报响应超过1个版本协商数据包。
有关版本协商的过程,详见第6章。
17.2.2 初始数据包
初始数据包(Initial packet)使用类型值为0x00
的长包头。它携带着发送自客户端和服务器的最初的加密帧以进行密钥交换,同时它还携带着任意方向的ACK帧。
初始数据包 {
包头形式 (1) = 1,
固定比特位 (1) = 1,
长数据包类型 (2) = 0,
保留比特位 (2),
数据包号长度 (2),
版本 (32),
目标连接ID长度 (8),
目标连接ID (0..160),
源连接ID长度 (8),
源连接ID (0..160),
令牌长度 (i),
令牌 (..),
长度 (i),
数据包号 (8..32),
数据包载荷 (8..),
}
初始数据包包含长包头、长度字段和数据包号字段,详见第17.2章。首个字节包含保留比特位和数据包号长度比特位,详见第17.2章。在源连接ID和长度字段间,是初始数据包特有的两个额外字段。
- 令牌长度(Token Length):
-
一个可变长度整型值,它指定了令牌字段以字节为单位的长度。如果不存在令牌,则该值为
0
。由服务器发送的初始数据包必须将令牌长度字段设置为0;收到一个令牌长度字段为非零值的客户端必须要么丢弃数据包,要么产生一个类型为PROTOCOL_VIOLATION
的连接错误。 - 令牌(Token):
-
先前由重试数据包或新令牌帧提供的令牌的值,详见第8.1章。
为了防止不感知版本的中间件的篡改,初始数据包由连接特定密钥和版本特定密钥(初始密钥)所保护,如QUIC-TLS中描述的那样。这种保护在面对有能力观测数据包的攻击者时并不能提供可信度或保证完整性,但是它能防止无法观察到数据包的攻击者伪造初始数据包。
客户端和服务器使用初始数据包这一类型,用于任何包含着初始加密握手消息的数据包。这包含所有需要新创建包含初始加密消息的数据包的情况,例如接收到一个重试数据包后被发送的的那个数据包,详见第17.2.5章。
服务器发送它的首个初始数据包,作为客户端初始包的响应。服务器可以发送多个初始数据包。加密密钥交换可能需要多轮往返或数据重传。
一个初始数据包的载荷包含一个(或多个)加密帧,每个加密帧包含一条加密握手消息,或数个ACK帧,或两者兼具。Ping帧、填充帧和类型为0x1c
的连接关闭帧也是被允许的。终端收到包含其他帧的初始数据包后可以将该数据包作为虚假数据包丢弃或将其视为连接错误。
由客户端发出的首个数据包总是包含一个加密帧,这个加密帧包含首个加密握手消息的起始或所有部分。首个加密帧总是在偏移为0
处起始,详见第7章。
注意如果服务器发送了一个TLS HelloRetryRequest(详见《QUIC-TLS》的第4.7章),客户端将另外发送一系列初始数据包。这些初始数据包将继续进行加密握手,并且其包含的加密帧的起始位置将与第一轮初始数据包中的加密帧尺寸相匹配。
17.2.2.1 停用初始数据包
17.2.3 0-RTT
0-RTT数据包使用类型值为0x01
的长包头,后面跟着长度和数据包号字段,详见第17.2章。首个字节包含保留比特位和数据包号长度比特位,详见第17.2章。0-RTT数据包被用来在握手完成前,携带来自客户端的“早期”数据,作为第一轮通信的一部分被发向服务器。
有关0-RTT数据及其局限的讨论,详见《TLS13》的第2.3章。
0-RTT数据包 {
包头形式 (1) = 1,
固定比特位 (1) = 1,
长数据包类型 (2) = 1,
保留比特位 (2),
数据包号长度 (2),
版本 (32),
目标连接ID长度 (8),
目标连接ID (0..160),
源连接ID长度 (8),
源连接ID (0..160),
长度 (i),
数据包号 (8..32),
数据包载荷 (8..),
}
受0-RTT保护的数据包,与受1-RTT保护的数据包使用相同的数据包号空间。
在客户端接收到重试数据包时,0-RTT数据包有可能是被弄丢了,或者被服务器丢弃了。客户端应该在发送新的初始数据包后尝试用0-RTT数据包重新发送数据。所有新发送的数据包都必须使用新的数据包号;如第17.2.5.3章所述,重用数据包号可能使数据包保护失效。
客户端只有在握手完成后才会收到0-RTT数据包的确认,如《QUIC-TLS》的第4.1.1章所述。
一旦客户端开始处理来自服务器的1-RTT数据包,它就必须不再发送0-RTT数据包。这意味着0-RTT数据包不能包含任何对于来自1-RTT数据包中的帧的回复。比如说,客户端不能在0-RTT数据包中发送ACK帧,因为它只能被用来确认1-RTT数据包。必须用1-RTT数据包来携带对于1-RTT数据包的确认。
服务器必须视违反已记录的限制的情况为一个合适类型的连接错误(例如,超过流数据限制时使用FLOW_CONTROL_ERROR
(流量控制错误))。
17.2.4 握手数据包
握手数据包(Handshake packet)使用类型值为0x02
的长包头,后面跟着长度和数据包号字段,详见第17.2章。首个字节包含保留比特位和数据包号长度比特位,详见第17.2章。这种数据包被用来携带来自服务器和客户端的加密握手消息和确认。
握手数据包 {
包头形式 (1) = 1,
固定比特位 (1) = 1,
长数据包类型 (2) = 2,
保留比特位 (2),
数据包号长度 (2),
版本 (32),
目标连接ID长度 (8),
目标连接ID (0..160),
源连接ID长度 (8),
源连接ID (0..160),
长度 (i),
数据包号 (8..32),
数据包载荷 (8..),
}
一旦客户端接收到了来自服务器的握手数据包,它就开始使用握手数据包来向服务器发送后续加密握手消息和确认。
握手数据包的目标连接ID字段包含一个由数据包接收方选择的连接ID;源连接ID包含的是数据包的发送方想要使用的连接ID,详见第7.2章。
握手数据包有它们自己的数据包号空间,因此由服务器发送的首个握手数据包使用的是值为0
的数据包号。
这种数据包的载荷是加密帧,也可以包含Ping帧、填充帧或ACK帧。握手数据包可以包含类型为0x1c
的连接关闭帧。若握手数据包中出现了其他种类的帧,则接收到该数据包的终端必须将该情况视作一个类型为PROTOCOL_VIOLATION
(协议违背)的连接错误。
如初始数据包一样(详见第17.2.2.1章),当握手保护密钥被弃用时,握手数据包中的加密帧里的数据会被丢弃且不再重新传输。
17.2.5 重试数据包
如图18所示,重试数据包(Retry packet)使用类型值为0x03
的长包头。它携带着由服务器创建的一个地址验证令牌。想要进行重试的服务器会使用它,详见第8.1章。
重试数据包 {
包头形式 (1) = 1,
固定比特位 (1) = 1,
长数据包类型 (2) = 3,
未使用 (4),
版本 (32),
目标连接ID长度 (8),
目标连接ID (0..160),
源连接ID长度 (8),
源连接ID (0..160),
重试令牌 (..),
重试完整性标签 (128),
}
重试数据包不包含任何受保护的字段。未使用字段中的值被服务器设置为任意值;客户端必须忽略这些比特位。除了来自长包头的字段之外,它还包含以下额外字段:
17.2.5.1 发送重试数据包
服务器在填写目标连接ID时,使用客户端在初始数据包中填写的源连接ID的值。
服务器在源连接ID字段中使用自己选择的连接ID。这个值必须不与发送自客户端的数据包的目标连接ID字段相同。如果客户端发现一个重试数据包中包含的的源连接ID字段与它发送的初始数据包中的目标连接ID字段一致,那么它必须将这个数据包丢弃。客户端必须使用来自重试数据包中源连接ID字段的值,作为目标连接ID字段来发送后续数据包。
服务器可以发送重试数据包来回复初始数据包和0-RTT数据包。服务器可以丢弃或缓存它收到的0-RTT数据包。当服务器接收到数个初始数据包或0-RTT数据包时,它可以发送数个重试数据包。服务器必须不发送超过一个重试数据包以回应单个UDP数据报。
17.2.5.2 处理重试数据包
对于单次连接尝试,客户端必须接受和处理至多一个重试数据包。在客户端已经接收和处理来自服务器的初始数据包或重试数据包之后,它必须丢弃后续接收到的任何重试数据包。
对于重试完整性标签无法被验证的重试数据包,客户端必须丢弃它们,详见《QUIC-TLS》的第5.8章。这会降低攻击者注入重试数据包的能力并且保护重试数据包免于意外受损。客户端必须丢弃重试令牌字段长度为零的重试数据包。
作为重试数据包的回复,客户端使用包含了前者提供的重试令牌的初始数据包来继续连接的建立过程。
客户端使用来自重试数据包中源连接ID字段的值,作为这个初始数据包的目标连接ID字段。更改目标连接ID字段会改变用来保护初始数据包的密钥。客户端还将令牌字段设置为重试数据包中提供的令牌。客户端必须不更改源连接ID因为服务器可能将连接ID作为了它令牌验证逻辑的一部分,详见第8.1.4章。
重试数据包不包含数据包号且不能被客户端显式确认。
17.2.5.3 重试后继续握手
后续来自客户端的初始数据包包含着来自重试数据包的连接ID和令牌值。客户端从重试数据包中拷贝源连接ID字段至目标连接ID字段并且持续使用它,直到接收到一个包含着更新过的值的初始数据包,详见第7.2章。令牌字段的值被拷贝至所有后续初始数据包,详见第8.1.2章。
除了要更新目标连接ID和令牌字段外,客户端后续发出的初始数据包与首个初始数据包受到同样的限制。客户端必须使用和首个初始数据包中相同的加密握手消息。对于包含了不同加密握手消息的数据包,服务器可以将其视作为一种连接错误,或丢弃它。注意,包含令牌字段会减少数据包中加密握手消息的可用空间,这可能导致客户端需要发送多个初始数据包。
客户端可以在接收到重试数据包后尝试0-RTT,方法是向服务器提供的连接ID发送0-RTT数据包。
客户端必须不在处理完重试数据包后重置任何数据包号空间中的数据包号。尤其是,0-RTT数据包包含着的机密信息很有可能在接收到重试数据包时被重新传输。由于是在回复一个重试数据包,用于保护这些新0-RTT数据包的密钥不会改变。然而,这些数据包中发送的数据可能和之前发送的不一样。用相同的数据包号发送这些新数据包可能使这些数据包的数据包保护失效,因为相同的密钥和随机数可能被用于保护不同内容。服务器如果检测到客户端重置了数据包号。它可以中止这次连接。
在客户端和服务器间被交换的初始数据包和重试数据包中的那些连接ID会被拷贝到传输参数中并且被验证,详见第7.3章。
17.3 短包头数据包
本QUIC版本定义了1种使用短数据包包头的数据包类型。
17.3.1 1-RTT数据包
1-RTT数据包使用短数据包包头。它在协商出版本和1-RTT密钥之后被使用。
1-RTT数据包 {
包头形式 (1) = 0,
固定比特位 (1) = 1,
自旋比特位 (1),
保留比特位 (2),
密钥阶段 (1),
数据包号长度 (2),
目标连接ID (0..160),
数据包号 (8..32),
数据包载荷 (8..),
}
1-RTT数据包包含以下字段:
- 包头形式(Header Form):
-
对于短包头,字节0的最高有效位(
0x80
)被设置为0
。 - 固定比特位(Fixed Bit):
-
字节0中的下一个比特位(
0x40
)被设置为1
。此比特位为0
的数据包表示它不是当前版本的合法数据包且必须被丢弃。此比特位为1
允许QUIC与其他协议共存,详见《RFC7983》。 - 自旋比特位(Spin Bit):
-
字节0的第三高有效比特位(
0x20
)是延迟自旋比特位,按第17.4章中描述的那样去设置。 - 保留比特位(Reserved Bits):
-
字节0中的后两个比特位(掩码为
0x18
的那两个)被保留使用。这些比特位被头部保护所保护,详见《QUIC-TLS》的第5.4章。在进行保护前,这两个比特位的值必须被设置为0。若在移除数据包保护和头部保护之后发现这些位被设置为非零值,则接收到该数据包的终端必须将该情况视作一个类型为PROTOCOL_VIOLATION
的连接错误。仅在移除头部保护后就丢弃这样的数据包会使终端暴露于攻击之下,详见《QUIC-TLS》的第9.5章。 - 密钥阶段(Key Phase):
-
字节0中的下一个比特位(
0x04
)表明了密钥阶段,它允许数据包的接收方辨别用于保护数据包的数据包保护密钥。有关细节见《QUIC-TLS》。这个比特位被头部保护所保护,详见《QUIC-TLS》的第5.4章。 - 数据包号长度(Packet Number Length):
-
在包含数据包号字段的数据包类型中,字节0最低的两个有效位(掩码为
0x03
的那两个)包含数据包号字段的长度。该长度被编码为一个2位无符号整型值,这个值比数据包号字段的字节长度小1
。也就是说,数据包号字段的长度等于这个字段的值加1
。这些比特位被头部保护所保护,详见《QUIC-TLS》的第5.4章。 - 目标连接ID(Destination Connection ID):
-
目标连接ID是一个由此数据包意图的接收方选择的一个连接ID,详见第5.1章。
- 数据包号(Packet Number):
-
这个字段的长度是1至4字节。数据包号被头部保护所保护,详见《QUIC-TLS》的第5.4章。数据包号字段的长度被编码进字节0的数据包号长度字段。有关细节见第17.1章。
- 数据包载荷(Packet Payload):
-
1-RTT数据包总是包含着受1-RTT保护的载荷。
一个短包头数据包的包头形式比特位和目标连接ID字段是版本无关的。其余字段是由选择的QUIC版本特定的。有关怎样解释不同QUIC版本的数据包的细节,见《QUIC不变量》。
17.4 延迟自旋比特位
为1-RTT数据包(第17.3.1章)定义的延迟自旋比特位允许网络路径上的各个观察点在整个连接期间进行被动的延迟监测。
自旋比特位仅在1-RTT数据包中出现,因为若要测量一条连接的初始RTT,可以通过观察握手过程来实现。因此,自旋比特位在版本协商和连接建立完成后才可用。《QUIC可管理性》中进一步讨论了延迟自旋比特位在链路上的测量和使用。
在本QUIC版本中,自旋比特位是一个可选特性。如下文所述,不支持此特性的终端必须禁用它。
每个终端对一条连接是否启用自旋比特位做单方面的决定。各个实现必须允许客户端和服务器的管理员能够禁用自旋比特位,要么全局禁用要么基于单条连接禁用。即使自旋比特位没有被管理员禁用,终端也必须在每16条网络路径中随机地选择至少一条,或每16个连接ID中选择一个,然后在使用这些选出的路径或连接ID时禁用自旋比特位,这是为了确保在网络上能经常观察到禁用自旋比特位的QUIC连接。当每个终端独立地禁用自旋比特位时,能确保自旋比特位信号量在大约八分之一的网络路径中是关闭的。
当自旋比特位被禁用时,终端可以将自旋比特位设置为任意值,且必须忽略任何传入值。推荐终端将自旋比特位设置为随机值,要么为每个数据包独立选择,要么为每个连接ID独立选择。
如果自旋比特位在当前连接中是启用的,终端就要为每条网络路径维护一个自旋值,并且当1-RTT数据包要在某条网络路径上发送时,将数据包头部中的自旋比特位设置为当前存储的自旋值。每个终端还要记录在每条路径上的对端所看到的最大数据包号。
当服务器在某条网络路径上接收到一个1-RTT数据包且它增大了服务器所记录的客户端看到的最大数据包号,服务器就将那条路径上的自旋值设置为接收到的那个数据包中的自旋值。
当客户端在某条网络路径上接收到一个1-RTT数据包且它增大了客户端所记录的服务器看到的最大数据包号,客户端就将那条路径上的自旋值设置为接收到的那个数据包中的自旋值的相反值。
当在一条网络路径上改变所使用的连接ID时,终端将那条网络路径上的自旋值重置为0
。
18. 传输参数编码
18.1 保留传输参数
标识符为31 * N + 27
——其中N
为整数——的传输参数保留用于执行未知传输参数。这些传输参数没有语义,可以携带任意值。
18.2 传输参数定义
本章描述本文定义的传输参数的细节。
这里列出的许多传输参数都是整型值。那些整型的传输参数使用变长整数编码,详见第16章。除非有额外说明,否则传输参数在不设置时的默认值是0。
各个传输参数具体定义如下:
- 原始目标连接ID
original_destination_connection_id
(0x00
): -
这个参数是由客户端发出的第一个初始包的目标连接ID字段的值,详见第7.3章。该传输参数只会由服务端发出。
- 最大空闲超时时间
max_idle_timeout
(0x01
): -
最大空闲超时是一个编码为整型的值,单位毫秒,详见第10.1章。当双端均忽略此传输参数或设置其值为0时,空闲超时将被禁用。
- 无状态重置令牌
stateless_reset_token
(0x02
): -
无状态重置令牌用于验证无状态重置,详见第10.3章。这个传输参数是一个16字节的序列。客户端必须不能发送这个参数,但是服务端可以发送。没有发送此参数的服务端不能对握手期间协商的连接ID使用无状态重置。
- 最大UDP载荷
max_udp_payload_size
(0x03
): -
最大UDP载荷参数是一个整型值,用于终端限制愿意接收的UDP载荷的大小。UDP报文的载荷如果大于这个限制将不太可能被服务端处理。
-
该参数默认值是最大UDP载荷65527。该值不能小于1200。
-
这个限制是对报文大小的一个与通道MTU一样的附加约束,但其是终端的属性而非通道的属性,详见第14章。应该认为这是终端用于保存传入数据包的空间。
- 初始最大数据量
initial_max_data
(0x04
): -
初始最大数据量参数是一个包含连接可发送初始最大数据量的整型值。等效于连接在完成握手后立即发送一个
最大数据量帧
(MAX_DATA
,详见第19.9章)。 - 初始本地最大双向流数据量
initial_max_stream_data_bidi_local
(0x05
): -
本参数是一个整型值,用于指定本地初始化的双向流的初始流量控制限制。这个限制适用于由发送传输参数端打开的新创建双向流。在客户端传输参数中,其适用于流标识符最低两个有效位设置为
0x00
的流;在服务端,其适用于流标识符最低两个有效位设置为0x01
的流。 - 初始远端最大双向流数据量
initial_max_stream_data_bidi_remote
(0x06
): -
本参数是一个整型值,用于指定对端初始化的双向流的初始流量控制限制。这个限制适用于由接收传输参数端打开的新创建双向流。在客户端传输参数中,其适用于流标识符最低两个有效位设置为
0x01
的流;在服务端,其适用于流标识符最低两个有效位设置为0x00
的流。 - 初始最大单向流数据量
initial_max_stream_data_uni
(0x07
): -
本参数是一个整型值,用于指定单向流的初始流量控制限制。这个限制适用于接收传输参数端打开的新创建单向流。在客户端传输参数中,其适用于流标识符最低两个有效位设置为
0x03
的流;在服务端,其适用于流标识符最低两个有效位设置为0x02
的流。 - 初始最大双向流数量
initial_max_streams_bidi
(0x08
): -
初始最大双向流数量参数是一个整型值,包含接收该传输参数的终端允许初始化的最大双向流数量。如果这个参数未设置或置为0,则对端不能开启双向流直到发完
最大流帧
。设置该参数等效于发送一个相关流类型的数值一致的最大流帧
(第19.11章)。 - 初始最大单向流数量
initial_max_streams_uni
(0x09
): -
初始最大单向流数量参数是一个整型值,包含接收该传输参数的终端允许初始化的最大单向流数量。如果该参数未设置或设置为0,则对端不能开启单向流直到发完
最大流帧
。设置该参数等效于发送一个相关流类型的数值一致的最大流帧
(第19.11章)。 - ACK延迟指数
ack_delay_exponent
(0x0a
): -
确认延迟指数是一个整型值,用于指定一个指数解码
ACK帧
的“ACK延迟”字段(第19.2章)。如果该值未设置,则置一个默认值3(表示8的乘数)。大于20的值非法。 - 最大ACK延迟
max_ack_delay
(0x0b
): -
最大确认延迟是一个整型值,表示终端会延迟发送确认包的最大毫秒数。该值应该包括警报触发时接收者的预期延迟。例如,如果接收者设置了一个5ms超时的定时器,且警报通常会延迟1ms,那么它应该发送一个值为6ms的
max_ack_delay
参数。如果该值未设置,则会设置默认值25ms。该值为214及以上非法。 - 禁止活跃迁移
disable_active_migration
(0x0c
): -
禁止活动迁移传输参数是在终端握手阶段所使用的地址不支持活跃连接迁移(第9章)时加入的。收到该传输参数的终端必须不使用一个新的本地地址发往对端在握手阶段使用的地址。该传输参数不能在
preferred_address
已经在客户端生效后阻止连接迁移。该参数的值为空。 - 首选地址
preferred_address
(0x0d
): -
服务端首选地址用于在握手最后变更服务端地址,如第9.6章所述。该传输参数只由服务端发送。服务端可以选择只发送一个地址族中的一个首选地址,另一个地址族则发送全零地址及端口(
0.0.0.0:0
或[::]:0
)。IP地址以网络字节序编码。preferred_address
传输参数包含一个IPv4和一个IPv6的地址和端口。4字节IPv4地址字段后接相关的2字节IPv4端口字段。接下来紧随一个16字节IPv6地址字段和一个2字节IPv6端口字段。在地址和端口组合之后的是连接ID长度字段,描述接下来的连接ID的长度。最后是一个与连接ID相关的16字节无状态重置令牌字段。本传输参数格式如下述图22所示。连接ID字段及无状态重置令牌字段包含一个可选的序列号为1的连接ID,详见第5.1.1章。将这些值与首选地址一起发送,可以确保当客户端开始迁移到首选地址时,至少有一个未使用的活动连接ID。
首选地址的连接ID和无状态重置令牌字段在语法和语义上与
新连接ID帧
相应字段相同(详见第19.15章)。服务端若选择了一个零长度的连接ID,则必须不提供首选地址。同理,服务端必须不在首选地址传输参数里包含零长度连接ID。客户端必须必须将违反这些要求的情况视为一个TRANSPORT_PARAMETER_ERROR
类型的连接错误。
首选地址 {
IPv4地址 (32),
IPv4端口 (16),
IPv6地址 (128),
IPv6端口 (16),
连接ID长度 (8),
连接ID (..),
无状态重置令牌 (128),
}
- 活跃连接ID上限
active_connection_id_limit
(0x0e
): -
这是一个整型值,用于指定终端愿意存储的来自对端的最大连接ID数量。该值包括握手阶段收到的连接ID,包括从
preferred_address
参数及新连接ID帧
里收到的。该值必须大于等于2。终端收到小于2的值必须以错误类型TRANSPORT_PARAMETER_ERROR
关闭连接。如果没有设置该传输参数,则默认值为2。如果终端指定一个零长度连接ID,则其永远不会发送新连接ID帧
,且会忽略从对端发来的active_connection_id_limit
参数值。 - 初始源连接ID
initial_source_connection_id
(0x0f
): -
这是终端给相应连接发送的首个初始包的源连接ID字段包含的值,详见第7.3章。
- 重试源连接ID
retry_source_connection_id
(0x10
): -
这是服务端包含于一个重试包的源连接ID字段的值,详见第7.3章。该传输参数仅由服务端发出。
如果存在,用于设置初始化每条流的流量控制上限的传输参数(initial_max_stream_data_bidi_local
、initial_max_stream_data_bidi_remote
及initial_max_stream_data_uni
)等效于在每条流开启后立即给它发送相应流类型的最大流数据量帧(详见第19.10章)。如果相应传输参数没有设置,则其对应流类型的流其初始流量控制上限就是0。
客户端必须不包含任何只有服务端可以发送的传输参数:original_destination_connection_id
、preferred_address
、retry_source_connection_id
或stateless_reset_token
。服务端必须将收到任何此类传输参数的情况视为TRANSPORT_PARAMETER_ERROR
类型连接错误。
19. 帧类型与格式
正如第12.4章所述,数据包包含一个或多个帧。本章描述QUIC帧类型的格式和语义。
19.1 填充帧
19.2 Ping帧
19.3 ACK帧
接收方发送ACK帧(类型为0x02
或0x03
)通知发送方其发出的数据包已经收到并处理完成了。ACK帧包含一个或多个ACK块(ACK Range)。ACK块标识被确认的数据包。如果帧类型为0x03
,ACK帧也会包含到目前为止在该连接上收到的带有相关ECN标记的QUIC数据包的累计值。QUIC实现必须准确处理这两种类型,并且,如果其对发送数据包采用了ECN,其应该使用ECN块中的信息管理其拥塞状态。
QUIC的确认是不可撤销的。数据包一旦被确认,其就会维持在被确认状态,即使其没有出现在后续的ACK帧中。这一点不同于TCP选择性确认(SACK)违约(《RFC2018》)。
属于不同数据包号空间的数据包可以使用相同的数值标识。数据包的一个确认需要同时标明数据包号和数据包号空间。这是通过让每个ACK帧只确认其所在数据包的相同数据包号空间内的数据包实现的。
不能确认版本协商和重试包,因为它们不包含数据包号。这些数据包与其依靠ACK帧,不如通过后续客户端发送的初始包隐式确认。
ACK帧格式如图25所示。
ACK帧 {
类型 (i) = 0x02..0x03,
最大确认数 (i),
ACK延迟 (i),
ACK块计数 (i),
首个ACK块 (i),
ACK块 (..) ...,
[ECN计数 (..)],
}
ACk帧包含下述字段:
- 最大确认数(Largest Acknowledged):
-
一个可变长度整型,表示对端确认的最大数据包号;这通常是对端在生成ACK帧前收到的最大数据包号。不同于QUIC长包头或短包头里的数据包号,ACK帧内的包号没有截断。
- ACK延迟(ACK Delay):
-
一个可变长度整型,编码ACK延迟,单位微秒,详见第13.2.5章。其通过将字段中的值乘以2的
ack_delay_exponent
次方来解码的,其中ack_delay_exponent
传输参数值是由ACK帧发送方发出的,详见第18.2章。相比于简单地用整型表示延迟,这种编码在相同字节数内支持更大范围的值,代价是分辨率较低。 - ACK块计数(ACK Range Count):
-
一个可变长度整型,表示ACK帧中ACK块字段的数目。
- 首个ACK块(First ACK Range):
-
一个可变长度整型,表示在最大确认数之前正在被确认的连续的数据包的数量。也就是说,块内最小被确认的数据包的包号可以通过最大确认数减去首个ACK块值得到。
- ACK块:
-
包含额外的数据包区段,这些数据包可以是未被确认的(空档),也可以是已被确认的(ACK块),详见第19.3.1章。
- ECN计数:
-
三个ECN统计,详见第19.3.2章。
19.3.1 ACK块
每个ACK块由交替的空档和ACK块长度值构成,按包号降序排列。ACK块可以重复。空档和ACK块长度值的数量由ACK块计数字段决定,每个ACK块中的一个值(空档或ACK块长度)对应ACK块计数字段的每个值。
ACK块的结构如图26所示。
ACK块 {
空档 (i),
ACK块长度 (i),
}
构成ACK块的字段是:
- 空档(Gap):
-
一个可变长度整型值,表示比前述ACK块中的最小数据包号小一的数据包之前连续未被确认数据包的数目。
- ACK块长度(ACK Range Length):
-
一个可变长度整型值,表示先前空档确定的最大数据包号之前连续被确认数据包的数目。
空档和ACK块长度值使用一个相对整型编码以提高效率。即使每个编码结果是正值,但也会被减去,因此每个ACK块表示的都是包号逐渐递减的数据包。
每个ACK块确认一段包号连续的数据包,通过ACK块内包号最大的被确认数据包之前的包数指定确认范围。值为0表示只有包号最大的数据包被确认。更大的ACK块值表示更大的确认范围,与此同时该块内最小数据包号则更小。换言之,给定ACK块最大数据包号,则最小数据包号通过下述公式计算:
最小数据包号 = 最大数据包号 - ACK块值
一个ACK块确认最小数据包号到最大数据包号之间的全部数据包,包含两端。
一个ACK块的最大数据包号通过累积减去先前的所有ACK块长度和空档确定。
每个空档表示一段未被确认的数据包。空档内未被确认的数据包数目比空档字段中的编码值多1。
空档字段值使用以下公式为后续ACK块生成最大的数据包号值:
最大数据包号 = 先前最小数据包号 - 空档值 - 2
如果任何有关计算得到的数据包号是负数,则终端必须产生一个FRAME_ENCODING_ERROR
类型的连接错误。
19.3.2 ECN计数
ACK帧使用类型值的最小有效位(也就是类型0x03
)表示ECN反馈,并通告收到在IP头部中带有与ECT(0)、ECT(1)或ECN-CE有关的ECN码点(codepoint)的QUIC数据包。ECN计数只由类型为0x03
的ACK帧携带。
ACK帧中ECN计数如图27所示。
ECN计数 {
ECT0计数 (i),
ECT1计数 (i),
ECN-CE计数 (i),
}
ECN计数各个字段分别是:
- ECT0计数:
-
一个可变长度整型值,表示收到的ACK帧所在数据包号空间里且携带ECT(0)码点的数据包总数。
- ECT1计数:
-
一个可变长度整型值,表示收到的ACK帧所在数据包号空间里且携带ECT(1)码点的数据包总数。
- ECN-CE计数:
-
一个可变长度整型值,表示收到的ACK帧所在数据包号空间里且携带ECN-CE码点的数据包总数。
每个数据包号空间单独维护各自的ECN计数。
19.4 流重置帧
终端使用流重置帧(RESET_STREAM frame,类型为0x04
)立即关闭流的发送部分。
发送完流重置帧后,终端停止在相应的流上传输或重传流帧。流重置帧的接收方可以忽略任何已经在该流上收到的数据。
终端收到一条只用于发送的流的重置帧时必须以STREAM_STATE_ERROR
类型错误关闭连接。
流重置帧格式如图28所示。
流重置帧 {
类型 (i) = 0x04,
流ID (i),
应用层协议错误码 (i),
最终大小 (i),
}
流重置帧包含下述字段:
19.5 停止发送帧
终端使用停止发送帧(STOP_SENDING frame,类型是0x05
)告知对方收到每个应用层请求时传入的数据将被丢弃。停止发送帧要求对端停止在某条流上传输数据。
一个停止发送帧可以在流的“接收”和“数据量确认”状态发送,详见第3.2章。收到一条由本地初始化但是却尚未被创建的流的停止发送帧必须视为一个STREAM_STATE_ERROR
类型的连接错误。终端收到只用于接收的流的停止发送帧必须以STREAM_STATE_ERROR
类型的错误关闭连接。
停止发送帧格式如图29所示。
停止发送帧 {
类型 (i) = 0x05,
流ID (i),
应用层协议错误码 (i),
}
停止发送帧包含如下字段:
- 流ID:
-
一个可变长度整型值,携带需要忽略其数据的流的流ID。
- 应用层协议错误码(Application Protocol Error Code):
-
一个可变长度整型值,包含应用层指定的发送者忽略该流数据的原因,详见第20.2章。
19.6 加密帧
加密帧(CRYPTO frame,类型是0x06
)用于传输加密握手信息。其可以被除0-RTT包以外的其他类型包发送。加密帧为加密协议提供了一个有序的字节流。加密帧除了不携带流标识符(流ID)、不进行流控,以及不携带可选偏移量、可选长度及流结束标记外,它们在功能上与流帧完全相同。
加密帧格式如图30所示。
加密帧 {
类型 (i) = 0x06,
偏移 (i),
长度 (i),
加密数据 (..),
}
加密帧包含下述字段:
- 偏移(Offset):
-
一个可变长度整型值,表示加密帧中加密数据在数据流中的字节偏移量。
- 长度:
-
一个可变长度整型值,表示加密帧的加密数据字段携带数据长度。
- 加密数据(Crypto Data):
-
加密信息数据。
每个加密级别都有一条单独的加密握手数据流,每条流都从偏移量0开始。也就是说每个加密级别都被视为一条单独的加密数据流。
这样的数据流其最大偏移量——偏移量与数据长度之和——不能超过226-1
。收到一个超过此上限的的帧必须视为一个FRAME_ENCODING_ERROR
类型或CRYPTO_BUFFER_EXCEEDED
类型的连接错误。
不同于流帧有着流ID用以区分数据属于哪条流,加密帧为每个加密级别携带单条流的数据。数据流没有一个明确的结束点,所以加密帧没有FIN位。
19.7 新令牌帧
服务端发送新令牌帧(NEW_TOKEN frame,类型是0x07
)给客户端提供一张令牌,以便客户端在一条后续流的初始包包头中携带该令牌发送过来。
新令牌帧格式如图31所示。
新令牌帧 {
类型 (i) = 0x07,
令牌长度 (i),
令牌 (i),
}
新令牌帧包含如下字段:
- 令牌长度(Token Length):
-
一个可变长度整型值,表示令牌的字节长度。
- 令牌:
-
一个不透明blob,客户端可以在后续的初始包中使用。令牌必须不为空。客户端必须将收到带空令牌字段的新令牌帧的情况视为一个
FRAME_ENCODING_ERROR
类型的连接错误。
如果包含这类帧的数据包被误认为丢失了,那么客户端可能收到多个携带相同令牌值的新令牌帧。客户端负责丢弃重复的令牌,这些令牌可能用于连接重试,详见第8.1.3章。
客户端必须不能发送新令牌帧。服务端必须将收到新令牌帧视为一个PROTOCOL_VIOLATION
类型连接错误。
19.8 流帧
流帧明确地创建一条流并携带流数据。流帧的类型字段格式形如0b00001XXX
(即从0x08
到0x0f
之间的值)。帧类型的三个低比特位标示帧的如下字段:
- 帧类型中的
OFF
位(0x04
)用于标识帧的偏移字段。 当置为1,则表示偏移字段存在。 当置为0,则偏移字段不存在,且流数据的偏移量从0另开(也就是说,该帧携带这条流的起始字节,或该流的终点且不携带任何数据)。 - 帧类型的
LEN
位(0x02
)用于标识帧的长度字段。 当置为0,则表示长度字段不存在,且流数据字段延续到数据包的末尾。 当置为1,则表示长度字段存在。 - 帧类型的
FIN
位(0x01
)标志流的结束。 流的最终数据量等于偏移量与该帧的长度之和。
如果收到属于一条由本地初始化却尚未创建的流或一条只用于发送的流的流帧,则终端必须以STREAM_STATE_ERROR
类型错误关闭连接。
流帧格式如图32所示。
流帧 {
类型 (i) = 0x08..0x0f,
流ID (i),
[偏移 (i)],
[长度 (i)],
流数据 (..),
}
流帧包含如下字段:
- 流ID:
-
一个可变长度整型值,表示流的流ID,详见第2.1章。
- 偏移(Offset):
-
一个可变长度整型值,表示流帧中的流数据在整条流中的字节偏移量。这个字段在
OFF
位置为1时存在。当偏移字段不存在时,偏移量为0。 - 长度:
-
一个可变长度整型值,表示流帧中的流数据字段的长度。该字段在
LEN
位置为1时存在。当LEN
位置为0,则流数据字段会囊括数据包的所有剩余字节。 - 流数据(Stream Data):
-
指定流中需要传递的字节。
当流数据字段长度为0,流帧的偏移量就是下一个将要发送的字节的偏移量。
流的首字节的偏移量是0。流传输的最大偏移量——帧的偏移值与数据长度之和——不能超过262-1
,因为无法为这样的数据量分配流量控制额度。收到一个超过该限制的帧的情况必须视为一个FRAME_ENCODING_ERROR
类型或FLOW_CONTROL_ERROR
类型的连接错误。
19.9 最大数据量帧
最大数据量帧(MAX_DATA frame,类型是0x10
)用于流量控制,告知对端可以在整个连接上发送的最大数据量。
最大数据量帧格式如图33所示。
最大数据量帧 {
类型 (i) = 0x10,
最大数据量 (i),
}
最大数据量帧包含下述字段:
- 最大数据量(Maximum Data):
-
一个可变长度整型值,表示可以在整个连接上发送的最大数据量,单位字节。
流帧上发送的所有数据的总和趋近该限制。所有流的最终数据量之和——包括处于关闭状态的流——必须不可超过接收方指定的这个值。如果终端收到的数据量超过了它发出的最大数据量值,其必须以FLOW_CONTROL_ERROR
类型错误关闭流。这包括违背早期数据记录的限制,详见第7.4.1章。
19.10 最大流数据量帧
最大流数据量帧(MAX_STREAM_DATA frame,类型是0x11
)用于流量控制中通知对端一条流上可以发送的最大数据量。
最大流数据量帧可以在流的“接收”状态发送,详见第3.2章。收到一条由本地初始化却尚未创建的流的最大流数据量帧的情况必须视为一个STREAM_STATE_ERROR
类型连接错误。收到只用于发送的流的最大流数据量帧的终端必须以STREAM_STATE_ERROR
类型错误关闭连接。
最大流数据量帧格式如图34所示。
最大流数据量帧 {
类型 (i) = 0x11,
流ID (i),
最大流数据量 (i),
}
最大流数据量帧包含如下字段:
- 流ID:
-
被作用的流的流ID,以可变长度整数值编码。
- 最大流数据量(Maximum Stream Data):
-
一个可变长度整型值,表示可以在标识流上发送的最大数据量,单位字节。
当累计数据量趋近该限制,终端统计在流上发送或接收的数据的最大接收偏移量。丢失或乱序可能意味着一条流的最大接收数据偏移量可能会大于该流上收到的总数据量。收到流帧可能不会提升最大接收偏移量。
流上发送的数据必须不可以超过接收方发出的最大流数据量值的最大值。如果终端收到比其给相关流发出的最大流数据量的最大值更多的数据量,终端必须以FLOW_CONTROL_ERROR
类型错误关闭连接。这包括违背早期数据记录的限制,详见第7.4.1章。
19.11 最大流帧
最大流帧(MAX_STREAMS frame,类型是0x12
或0x13
)用于告知对端允许打开给定类型的流的累积数量。类型0x12
的最大流帧用于双向流,类型0x13
的则用于单向流。
最大流帧格式如图35所示。
最大流帧 {
类型 (i) = 0x12..0x13,
最大流数 (i),
}
最大流帧包含下述字段:
- 最大流数:
-
在连接的生命周期内可以打开的相应类型流的累积总数。该值不能超过
260
,因为不能编码大于260-1
的流ID。收到允许开启高于该限制数量流的最大流帧的情况必须视为一个FRAME_ENCODING_ERROR
类型的连接错误。
丢失或乱序可能导致终端收到一个最大流帧,其限制低于之前收到的最大流帧。必须忽略不能提高流数量限制的最大流帧。
终端必须不打开超过其对端设置的当前流限制所允许的更多的流。例如,服务端收到限制单向流数量为3,则其可以打开流3、7和11,但是不能打开流15。如果对端打开超过其被允许的流,那么终端必须以STREAM_LIMIT_ERROR
类型错误关闭连接。这包括违背早期数据记录的限制,详见第7.4.1章。
注意这些帧(以及有关传输参数)并未描述可以并发打开的流的数量。该限制包括已经关闭的流和打开的流。
19.12 数据阻塞帧
19.13 流数据阻塞帧
19.14 流阻塞帧
发送方应该在其希望打开一条流但是被对端设置的最大流上限(详见第19.11章)所限制时,发送流阻塞帧(STREAM_BLOCKED frame,类型是0x16
或0x17
)。类型为0x16
的流阻塞帧用于表示双向流达到上限,而类型0x17
则表示单向流达到上限。
流阻塞帧不会打开流,而是告知对端需要打开一条流,但是当前的流数上限阻止了这条流的创建。
流阻塞帧格式如图38所示。
流阻塞帧 {
类型 (i) = 0x16..0x17,
最大流数 (i),
}
流阻塞帧包含如下字段:
- 最大流数:
-
一个可变长度整型值,表示该帧发送时允许创建的最大流数量。该值不能超过
260
,因为不能编码超过262-1
的流ID。收到编码值超过这个限制的流阻塞帧的情况必须被视为一个STREAM_LIMIT_ERROR
或FRAME_ENCODING_ERROR
类型的连接错误。
19.15 新连接ID帧
终端发送新连接ID帧(NEW_CONNECTION_ID frame,类型为0x18
)给对端提供可选连接ID,其可以用于连接迁移时中断连接性,详见第9.5章。
新连接ID帧格式如图39所示。
新连接ID帧 {
类型 (i) = 0x18,
序列号 (i),
停用至 (i),
长度 (i),
连接ID (8..160),
无状态重置令牌 (128),
}
新连接ID帧包含如下字段:
- 序列号:
-
序列号由发送方分配给连接ID,编码为可变长度整型值,详见第5.1.1章。
- 停用至(Retire Prior To):
-
一个可变长度整型值,表示被停用的连接ID(们),详见第5.1.2章。
- 长度(Length):
-
一个8位无符号整型值,包含连接ID的长度。小于1或大于20的长度值均是无效的,且必须视其为一个
FRAME_ENCODING_ERROR
类型连接错误。 - 连接ID:
-
一个指定长度的连接ID。
- 无状态重置令牌(Stateless Reset Token):
-
一个128位值,在对关联的连接ID进行无状态重置时使用,详见第10.3章。
如果终端当前需要对端使用0长度的目标连接ID,那么其必须不能发送该帧。将连接ID长度改为0长度或从0长度改为非0长度都会使得难以辨别连接ID值何时发生了改变。终端发送0长度目标连接ID的数据包时,必须将收到新连接ID帧的情况视为一个PROTOCOL_VIOLATION
类型连接错误。
传输错误、超时和重传可能导致相同的新连接ID帧被重复接收。重复接收相同的该类帧的情况必须不能被当作连接错误处理。接收方可以根据新连接ID帧提供的序列号处理重复收到相同新连接ID帧的情况。
如果终端收到一个新连接ID帧重复了之前发布的连接ID,却有着不同的无状态重置令牌或不同的序列号字段值或该序列号用于其他不同的连接ID,终端可以将之视为一个PROTOCOL_VIOLATION
类型连接错误。
“停用至”字段适用于在连接设置期间创建的连接ID以及preferred_address
传输参数,详见第5.1.2章。“停用至”字段值必须小于或等于“序列号”字段值。收到“停用至”字段值大于序列号字段值必须视为一个FRAME_ENCODING_ERROR
类型连接错误。
一旦发送方指定了一个“停用至”字段值,在后续新连接ID帧中发送的较小值就不再发挥作用了。接收方必须忽略任何没有提高最大“停用至”值的“停用至”字段。
收到序列号小于已收到新连接ID帧的“停用至”字段值的新连接ID帧的终端必须发送一个相应的停用新收到连接ID的停用连接ID帧,除非其已经对该序列号发过这个帧了。
19.16 停用连接ID帧
终端发送停用连接ID帧(RETIRE_CONNECTION_ID frame,类型是0x19
)来表明其将不再使用对端发布的某个连接ID。这包含握手期间提供的连接ID。发送一个停用连接ID帧也作为一个请求令对端发送额外的连接ID以备后续使用,详见第5.1章。新连接ID可以通过新连接ID帧发往对端。
停用一个连接ID会令该连接ID关联的无状态重置令牌失效。
停用连接ID帧格式如图40所示。
停用连接ID帧 {
类型 (i) = 0x19,
序列号 (i),
}
停用连接ID帧包含如下字段:
- 序列号:
-
被停用的连接ID的序列号,详见第5.1.2章。
收到包含大于任何先前发往对端的序列号的停用连接ID帧的情况必须视为一个PROTOCOL_VIOLATION
类型连接错误。
停用连接ID帧指定的序列号必须不能指向包含该帧的数据包的目标连接ID。对端可以将这种情况视为一个PROTOCOL_VIOLATION
类型连接错误。
如果对端提供过一个零长度连接ID,终端不能发生这类帧。提供零长度连接ID的终端必须将收到停用连接ID帧视为一个PROTOCOL_VIOLATION
类型连接错误。
19.17 通道挑战帧
19.18 回复通道帧
19.19 连接关闭帧
终端发送连接关闭帧(CONNECTION_CLOSE frame,类型是0x1c
或0x1d
)通知对端连接正在关闭中。0x1c
类型的连接关闭帧用于QUIC层发送错误信号,或没有错误(携带NO_ERROR
码)。0x1d
类型的连接关闭帧用于使用QUIC的应用程序发送错误信号。
如果有打开的流尚未被显式关闭,当连接关闭时它们将被隐式关闭。
连接关闭帧格式如图43所示。
连接关闭帧 {
类型 (i) = 0x1c..0x1d,
错误码 (i),
[帧类型 (i)],
原因语句长度 (i),
原因语句 (..),
}
连接关闭帧包含如下字段:
- 错误码:
-
一个可变长度整型值,表示关闭该连接的原因。
0x1c
类型连接关闭帧使用第20.1章定义的错误码;0x1d
类型连接关闭帧使用第20.2章定义的错误码。 - 帧类型:
-
一个可变长度整型值,编码触发该错误的帧类型。值为0(相当于填充帧)用于未知帧类型。应用层型连接关闭帧(类型
0x1d
)不包含此字段。 - 原因语句长度(Reason Phrase Length):
-
一个可变长度整型值,指定原因语句的字节长度。由于连接关闭帧不能被拆分到多个数据包,因此任何对数据包大小作出的限制也会相应地限制原因语句的可用空间。
- 原因语句(Reason Phrase):
-
关闭相关的附加诊断信息。如果发送方选择不在错误码之外给出更多细节,该字段可以是零长度。该字段应该是UTF-8编码字符串RFC3629,即使该帧没有携带如语言标签之类的信息,这些信息有助于帮助创建文本之外的其他实体理解。
连接关闭帧(0x1d
类型)的应用指定变量只可以通过0-RTT或1-RTT包发送,详见第12.5章。当应用层意图在握手阶段放弃一条连接时,终端可以通过初始包或握手包发送一个错误码为APPLICATION_ERROR
的连接关闭帧(类型0x1c
)。
19.20 握手完成帧
服务端使用握手完成帧(HANDSHAKE_DONE frame,类型0x1e
)向客户端发送确认握手的信号。
握手完成帧格式如图44所示,可见该类帧没有内容。
握手完成帧 {
类型 (i) = 0x1e,
}
握手完成帧只可以由服务端发送。服务端必须不能在完成握手前发送握手完成帧。服务端必须必须将收到握手完成帧视为一个PROTOCOL_VIOLATION
类型连接错误。
19.21 扩展帧
QUIC帧不使用自描述编码。终端因而需要理解所有帧的语法才能成功处理数据包。这使帧的编码得以高效,但是也意味着终端不能发送一个对端不知道类型的帧。
意图使用新类型帧的QUIC扩展必须事先确保对端能够理解该帧。终端可以使用传输参数表示其愿意接收的扩展帧类型。一个传输参数可以表示支持一个或多个扩展帧类型。
修改或替换核心协议功能(包括帧类型)的扩展将难以与其他修改相同功能的扩展兼容,除非明确定义了组合的行为方式。这种扩展应该定义如何与先前定义的修改了相同协议组件的扩展进行互动。
扩展帧必须是受拥塞控制的,且必须触发ACK帧发送。替换或补充ACK帧的扩展帧除外。扩展帧不包含在流量控制中,除非在扩展中有指定。
IANA注册表用于管理帧类型的分配,详见第22.2章。
20. 错误码
QUIC传输层错误码和应用层错误码都是62位无符号整型。
20.1. 传输层错误码
本章列出可能在0x1c
类型连接关闭帧中使用的QUIC错误码的定义。这些错误可能在整个连接期间发生。
- NO_ERROR (
0x00
,无错误): -
终端在连接关闭帧中携带该错误码以通知连接在没有发生任何错误之下关闭。
INTERNAL_ERROR (0x01
,内部错误):
: 终端遇到内部错误而不能继续维持连接。
- CONNECTION_REFUSED (
0x02
,连接拒绝): -
服务端拒绝接收新连接。
- FLOW_CONTROL_ERROR (
0x03
,流量控制错误): -
终端收到的数据量多于其被推荐的数据量上限,详见第4章。
- STREAM_LIMIT_ERROR (
0x04
,流限制错误): -
终端收到一个流标识对应帧超过了该相关类型的流被推荐的流上限。
- STREAM_STATE_ERROR (
0x05
,流状态错误): -
终端收到流的一个帧,但是当前所处状态不允许接收该类帧,详见第3章。
- FINAL_SIZE_ERROR (
0x06
,最终大小错误): -
1、终端收到包含超过先前确立的最终大小数据量数据的流帧;2、终端收到包含一个最终大小的流帧或流重置帧,该最终大小小于该流已经收到数据的大小;3、或终端收到包含的最终大小与已经确立的最终大小不一致的流帧或流重置帧。
- FRAME_ENCODING_ERROR (
0x07
,帧编码错误): -
终端收到一个错误格式的帧——例如,一个未知类型的帧,或包含过多确认范围以至于超出数据包剩余空间所能承载的ACK帧。
- TRANSPORT_PARAMETER_ERROR (
0x08
,传输参数错误): -
终端收到的传输参数存在格式错误、包含无效值、省略了强制要求传输的、传输了禁止传输的,或存在其他错误。
- CONNECTION_ID_LIMIT_ERROR (
0x09
,连接ID限制错误): -
对端提供的连接ID数量超出了
active_connection_id_limit
的限制。 - PROTOCOL_VIOLATION (
0x0a
,协议违背): -
终端检测到未被更具体的错误码覆盖的违背协议错误。
- INVALID_TOKEN (
0x0b
,无效令牌): -
服务端收到客户端包含无效令牌字段的初始包。
- APPLICATION_ERROR (
0x0c
,应用错误): -
应用程序或应用层协议导致连接关闭。
- CRYPTO_BUFFER_EXCEEDED (
0x0d
,加密缓存溢出): -
终端在加密帧收到的数据量超出其缓存容量。
- KEY_UPDATE_ERROR (
0x0e
,密钥更新错误): - AEAD_LIMIT_REACHED (
0x0f
,触及AEAD上限): -
终端已经触及给定连接所用AEAD算法的保密性或完整性上限。
- NO_VIABLE_PATH (
0x10
,无可行通道): -
终端已经确认网络通道不能支持QUIC。终端不太可能收到携带该错误码的连接关闭帧,除非通道不支持足够大的MTU。
- CRYPTO_ERROR (
0x0100
-0x01ff
,加密错误): -
加密握手失败。为所使用的加密握手专用的错误码保留256个值的范围。使用TLS进行加密握手时可能出现的错误码详见《QUIC-TLS》第4.8章。
20.2 应用协议错误码
21. 关于安全性的考量
QUIC的目标是提供一条安全的传输层连接。第21.1章给出了关于这种安全性的概述;后续章节讨论了有关安全性的限制和警告,其中描述了已知的攻击手段和对抗措施。
21.1. 安全性概述
完整的对于QUIC安全性的分析超出了本文档的范畴。本节对QUIC应有的安全性给出了一份非正规的描述,用它来辅助QUIC实现方对协议进行分析。
QUIC使用了在《SEC-CONS》中描述的威胁模型假设,并针对此模型中的多种攻击方式提供了保护手段。
为此,我们将攻击分类为被动攻击和主动攻击。被动的攻击者具有从网络中读取数据包的能力,而主动的攻击者还具有向网络写入数据包的能力。然而,被动攻击中的攻击者可以具有引发路由变化的能力或对传递数据包的路径作出其他修改的能力。
攻击者还被归类为在路径上的攻击者或不在路径上的攻击者。在路径上的攻击者可以读取或修改它所观察到的任意数据包,还可以移除任意数据包从而使得数据包无法到达它的目的地,而不在路径上的攻击者能观察数据包但无法阻止原始数据包抵达它的目的地。此外,这两种类型的攻击者都能发送任意数据包。这样的定义与《SEC-CONS》的第3.5章中的不同,区别是不在路径上的攻击者可以观察到数据包。
握手的安全性、受保护数据包的安全性和连接迁移的安全性会被分别考量。
21.1.1. 握手
QUIC握手中包含着TLS 1.3握手,所以会继承在《TLS13》的附录E.1中描述的加密安全性。QUIC的安全性有相当一部分依靠TLS握手来提供。任何针对TLS握手的攻击都会影响到QUIC。
对于在会话密钥的机密性和唯一性上或在认证对端身份时让步的TLS握手的攻击会影响到QUIC所提供的依赖这些密钥的安全性保证。比如,连接迁移(详见第9章)为了避免跨网络路径的可关联性,依赖着在使用TLS握手的密钥协商过程和QUIC数据包保护过程中的可信度保护。
针对TLS握手的完整性进行的攻击可能使得攻击者能够影响应用协议或QUIC版本的选择。
在TLS提供的安全性之外,QUIC握手也提供了一些防御手段来对抗针对握手的拒绝服务攻击。
21.1.1.1. 抗放大
地址验证(详见第8章)被用于验证声称位于某个地址的实体是否有在那个地址接收数据包的能力。对于一些地址,攻击者能够观察到发向这些地址的数据包,而地址验证能限制针对这些地址的放大攻击。
在完成地址验证前,终端在能够发送的数据量上会受到限制。终端向未经验证的地址发送的数据量不能超过从那个地址接收到的数据量的三倍。
注意:抗放大限制仅适用于终端对接收自未经验证地址的数据包进行响应的情况。抗放大限制不适用于客户端建立新连接或发起连接迁移的情况。
21.1.1.2. 针对服务器的拒绝服务
服务器为完整的握手过程计算首轮数据可能需要耗费大量资源,其中需要涉及签名和密钥交换的计算。为了抵御针对计算资源的拒绝服务攻击,重试数据包提供了一种低成本的令牌交换机制,使得服务器能够以单轮数据包往返的代价,在进行大量计算前先验证客户端的IP地址。在成功完成握手后,服务器可以向客户端签发新令牌,这使得下次建立连接时不需要引入上述代价。
21.1.1.3. 在路径上的握手终止
在路径上的或不在路径上的攻击者都可以通过替换或竞速初始数据包的方式强制握手失败。一旦合法的初始数据包完成交换,后续的握手数据包就会受到握手密钥的保护,于是在路径上的攻击者就不能再强制握手失败,除非它丢弃掉数据包从而使得终端放弃尝试连接。
在路径上的攻击者还可以替换任意一侧的数据包中的地址,从而使得客户端或服务器得知虚假的远程地址。这样的攻击无法与NAT进行的转换相区分。
21.1.1.4. 参数协商
整个握手都受到加密保护,其中初始数据包被由QUIC版本指定的密钥加密,握手数据包和后续数据包被衍生自TLS密钥交换的密钥加密。此外,参数协商会被包含在TLS的记录单(transcript
)中,因此它与普通的TLS协商提供相同的完整性保证。攻击者可以观测客户端的传输参数(只要它知道那个由QUIC版本指定的盐),但是却不能观测到服务器的传输参数,因此无法影响到参数协商。
连接ID不会被加密,但是会在所有数据包中受到完整性保护。
本QUIC版本中不包含版本协商机制;互不兼容的版本的实现之间无法建立连接。
21.1.2. 受保护的数据包
数据包保护(详见第12.1章)将认证加密应用到除版本协商数据包外的所有数据包上,不过出于使用了由QUIC版本指定的密钥材料的缘故,初始数据包和重试数据包仅受到有限的保护;有关细节详见《QUIC-TLS》。本节考量了针对受保护数据包的被动与主动攻击。
在路径上和不在路径上的攻击者都能发起被动攻击,在这种攻击中,攻击者将观察到的数据包保存起来,将之用于将来的针对数据包保护的离线攻击;这对任何网络中任何数据包的任何观察者都是成立的。
没有观察连接中数据包的能力就注入数据包的攻击者不太可能会成功,因为数据包保护确保了合法的数据包只能由拥有在握手期间建立的密钥材料的终端才能创建出来;详见第7章和第21.1.1章。类似地,任何观察数据包并尝试往这些数据包中插入新数据或修改原有数据的主动攻击者应该都无法创建出能被接收方认定为合法的数据包,除非它操作的是初始数据包。
伪造攻击,也就是由主动攻击者改写由它转发或被它注入的数据包中未经保护的部分,例如源地址或目标地址,只有在攻击者能够将数据包转发到原始终端上时才会生效。数据包保护确保了数据包载荷只能被完成了握手的终端所处理,并且非法的数据包会被终端忽略。
攻击者还能修改数据包与UDP数据报间的边界,使得多个数据包被合并至单个数据报中,或将经合并的数据包拆分至多个数据报中。除了需要扩充的包含初始数据包的数据报外,对数据包在数据报中的排列方式的修改对于连接没有实际效果,不过它可以改变一些性能上的特征。
21.1.3. 连接迁移
21.1.3.1. 在路径上的主动攻击
能够令观察到的数据包抵达不了它原本目的地的攻击者被认为是在路径上的攻击者。当客户端和服务器间存在攻击者时,终端为了在给定路径上建立连接,发送的数据包不得不经过此攻击者。
在路径上的攻击者可以做到:
-
检视数据包
-
修改IP数据包和UDP数据包头部
-
注入新数据包
-
使数据包延误
-
对数据包重新排序
-
丢弃数据包
-
拆分与合并数据报和数据包
在路径上的攻击者不能做到:
- 既修改数据包中经认证的部分又使得接收方接收该数据包。
在路径上的攻击者有修改它观察到的数据包的机会;然而,对于数据包中经认证部分的任何修改都会使得它被接收方认定为非法而遭到丢弃,因为数据包载荷是经过认证与加密的。
QUIC旨在将在路径上的攻击者的能力限制至:
-
在路径上的攻击者可以阻止将一条路径用于建立连接,如果终端找不到一条不包含攻击者的路径,连接就会失败。这可以通过丢弃所有数据包、修改数据包以使得它们无法通过解密或其他方式来做到。
-
在路径上的攻击者可以通过使新路径上的地址验证失败的方式,阻止连接迁移到一条依旧包含攻击者的路径上。
-
在路径上的攻击者不能阻止客户端迁移到一条不包含攻击者的路径上。
-
在路径上的攻击者可以通过使数据包延误或丢弃数据包的方式,降低连接的吞吐量。
-
在路径上的攻击者不能令终端接受一个被它修改过经认证部分的数据包。
21.1.3.2. 不在路径上的主动攻击
不在路径上的攻击者并不直接位于客户端与服务器间的路径上,但是拥有获得客户端与服务器间发送的部分或全部数据包的能力。它还能够将那些数据包的副本发送给任一终端。
不在路径上的攻击者可以做到:
-
检视数据包
-
注入新数据包
-
对注入的数据包重新排序
不在路径上的攻击者不能做到:
-
修改由终端发送的数据包
-
使数据包延误
-
丢弃数据包
-
对原始数据包重新排序
不在路径上的攻击者可以为它观察到的数据包创建修改过的副本,并将这些副本注入网络中,副本的源地址和目标地址可以是伪造的。
在本讨论中,我们假定不在路径上的攻击者具有将修改后的数据包副本注入到网络中的能力,并且这个数据包会比它观察的那个原始数据包更早抵达目标终端。换句话说,攻击者具有持续“赢下”与原始数据包间的竞速的能力,并使得原始数据包被接收方忽略。
我们还假设了攻击者具有影响NAT状态所需的资源。尤其是,攻击者有能力使某终端丢失它的NAT绑定,然后取代它,使用与终端先前所使用的相同的端口来吞吐流量。
QUIC旨在将不在路径上的攻击者的能力限制至:
-
不在路径上的攻击者可以竞速数据包并尝试成为在“受限”路径上的攻击者。
-
不在路径上的攻击者可以通过转发数据包并将源地址改为自己的地址的方式,使自己的地址通过验证,前提是它可以在客户端和服务器间提供更优秀的连接质量。
-
不在路径上的攻击者不能在握手完成后引发连接关闭。
-
不在路径上的攻击者不能在无法观察新路径的条件下使得连接迁移到这条路径上。
-
不在路径上的攻击者可以在迁移到新路径的期间成为一个在受限路径上的攻击者,而它在新路径上仍是一个不在路径上的攻击者。
-
不在路径上的攻击者可以通过影响与客户端共享的NAT状态的方式成为一个在受限路径上的攻击者,以便从客户端曾使用过的IP地址和端口向服务器发送数据包。
21.1.3.3. 在受限路径上的主动攻击
在受限路径上的攻击者指的是一个通过在服务器和客户端间复制并转发原始数据包的方式提供更优秀路由质量的不在路径上的攻击者,这会使得那些数据包比原始数据包要更早抵达,从而令原始数据包被目标终端丢弃。
在受限路径上的攻击者与在路径上的攻击者的不同之处在于它并不在终端间的原始路径上,因此由终端发送的原始数据包仍然在前往它们目的地的途中。这意味着只要没有将数据包副本更快地路由到目的地,就无法阻止原始数据包抵达目的地。
在受限路径上的攻击者可以做到:
-
检视数据包
-
注入新数据包
-
修改未经加密的数据包头部
-
对数据包重新排序
在受限路径上的攻击者不能做到:
-
使数据包延误,从而令它们比在原始路径上发送的数据包更晚抵达
-
丢弃数据包
-
修改数据包中经认证和加密的部分,并使得接收方接受该数据包
在受限路径上的攻击者只能够使数据包副本延误至原始数据包抵达为止,这意味着它不能提供比原始路径的延迟表现要差的路由质量。就算在受限路径上的攻击者将数据包副本丢弃,原始数据包仍然会抵达目标终端。
QUIC旨在将在受限路径上的攻击者的能力限制至:
-
在受限路径上的攻击者不能在握手完成后引发连接关闭。
-
在受限路径上的攻击者不能在客户端首先恢复活动的条件下引发空闲连接的关闭。
-
在受限路径上的攻击者可以在服务器首先恢复活动的条件下使得空闲连接被认定为丢失。
注意,出于完全相同的原因,这些保证与任何NAT所提供的保证是一致的。
21.2. 针对握手的拒绝服务攻击
作为一种经加密和认证的传输层协议,QUIC针对拒绝服务提供了诸多保护措施。一旦加密握手完成,QUIC终端就会丢弃绝大多数未经认证的数据包,极大地限制了攻击者介入已建立的连接的能力。
在连接被建立后,QUIC终端可以接受一些未经认证的ICMP数据包(详见第14.2.1章),但是对这些数据包的使用是极度受限的。终端可以接受的另一种数据包是无状态重置(详见第10.3章),它依赖于在令牌被使用前保持令牌的机密性。
在创建连接期间,QUIC提供的保护仅针对不在路径上的攻击。所有的QUIC数据包都包含着能表明终端接收到了来自对端的前序数据包的依据。
在握手期间无法更改地址,所以终端可以丢弃另一条网络路径上接收到的数据包。
在握手期间,源连接ID字段和目标连接ID字段是抵御不在路径上的攻击的主要手段;详见第8.1章。这些字段必须与对端填写的对应字段匹配。除了初始数据包和无状态重置外,终端只接受包含着与终端之前选择的值匹配的目标连接ID字段的数据包。这是为版本协商数据包提供的唯一保护。
初始数据包中的目标连接ID字段是由客户端选择的不可预测的值,它起着额外的作用。传递加密握手消息的数据包会受到密钥的保护,该密钥衍生自此连接ID和由QUIC版本指定的盐。这使得终端在加密握手完成后始终使用一致的计算过程来认证接收到的数据包。未能通过认证的数据包会被丢弃。以这种方式保护数据包能提供一种强有力的保证,保证数据包的发送方接收到了初始数据包并且能够理解它。
这些保护在面对有能力在连接建立前就接收到QUIC数据包的攻击者时并不一定有效。这样的攻击者可能发送出会被QUIC终端接受的数据包。本QUIC版本尝试检测出此类攻击,但是终端应该令连接的建立过程直接失败,而不是试图从中恢复。在握手期间检测出篡改行为的责任主要在于加密握手协议(详见《QUIC-TLS》)。
允许终端使用其他手段来检测出握手被介入的情况并尝试从中恢复。非法数据包可以被其他手段识别出来并被丢弃,不过在本文档中没有推荐什么特别的方法。
21.3. 放大攻击
21.4. 针对乐观ACK的攻击
会对尚未接收到的数据包进行确认的终端可能使得拥塞控制器允许以超过网络所支持的速率进行发送。要检测此行为,终端可以在发送数据包时跳过某些数据包号。终端在检测到对端的此类行为后可以立即用类型为PROTOCOL_VIOLATION
(协议违背)的连接错误来关闭连接;详见第10.2章。
21.5. 请求伪造攻击
在请求伪造攻击中,终端使得对端向受害者发起一次由终端控制的请求。请求伪造攻击旨在使得攻击者能够操作对端所具有的功能,而一般情况下攻击者无法访问到这些功能。对于一份网络协议来说,正因为对端在网络中所处的特殊位置,请求伪造攻击经常被用于利用在受害者与对端间非公开的鉴权认证。
要使请求伪造生效,攻击者需要具有影响对端所发送的数据包以及这些数据包的目的地的能力。如果攻击者可以使用它所控制的载荷来针对某个易受攻击的服务进行攻击,那么该服务就可能进行一些看上去是由攻击者的对端发起实际却是由攻击者控制的操作。
举例来说,互联网上的跨站请求伪造(详见《CSRF》)漏洞会使得客户端发起携带着认证Cookie(详见《COOKIE》)的请求,使得某个网站能够访问到本应只有另一个网站才能访问的信息和操作。
由于QUIC运行在UDP之上,主要关心的攻击形式是攻击者能够操纵其对端将UDP数据报发向的地址并且能够控制数据包载荷中的部分原始内容。由于QUIC终端发送的许多数据都是受保护的,所以这种攻击包含着对密文的控制。如果攻击者能够使得对端向某个会基于数据报内容进行相应操作的主机发送UDP数据报,那么攻击就会成功。
本节讨论了可能使用QUIC来进行伪造请求攻击的各种方式。
本节还讨论了QUIC终端可以实现但是效果有限的对抗措施。这些抵御方式可以被某个QUIC实现或部署方式单方面实施,而不需要请求伪造攻击的潜在目标采取行动。然而,如果基于UDP的服务没有恰当地对请求进行鉴权认证,那么单靠这些对抗措施会是不够充分的。
因为在第21.5.4章中描述的迁移攻击相当强力并且针对它没有适当的对抗措施,所以QUIC实现应该假定攻击者可以利用它们来生成任意UDP载荷并将其发送至任意目标。QUIC服务器不应该被部署到没有启用传入流量过滤(详见《BCP38》)和存在安全性未经充分加固的UDP终端的网络中。
虽然通常不太可能确保客户端没有和易受攻击的终端处在一起,但是本QUIC版本并不允许服务器发起迁移,从而阻止了对客户端发起虚假迁移攻击。任何将来的允许服务器迁移的扩展必须为伪造攻击定义对抗措施。
21.5.1. 控制终端的途径
QUIC给了攻击者一些影响或操纵其对端的UDP数据报发送目标的机会:
-
初始的连接建立(详见第7章),此时服务器拥有操纵客户端的数据包发送目标的能力——例如,通过填写DNS记录的方式;
-
首选地址(详见第9.6章),此时服务器拥有操纵客户端的数据包发送目标的能力;
-
虚假的连接迁移(详见第9.3.1章),此时客户端具有使用伪造的源地址来操纵服务器后续数据报的发送目标的能力;以及
-
伪造的数据包,这能令服务器发送版本协商数据包(详见第21.5.5章)。
在任一情况下,攻击者都能令其对端向某个受害者发送数据报,无论受害者能不能理解QUIC。也就是说,这些数据包会在对端进行地址验证前就被发送出去;详见第8章。
在数据包经加密的部分之外,QUIC给予了终端一些可选项,用于控制其对端发送的UDP数据报的内容。目标连接ID字段提供了针对对端发送的数据包中位置靠前的字节的直接控制;详见第5.1章。初始数据包中的令牌字段提供了针对初始数据包中其他字节的控制;详见第17.2.2章。
在本QUIC版本中没有任何方法来阻止针对数据包中经加密的部分的间接控制。假定终端能够控制对端所发送的帧内容是很有必要的,尤其是那些传递应用数据的帧,例如流帧。尽管它在一定程度上依赖于应用协议的细节部分,但是在多数协议的使用场景中都有可能出现某种程度的控制能力。只要攻击者能访问到数据包保护密钥,它们就有能力预测对端会如何加密后续数据包。于是,要成功控制数据报内容,只需要攻击者有能力预测数据包号和数据包中的帧大概会出现的位置。
本节假定了我们无法限制攻击者对数据报内容的控制。在后续章节中,抵御手段的重点在于限制数据报会在进行地址验证前就被发送出去的情况,以防止它们被用于进行请求伪造。
21.5.2. 使用客户端初始数据包的请求伪造
假扮成服务器的攻击者可以自由宣称自己所在的IP地址和端口,所以发送自客户端的初始数据包被假定为可以用于此类攻击。在握手中隐含着的地址验证确保了——在建立新连接时——客户端不会向一个不理解QUIC或不愿意接受QUIC连接的目标发送其他类型的数据包。
初始数据包保护(详见《QUIC-TLS》的第5.2章)使得服务器难以操纵由客户端发送的初始数据包内容。客户端会选择一个不可预测的目标连接ID,确保了服务器无法操纵客户端初始数据包中任何经加密的部分。
然而,令牌字段是开放给服务器去控制的,并且它确实允许服务器利用客户端来进行请求伪造攻击。使用由新令牌帧(详见第8.1.3章)提供的令牌成为了在连接建立期间进行请求伪造的唯一途径。
不过,客户端并没有一定要使用新令牌帧的义务。只需要客户端在服务器的地址与发送新令牌帧时的不同的情况下发送空的令牌字段,依赖于令牌字段的请求伪造攻击就可以被避免。
客户端可以在服务器地址变化时避免使用新令牌帧。然而,不使用令牌字段会对性能产生不利影响。服务器可能依赖于新令牌帧以在发送数据时超过三倍限制;详见第8.1章。尤其是,这会影响客户端使用0-RTT来从服务器请求数据的情况。
重试数据包为服务器提供了改变令牌字段的一种途径。在发送了重试数据包后,服务器还可以控制客户端后续初始数据包中的目标连接ID字段。这还有可能实现对初始数据包中经加密的内容的间接控制。然而,有关重试数据包的通信验证了服务器的地址,于是阻止了将后续初始数据包用于请求伪造。
21.5.3. 使用首选地址的请求伪造
21.5.4. 使用虚假迁移的请求伪造
客户端能够在连接迁移中使用伪造的源地址,使得服务器向该地址发送数据报。
服务器后续向这个假地址发送的任何数据包中的目标连接ID字段都能被用于请求伪造。客户端可能还有影响密文的能力。
在进行地址验证前只会向该地址发送探测数据包(详见第9.1章)的服务器让攻击者对数据报中经加密的部分仅持有有限的控制。然而,尤其是对NAT重绑定来说,这会对性能产生不利影响。如果服务器发送了携带着应用数据的帧,那么攻击者就可能控制数据报中的绝大多数内容。
除了在第21.5.6章中描述的通用措施外,本文档没有提供终端能够实现的专门的对抗措施。然而,在网络层面的针对地址伪造的对抗措施——尤其是传入流量过滤(详见BCP38)——对于使用伪造地址且源自外部网络的攻击是极其有效的。
21.5.5. 使用版本协商的请求伪造
21.5.6. 针对请求伪造的通用对抗措施
针对请求伪造攻击的最有效防御方法就是修改易受攻击的服务,让它使用强有力的鉴权认证。然而,这一点并不总是在QUIC部署的控制之下。本节概述了QUIC终端可以单方面采取的一些其他措施。这些额外措施都应该酌情使用,因为根据情况的不同,它们可能干涉或阻止QUIC的正常工作。
通过回环接口提供的服务通常缺乏恰当的鉴权认证。终端可以阻止指向回环地址的连接尝试或迁移。当指向的服务曾在另一个接口上开放过,或者指向的回环地址是由位于非回环地址的服务提供的,那么终端不应该批准指向这个回环地址的连接或迁移。依赖于这些能力的终端可以提供一个可以禁用这些保护的选项。
类似地,终端可以将从全球唯一地址、唯一本地地址(详见《RFC4193》)或非私有地址变更到链路本地地址(详见《RFC4291》)或处于私有使用范围的地址(详见《RFC1918》)的情况视作潜在的对请求伪造的企图。终端可以完全拒绝使用以上地址,但是要承担干涉QUIC合法用途的巨大风险。终端不应该拒绝使用某个地址,除非它对于网络的先验知识告诉它向某范围内的未经验证的地址发送数据报是不安全的。
终端可以选择以不使用来自初始数据包中新令牌帧的值的方式或只有在完成地址验证后才在数据包中发送探测帧的方式来减少请求伪造的风险。注意,这不能阻止攻击者将目标连接ID用于攻击中。
终端不需要专门持有容易成为请求伪造攻击目标的服务器的位置信息。然而,经过一段时间后,识别出某些常常成为攻击目标的UDP端口或被用于进行攻击的数据报中的特定模式是有可能的。终端可以选择避免向那些端口发送数据报或不在验证目标地址前发送符合这种模式的数据包。终端可以撤销符合已知可能产生问题的模式的连接ID而不使用它们。
注意: 修改终端来应用以上保护措施比起部署基于网络的保护措施要更高效,因为终端在向经验证的地址发送数据时不需要进行额外的处理。
21.6. 慢速连接攻击
通常被称为慢速连接攻击(详见《SLOWLORIS》)的攻击方式会尝试打开许多与目标终端的连接并且尽可能长地维持它们。这类攻击可以通过进行最低限度的活动以避免连接因无活动而被关闭的方式攻击某个QUIC终端。其中可能还包含发送少量数据,逐渐打开流量控制窗口从而控制发送者的速率,以及制造能模拟高丢包率的ACK帧。
QUIC部署应该针对慢速连接攻击提供抵御手段,例如扩大服务器允许的最大客户端数量、限制单个IP地址能够创建的连接数量、向连接所允许的最低传输速率施加限制,以及限制终端能够保持连接的时长。
21.7. 流分段与重组攻击
恶意的攻击者可能有意不发送一部分流数据,使得接收者为那些未发送的数据预留资源。这会造成接收方出现极高的缓存占用并且/或者创建出巨大且低效的数据结构。
恶意的接收者可能有意不确认包含流数据的数据包,为的是强制发送方持续存储未确认的流数据以便重传。
当流量控制窗口与可用内存相关时,针对接收方的攻击就能被抵御。然而,一些接收方会过度占用内存,并且在合计的时候宣告会超过实际可用内存的流量控制偏移值。这种透支策略在终端运行状态良好时能够提高性能,但是会在遭受流分段攻击时使得终端变得脆弱。
QUIC实现应该针对流分段攻击提供抵御手段。其中可以包含避免透支内存、限制持续维护的数据结构的尺寸、延迟组装流帧、实现基于数据重组缺口时长的启发式方法,以及以上措施的各种组合。
21.8. 流占用攻击
恶意的终端可以打开大量的流并耗尽终端的状态。恶意的终端可以在大量连接上重复此过程,就像TCP中的SYN泛洪攻击那样。
通常情况下,客户端会按顺序打开流,就像第2.1章中解释的那样。然而,当短时间内发起多个流时,丢包或乱序会使得打开流的流帧被乱序地接收到。在接收到较高编号的流ID时,接收者需要打开同类型的所有低编号流;详见第3.2章。因此,在一条新连接上,打开了编号为4000000
的流就会打开一百万零一条由客户端发起的双向流。
活跃流的数量是由传输参数initial_max_streams_bidi
(初始最大双向流数量)和initial_max_streams_uni
(初始最大单向流数量)限制的,并会随着接收到的最大流帧而被更新,如第4.6章所述。只要审慎地选择,这些限制就能抵御流占用攻击会产生的效果。然而,将限制设置得过低则会在应用希望打开大量流时影响性能。
21.9. 来自对端的拒绝服务攻击
QUIC和TLS中的一些帧和消息在某些情况下具有合理用途,但也可以被滥用,使得终端花费大量计算资源却对连接的状态产生不了肉眼可见的影响。
消息还能被用于以简短快捷的方式更改和回退状态数据,例如向流量控制的限制值发送较小的增量更改。
如果计算上的代价比起带宽上的消耗或对状态的影响要大得多,那么这可能使得恶意的对端能够耗尽终端计算资源。
尽管所有消息都有其合理用途,但是QUIC实现应该监视处理过程中在计算上的消耗并且将任何投入巨大却产出低下的数据包处理过程视作潜在的攻击。终端可以用连接错误或数据包的丢弃来响应这种情况。
21.10. 显式拥塞通知攻击
21.11. 无状态重置泄密
无状态重置会创建一种潜在的拒绝服务攻击,它与TCP重置注入很相似。这种攻击的成功需要攻击者对于连接ID为特定值的连接,具有触发其无状态重置令牌的生成的能力。能够触发终端生成该令牌的攻击者就能重置具有该连接ID的活跃连接。
如果数据包会被路由至共享同一静态密钥的其他实例上——例如,通过改变IP地址或端口——那么攻击者就能令服务器发送无状态重置。要抵御这种形式的拒绝服务攻击,共享用于无状态重置的同一密钥的终端(详见第10.3.2章)必须被妥善安排,从而使具有给定连接ID的数据包总是抵达具有连接状态的那个实例,除非该连接不再处于活跃状态。
更一般地,如果具有相同连接ID的连接在使用相同静态密钥的其他终端上可能处于活跃状态,那么服务器就必须不创建无状态重置。
在服务器集群使用动态负载均衡的情况中,可能出现对负载均衡器配置进行了更改而活跃的实例仍维护着连接状态的现象。即使某个实例维护着连接状态,路由和作为结果的无状态重置的变化会使得连接被终止。如果数据包没有机会被路由到正确的实例上,那么比起等待连接超时,最好发送无状态重置。然而,该做法只有在攻击者无法影响到路由的情况下才能被使用。
21.12. 版本降级
本文档定义了QUIC的版本协商包(详见第6章),它能被用于协商在两个终端间使用的QUIC版本。然而,本文档没有规定在本版本与后续版本之间如何进行协商。尤其是,版本协商数据包中并不包含任何能阻止版本降级攻击的机制。将来的使用版本协商数据包的QUIC版本必须定义一种针对版本降级攻击的健壮的防御机制。
21.13. 通过路由的定向攻击
QUIC部署应该限制攻击者将新连接定向至特定服务器实例的能力。理想情况下,路由决策应该与客户端的相关值无关,包括地址。一旦选定了某个实例,就能决定连接ID,以便将来的数据包都被路由至同一实例。
21.14. 流量分析
QUIC数据包的长度会透露出有关这些数据包内容长度的信息。填充帧给予了终端混淆数据包内容长度的能力;详见第19.1章。
对流量分析的防御是充满挑战的,并且是很多积极研究的主题。长度不是唯一会泄露信息的途径。终端还可能通过其他侧信道泄露敏感信息,例如数据包的计时侧信道。
22. 关于IANA的考量
本文档为QUIC中码点的管理建立了一些注册表。这些注册表遵循在第22.1章中描述的一组通用规范。
22.1. QUIC注册表中注册项的规范
所有QUIC注册表都允许码点以临时或永久的形式注册。本节记述了对这些注册表的通用规范。
22.1.1. 临时注册项
码点的临时注册项是为了支持私有用途和QUIC的实验性扩展。临时注册项中只需要包含码点值和联系方式。然而,临时注册项可以被释放或重新指定为另一用途。
临时注册项需要经过专家评审(Expert Review
),就像《RFC8126》的第4.5章中定义的那样。建议被指派的专家或专家组仅对那些在不在剩余码点空间中的申请,或者对可用码点空间中首个未分配值的申请(详见第22.1.2章)做出拒绝。
临时注册项中将包含日期字段,它表明了该注册项的最后更新时间。对任何临时注册项的日期进行更新的申请不需要经过专家(组)的评审。
为了支持临时注册项,所有QUIC注册表都包含以下字段:
- 值:
-
分配的码点。
- 状态:
-
“永久”或“临时”。
- 规范:
-
指向一份公开可用的针对此值的规范的引用。
- 日期:
-
最后一次更新此注册项的日期。
- 更改责任人:
-
对此注册项的定义负责的实体。
- 联系方式:
-
注册者的联系方式。
- 备注:
-
关于此注册项的补充性备注。
临时注册项中可以省略规范和备注字段,以及任何对永久注册项来说必需的额外字段。日期字段在申请注册时不是必需的,因为它会在创建或更新注册项时被设置上去。
22.1.2. 挑选码点
对来自QUIC注册表中的码点的申请应该使用一个随机选择的码点,并且避开已分配的码点和选定空间中的首个未分配码点。可以使用一个连续的范围来申请复数码点。这使得不同实现将相差过大的语义赋给同一码点的风险能够降至最低。
首个未分配码点是保留码点,对它的分配遵循着标准行为(Standards Action
)流程;详见《RFC8126》的第4.9章。早期码点分配过程(详见《EARLY-ASSIGN》)适用于这些值。
对于以可变长度整型编码(详见第16章)的码点,例如帧类型,应该使用被编码至四字节或八字节(也就是大于等于214
的值)的码点,除非它对编码结果的长度有特殊要求。
在QUIC注册表中注册码点的应用可以申请一个码点并作为注册的一部分。如果这个码点未被分配并且符合注册流程的各项要求,那么IANA必须分配所选码点。
22.1.3. 释放临时码点
可以申请从注册表中移除一条未使用的临时注册项从而腾出注册表中的空间,也可以申请移除注册表的一块区域(例如从64
至16383
的范围,它们是使用了可变长度整型编码的码点)。该申请应该只从具有最早记录日期的码点开始进行,并且一年内曾被更新过的注册项不应该被释放。
对于移除码点的申请必须得到所指派的专家组的评审。专家组必须尝试判断该码点是否仍在使用中。专家组应该联系注册项中列出的联系方式,和尽可能多的协议实现方,从而判断该码点是否仍有使用场景。专家组还应该拥有至少四周的时间用于做出答复。
如果上文中查找使用场景的结果是认定该码点仍在使用,或出现了更新此注册项的申请,那么必须不释放该码点。取而代之的是,注册表项的日期会被更新。可以为此注册项新增一条备注,记录在此过程中了解到的相关信息。
如果没有找到该码点的使用场景,并且没有出现更新此注册项的申请,那么该码点可以被移除出注册表。
这种评审与商讨的过程还适用于将临时注册项变更为永久注册项的申请,只不过目标不是判断有没有该码点的使用场景,而是判断此注册项是否准确地表达了它在某一部署场景中的用途。
22.1.4. 永久注册项
QUIC注册表中的永久注册项遵循的是强制规范(Specification Required
)流程(详见《RFC8126》的第4.6章),除非有特别规定。被指派的专家或专家组验证规范文件是否存在并且具备可访问性。鼓励专家组倾向于批准注册申请,除非申请自身带有辱骂、轻蔑或者其他有害性质(不仅仅是在美学上令人不快或在结构上令人怀疑)。在创建注册项时可以对永久注册项指定额外限制。
注册项的创建可以指定一定范围的码点,使它们受到另一种注册流程的管理。例如,注册表“QUIC帧类型”(详见第22.4章)中从0
至63
的码点受到更严格流程的控制。
对永久注册项的任何更严格的要求都不会影响相关码点上的临时注册项。例如,可以为帧类型61
申请临时注册项。
所有由标准化出版物创建的注册项必须是永久的。
本文档中的所有注册项均被指定为永久状态,更改责任人为IETF,联系方式为QUIC工作组(quic@ietf.org)。
22.2. QUIC版本注册表
22.3. QUIC传输参数注册表
IANA在“QUIC”条目下新增了“QUIC传输参数”注册表。
“QUIC传输参数”注册表管理着一段62位长的空间。该注册表遵循着来自第22.1章的注册流程。该注册表中的永久注册项被指定为使用强制规范流程(详见《RFC8126》的第4.6章),但是从0x00
至0x3f
间(十六进制,包含两端)的值除外,这些值会以标准行为或IESG批准的方式指定,有关定义详见《RFC8126》的第4.9章和第4.10章。
除了在第22.1.1章中列出的字段外,该注册表中的永久注册项还必须包含以下字段:
- 参数名称:
-
该参数简短的帮助记忆的名称。
该注册表的初始内容如表6所示。
值 | 参数名称 | 规范 |
---|---|---|
0x00 | original_destination_connection_id | 第18.2章 |
0x01 | max_idle_timeout | 第18.2章 |
0x02 | stateless_reset_token | 第18.2章 |
0x03 | max_udp_payload_size | 第18.2章 |
0x04 | initial_max_data | 第18.2章 |
0x05 | initial_max_stream_data_bidi_local | 第18.2章 |
0x06 | initial_max_stream_data_bidi_remote | 第18.2章 |
0x07 | initial_max_stream_data_uni | 第18.2章 |
0x08 | initial_max_streams_bidi | 第18.2章 |
0x09 | initial_max_streams_uni | 第18.2章 |
0x0a | ack_delay_exponent | 第18.2章 |
0x0b | max_ack_delay | 第18.2章 |
0x0c | disable_active_migration | 第18.2章 |
0x0d | preferred_address | 第18.2章 |
0x0e | active_connection_id_limit | 第18.2章 |
0x0f | initial_source_connection_id | 第18.2章 |
0x10 | retry_source_connection_id | 第18.2章 |
对于非负整数N
,所有满足形式31 * N + 27
的值(也就是27
、58
、89
……)的使用都被保留;它们必须不被IANA指定,并且必须不被列出在已指定的值中。
22.4. QUIC帧类型注册表
IANA在“QUIC”条目下新增了“QUIC帧类型”注册表。
“QUIC帧类型”注册表管理着一段62位长的空间。该注册表遵循着来自第22.1章的注册流程。该注册表中的永久注册项被指定为使用强制规范流程(详见《RFC8126》的第4.6章),但是从0x00
至0x3f
间(十六进制,包含两端)的值除外,这些值会以标准行为或IESG批准的方式指定,有关定义详见《RFC8126》的第4.9章和第4.10章。
除了在第22.1.1章中列出的字段外,该注册表中的永久注册项还必须包含以下字段:
- 帧类型名称:
-
该参数简短的帮助记忆的名称。
除了在第22.1章中给出的建议外,新的永久注册项的规范应该提供一份描述,描述终端可以用于判断是否能够发送该类型的帧的方法。对绝大多数注册项来说,还应该同时注册相关的传输参数;详见第22.3章。永久注册项的规范还需要描述该类型的帧中所有字段的格式及其语义。
22.5. QUIC传输层错误码注册表
IANA在“QUIC”条目下新增了“QUIC传输层错误码”注册表。
“QUIC传输层错误码”注册表管理着一段62位长的空间。该空间被拆分为三段由不同注册流程管理的空间。该注册表遵循着来自第22.1章的注册流程。该注册表中的永久注册项被指定为使用强制规范流程(详见《RFC8126》的第4.6章),但是从0x00
至0x3f
间(十六进制,包含两端)的值除外,这些值会以标准行为或IESG批准的方式指定,有关定义详见《RFC8126》的第4.9章和第4.10章。
除了在第22.1.1章中列出的字段外,该注册表中的永久注册项还必须包含以下字段:
- 代码:
-
该参数简短的帮助记忆的名称。
- 描述:
-
对该错误码语义的简要描述,当提供了指向规范文件的引用时,此内容可以是一份概述。
该注册表的初始内容如表7所示。
值 | 代码 | 描述 | 规范 |
---|---|---|---|
0x00 | NO_ERROR | 无错误 | 第20章 |
0x01 | INTERNAL_ERROR | 实现中的错误 | 第20章 |
0x02 | CONNECTION_REFUSED | 服务器拒绝了连接 | 第20章 |
0x03 | FLOW_CONTROL_ERROR | 流量控制错误 | 第20章 |
0x04 | STREAM_LIMIT_ERROR | 打开了过多流 | 第20章 |
0x05 | STREAM_STATE_ERROR | 接收到了在当前流状态下非法的帧 | 第20章 |
0x06 | FINAL_SIZE_ERROR | 最终尺寸发生了变化 | 第20章 |
0x07 | FRAME_ENCODING_ERROR | 帧编码错误 | 第20章 |
0x08 | TRANSPORT_PARAMETER_ERROR | 传输参数中出现错误 | 第20章 |
0x09 | CONNECTION_ID_LIMIT_ERROR | 接收到了过多的连接ID | 第20章 |
0x0a | PROTOCOL_VIOLATION | 通用的协议违背错误 | 第20章 |
0x0b | INVALID_TOKEN | 接收到了非法的令牌 | 第20章 |
0x0c | APPLICATION_ERROR | 应用错误 | 第20章 |
0x0d | CRYPTO_BUFFER_EXCEEDED | 加密数据缓存溢出 | 第20章 |
0x0e | KEY_UPDATE_ERROR | 对数据包保护密钥的非法更新 | 第20章 |
0x0f | AEAD_LIMIT_REACHED | 对数据包保护密钥的超量使用 | 第20章 |
0x10 | NO_VIABLE_PATH | 无可用网络路径 | 第20章 |
0x0100- 0x01ff |
CRYPTO_ERROR | TLS警告码 | 第20章 |
23. 参考文献
23.1. 规范性参考文献
“Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing”, BCP 38, RFC 2827, May 2000. https://www.rfc-editor.org/info/bcp38
“Packetization Layer Path MTU Discovery for Datagram Transports”, RFC 8899, DOI 10.17487/RFC8899, September 2020, https://www.rfc-editor.org/info/rfc8899.
“Early IANA Allocation of Standards Track Code Points”, BCP 100, RFC 7120, DOI 10.17487/RFC7120, January 2014, https://www.rfc-editor.org/info/rfc7120.
“Internet Protocol”, STD 5, RFC 791, DOI 10.17487/RFC0791, September 1981, https://www.rfc-editor.org/info/rfc791.
“Version-Independent Properties of QUIC”, RFC 8999, DOI 10.17487/RFC8999, May 2021, https://www.rfc-editor.org/info/rfc8999.
“QUIC Loss Detection and Congestion Control”, RFC 9002, DOI 10.17487/RFC9002, May 2021, https://www.rfc-editor.org/info/rfc9002.
“Using TLS to Secure QUIC”, RFC 9001, DOI 10.17487/RFC9001, May 2021, https://www.rfc-editor.org/info/rfc9001.
Mogul, J. and S. Deering, “Path MTU discovery”, RFC 1191, DOI 10.17487/RFC1191, November 1990, https://www.rfc-editor.org/info/rfc1191.
[RFC2119] RFC文档中用于指出要求级别的关键字
Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels”, BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, https://www.rfc-editor.org/info/rfc2119.
Ramakrishnan, K., Floyd, S., and D. Black, “The Addition of Explicit Congestion Notification (ECN) to IP”, RFC 3168, DOI 10.17487/RFC3168, September 2001, https://www.rfc-editor.org/info/rfc3168.
Yergeau, F., “UTF-8, a transformation format of ISO 10646”, STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, https://www.rfc-editor.org/info/rfc3629.
Amante, S., Carpenter, B., Jiang, S., and J. Rajahalme, “IPv6 Flow Label Specification”, RFC 6437, DOI 10.17487/RFC6437, November 2011, https://www.rfc-editor.org/info/rfc6437.
Eggert, L., Fairhurst, G., and G. Shepherd, “UDP Usage Guidelines”, BCP 145, RFC 8085, DOI 10.17487/RFC8085, March 2017, https://www.rfc-editor.org/info/rfc8085.
Cotton, M., Leiba, B., and T. Narten, “Guidelines for Writing an IANA Considerations Section in RFCs”, BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017, https://www.rfc-editor.org/info/rfc8126.
[RFC8174] RFC2119中关键字大写与小写的歧义
Leiba, B., “Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words”, BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, https://www.rfc-editor.org/info/rfc8174.
McCann, J., Deering, S., Mogul, J., and R. Hinden, Ed., “Path MTU Discovery for IP version 6”, STD 87, RFC 8201, DOI 10.17487/RFC8201, July 2017, https://www.rfc-editor.org/info/rfc8201.
Black, D., “Relaxing Restrictions on Explicit Congestion Notification (ECN) Experimentation”, RFC 8311, DOI 10.17487/RFC8311, January 2018, https://www.rfc-editor.org/info/rfc8311.
Rescorla, E., “The Transport Layer Security (TLS) Protocol Version 1.3”, RFC 8446, DOI 10.17487/RFC8446, August 2018, https://www.rfc-editor.org/info/rfc8446.
Postel, J., “User Datagram Protocol”, STD 6, RFC 768, DOI 10.17487/RFC0768, August 1980, https://www.rfc-editor.org/info/rfc768.
23.2. 资料性参考文献
McGrew, D., “An Interface and Algorithms for Authenticated Encryption”, RFC 5116, DOI 10.17487/RFC5116, January 2008, https://www.rfc-editor.org/info/rfc5116.
Friedl, S., Popov, A., Langley, A., and E. Stephan, “Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension”, RFC 7301, DOI 10.17487/RFC7301, July 2014, https://www.rfc-editor.org/info/rfc7301.
Nottingham, M., McManus, P., and J. Reschke, “HTTP Alternative Services”, RFC 7838, DOI 10.17487/RFC7838, April 2016, https://www.rfc-editor.org/info/rfc7838.
Barth, A., “HTTP State Management Mechanism”, RFC 6265, DOI 10.17487/RFC6265, April 2011, https://www.rfc-editor.org/info/rfc6265.
Barth, A., Jackson, C., and J. Mitchell, “Robust defenses for cross-site request forgery”, Proceedings of the 15th ACM conference on Computer and communications security - CCS ‘08, DOI 10.1145/1455770.1455782, 2008, https://doi.org/10.1145/1455770.1455782.
Roskind, J., “QUIC: Multiplexed Stream Transport Over UDP”, 2 December 2013, https://docs.google.com/document/d/1RNHkx_VvKWyWg6Lr8SZ-saqsQx7rFV-ev2jRFUoVD34/edit?usp=sharing.
Hätönen, S., Nyrhinen, A., Eggert, L., Strowes, S., Sarolahti, P., and M. Kojo, “An experimental study of home gateway characteristics”, Proceedings of the 10th ACM SIGCOMM conference on Internet measurement - IMC ‘10, DOI 10.1145/1879141.1879174, November 2010, https://doi.org/10.1145/1879141.1879174.
Belshe, M., Peon, R., and M. Thomson, Ed., “Hypertext Transfer Protocol Version 2 (HTTP/2)”, RFC 7540, DOI 10.17487/RFC7540, May 2015, https://www.rfc-editor.org/info/rfc7540.
Deering, S. and R. Hinden, “Internet Protocol, Version 6 (IPv6) Specification”, STD 86, RFC 8200, DOI 10.17487/RFC8200, July 2017, https://www.rfc-editor.org/info/rfc8200.
Kuehlewind, M. and B. Trammell, “Manageability of the QUIC Transport Protocol”, Work in Progress, Internet-Draft, draft-ietf-quic-manageability-11, 21 April 2021, https://tools.ietf.org/html/draft-ietf-quic-manageability-11.
Eastlake 3rd, D., Schiller, J., and S. Crocker, “Randomness Requirements for Security”, BCP 106, RFC 4086, DOI 10.17487/RFC4086, June 2005, https://www.rfc-editor.org/info/rfc4086.
Baker, F., Ed., “Requirements for IP Version 4 Routers”, RFC 1812, DOI 10.17487/RFC1812, June 1995, https://www.rfc-editor.org/info/rfc1812.
Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., and E. Lear, “Address Allocation for Private Internets”, BCP 5, RFC 1918, DOI 10.17487/RFC1918, February 1996, https://www.rfc-editor.org/info/rfc1918.
Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, “TCP Selective Acknowledgment Options”, RFC 2018, DOI 10.17487/RFC2018, October 1996, https://www.rfc-editor.org/info/rfc2018.
Krawczyk, H., Bellare, M., and R. Canetti, “HMAC: Keyed-Hashing for Message Authentication”, RFC 2104, DOI 10.17487/RFC2104, February 1997, https://www.rfc-editor.org/info/rfc2104.
Balakrishnan, H., Padmanabhan, V., Fairhurst, G., and M. Sooriyabandara, “TCP Performance Implications of Network Path Asymmetry”, BCP 69, RFC 3449, DOI 10.17487/RFC3449, December 2002, https://www.rfc-editor.org/info/rfc3449.
Hinden, R. and B. Haberman, “Unique Local IPv6 Unicast Addresses”, RFC 4193, DOI 10.17487/RFC4193, October 2005, https://www.rfc-editor.org/info/rfc4193.
Hinden, R. and S. Deering, “IP Version 6 Addressing Architecture”, RFC 4291, DOI 10.17487/RFC4291, February 2006, https://www.rfc-editor.org/info/rfc4291.
Conta, A., Deering, S., and M. Gupta, Ed., “Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification”, STD 89, RFC 4443, DOI 10.17487/RFC4443, March 2006, https://www.rfc-editor.org/info/rfc4443.
Audet, F., Ed. and C. Jennings, “Network Address Translation (NAT) Behavioral Requirements for Unicast UDP”, BCP 127, RFC 4787, DOI 10.17487/RFC4787, January 2007, https://www.rfc-editor.org/info/rfc4787.
Allman, M., Paxson, V., and E. Blanton, “TCP Congestion Control”, RFC 5681, DOI 10.17487/RFC5681, September 2009, https://www.rfc-editor.org/info/rfc5681.
Krawczyk, H. and P. Eronen, “HMAC-based Extract-and-Expand Key Derivation Function (HKDF)”, RFC 5869, DOI 10.17487/RFC5869, May 2010, https://www.rfc-editor.org/info/rfc5869.
Petit-Huguenin, M. and G. Salgueiro, “Multiplexing Scheme Updates for Secure Real-time Transport Protocol (SRTP) Extension for Datagram Transport Layer Security (DTLS)”, RFC 7983, DOI 10.17487/RFC7983, September 2016, https://www.rfc-editor.org/info/rfc7983.
Fairhurst, G. and M. Welzl, “The Benefits of Using Explicit Congestion Notification (ECN)”, RFC 8087, DOI 10.17487/RFC8087, March 2017, https://www.rfc-editor.org/info/rfc8087.
Gont, F., Krishnan, S., Narten, T., and R. Draves, “Temporary Address Extensions for Stateless Address Autoconfiguration in IPv6”, RFC 8981, DOI 10.17487/RFC8981, February 2021, https://www.rfc-editor.org/info/rfc8981.
Rescorla, E. and B. Korver, “Guidelines for Writing RFC Text on Security Considerations”, BCP 72, RFC 3552, DOI 10.17487/RFC3552, July 2003, https://www.rfc-editor.org/info/rfc3552.
“RSnake” Hansen, R., “Welcome to Slowloris - the low bandwidth, yet greedy and poisonous HTTP client!”, June 2009, https://web.archive.org/web/20150315054838/http://ha.ckers.org/slowloris/.
附录A. 伪代码
本章的伪代码描述了一些算法样例。这些算法有意写得准确且清晰,没有为了性能而优化。
本章中的伪代码片段以代码组件的形式受到权利保护;详见版权声明。
A.1. 可变长度整型解码样例
图45中的伪代码展示了如何从字节流中读取可变长度整型值。ReadVarint
函数接收单个参数——一个字节序列,它将以网络字节序被读取。
ReadVarint(data):
// 可变长度整型值的长度被编码在首个字节的前两个比特位中。
v = data.next_byte()
prefix = v >> 6
length = 1 << prefix
// 一旦长度已知,就移除这些比特位,并读取剩余字节。
v = v & 0x3f
repeat length-1 times:
v = (v << 8) + data.next_byte()
return v
举例来说,八字节序列0xc2197c5eff14e88c
会被解码为十进制值151,288,809,941,952,652
;四字节序列0x9d7f3e7d
会被解码为494,878,333
;双字节序列0x7bbd
会被解码为15,293
;而单字节0x25
会被解码为37
(和解码双字节序列0x4025
的结果一致)。
A.2. 数据包号编码算法样例
图46中的伪代码展示了QUIC实现怎样选择合适长度的数据包号编码。
EncodePacketNumber
函数接收两个参数:
-
full_pn
是正在发送的数据包的完整数据包号。 -
largest_acked
是当前数据包号空间中已被对端确认的最大数据包号,如果有的话。
EncodePacketNumber(full_pn, largest_acked):
// 比特位的数量必须至少比连续未被确认的数据包号的数量(包括此数据包本身)的
// 以`2`为底的对数值大`1`
if largest_acked is None:
num_unacked = full_pn + 1
else:
num_unacked = full_pn - largest_acked
min_bits = log(num_unacked, 2) + 1
num_bytes = ceil(min_bits / 8)
// 将整型值编码,截断为仅剩最低`num_bytes`个字节
return encode(full_pn, num_bytes)
举例来说,如果终端接收到了对于数据包0xabe8b3
的确认,并且正在发送数据包号为0xac5c02
的数据包,那么就存在着29,519
(0x734f
)个未确认的数据包号。为了能够至少表示这个数量的两倍大小(59,038
个,或者说0xe69e
个数据包),就需要16个比特位。
在相同的状态下,发送数据包号为0xace8fe
的数据包会使用长度为24比特位的编码方式,因为至少需要18个比特位才能表示缺口数量的两倍(131,222
个,或者说0x020096
个数据包)。
A.3. 数据包号解码算法样例
图47中的伪代码包含了在移除头部保护后解码数据包号的算法样例。
DecodePacketNumber
函数接收三个参数:
-
largest_pn
是当前数据包号空间中已成功处理的最大数据包号 -
truncated_pn
是数据包号字段的值 -
pn_nbits
是数据包号字段中比特位的数量(8
、16
、24
或32
)。
DecodePacketNumber(largest_pn, truncated_pn, pn_nbits):
expected_pn = largest_pn + 1
pn_win = 1 << pn_nbits
pn_hwin = pn_win / 2
pn_mask = pn_win - 1
// 传入数据包号应该大于`expected_pn - pn_hwin`且小于等于
// `expected_pn + pn_hwin`
//
// 这意味着我们不能简单地去掉`expected_pn`中末尾的比特位再加上`truncated_pn`
// 因为那样会产生一个超过窗口范围的值。
//
// 接下来的代码计算了一个候选值,并确保它处于数据包号窗口范围中。
// 注意用于防止数值过大和数值过小的额外检查。
candidate_pn = (expected_pn & ~pn_mask) | truncated_pn
if candidate_pn <= expected_pn - pn_hwin and
candidate_pn < (1 << 62) - pn_win:
return candidate_pn + pn_win
if candidate_pn > expected_pn + pn_hwin and
candidate_pn >= pn_win:
return candidate_pn - pn_win
return candidate_pn
举例来说,如果已成功认证数据包的数据包号中,最大值为0xa82f30ea
,那么值为0x9b32
的16比特长的数据包号字段会被解码为0xa82f9b32
。
A.4. ECN验证算法样例
每次终端开始在新的网络路径上发送时,它都要判断该路径是否支持ECN;详见第13.4章。如果该路径支持ECN,那么就要将ECN利用起来。终端还可以每隔一段时间重新判断一次被认为不支持ECN的路径。
本节描述了一种用于测试新路径的方法。本算法的意图是展示如何测试一条路径是否支持ECN。终端可以用其他方法实现。
受测试的路径会被指定一种ECN状态,它会是“测试中”、“未知”、“失败”和“支持”中的一种。在状态为“测试中”或“支持”的路径上,终端发送的数据包会带有ECT
标记——默认是ECT(0)
;否则,终端发送的数据包上不带标记。
要启动对一条路径的测试,其ECN状态会被置为“测试中”,并且当前的ECN计数被记录为基准值。
测试期间会持续发送数个数据包,或者持续一段指定的时间,这由终端决定。此处的目标不是要限制测试的时长,而是确保发送了足够多经标记的数据包以使得接收到的ECN计数能够清晰地表明该路径会怎样对待经标记的数据包。第13.4.2章建议将此时长限制为发送10个数据包的耗时或PTO的三倍大小。
在测试期结束时,该路径的ECN状态会转为“未知”。对ACK帧中ECN计数的成功验证会使得该路径的状态从“未知”转为“支持”,除非没有经标记的数据包得到确认。
一旦对ECN计数的验证失败,那么相关路径的ECN状态就会转为“失败”。终端还可以在经标记的数据包全部被认定为丢包或全部被标记上ECN-CE
时将ECN状态置为“失败”。
按照本算法,能够确保在正确支持ECN的路径上ECN几乎不会被禁用。任何错误地修改了标记的路径都会使得ECN被禁用。对于那些经标记数据包被路径丢弃的罕见情况,短暂的测试期能够限制被丢弃数据包的数量。
贡献者
本协议背后的原始设计和基本原理很大程度上来自Jim Roskind的工作(详见《EARLY-DESIGN》)。
IETF QUIC工作组接收到了来自许多人员的大量支持。以下人员对本文档做出了重要贡献:
-
Alessandro Ghedini
-
Alyssa Wilk
-
Antoine Delignat-Lavaud
-
Brian Trammell
-
Christian Huitema
-
Colin Perkins
-
David Schinazi
-
Dmitri Tikhonov
-
Eric Kinnear
-
Eric Rescorla
-
Gorry Fairhurst
-
Ian Swett
-
Igor Lubashev
-
奥 一穂 (Kazuho Oku)
-
Lars Eggert
-
Lucas Pardue
-
Magnus Westerlund
-
Marten Seemann
-
Martin Duke
-
Mike Bishop
-
Mikkel Fahnøe Jørgensen
-
Mirja Kühlewind
-
Nick Banks
-
Nick Harper
-
Patrick McManus
-
Roberto Peon
-
Ryan Hamilton
-
Subodh Iyengar
-
Tatsuhiro Tsujikawa
-
Ted Hardie
-
Tom Jones
-
Victor Vasiliev
联系作者
Jana Iyengar (编辑)
Fastly
Email: jri.ietf@gmail.com
Martin Thomson (编辑)
Mozilla
Email: mt@lowentropy.net
译
-
- Email: yunzhe@zju.edu.cn
-
- Email: fangqiuhang@163.com