QUIC 基于UDP的多路复用和安全传输 (draft-ietf-quic-transport-24)

QUIC: 基于UDP的多路复用和安全传输 (draft-ietf-quic-transport-24)

1. 介绍

QUIC是一种多路复用的安全传输为目标的协议。提供如下特性:

  • 复用Stream
  • Stream和连接级别的流量控制
  • 低延迟连接建立
  • 连接迁移和NAT重绑定的快速恢复
  • 认证且加密的头部和负载(payload)
    QUIC使用UDP作为TCP的替代,来避免对旧客户端操作系统和中间件的改动要求。QUIC对所有头部进行认证,并且对大部分交换的数据进行加密,包括信令,来避免中间件引入额外的依赖。

2. Streams(流)

QUIC中的流,为应用提供一种轻量的,有序字节流抽象。流可以是单向或双向的。QUIC单向流几乎可以看做无限长度的“消息”的概念。

流可以通过发送数据创建。其他与流管理相关的操作,结束(ending),取消(cancelling)和流量控制,都是以最小化负荷为目的设计的。比如,单个STREAM帧可以打开、携带数据以及关闭一个流。流也可以长期存在于整个连接期间。

流可以被任意端创建,可以与其他流并发地发送数据,也可以被取消。QUIC不会不确保不同流之间的字节数据的有序性。

在流量控制约束和流限制下,QUIC可以并发地处理任意数量的流,任意数量的数据。

2.1 流的类型和id

流可以是单向或双向的。单向流向一个方向承载数据:从流的发起者到对端。双向流允许两个方向发送数据。
一个连接中的流都被数字标记,也就是流ID(stream ID),流ID是一个62位整数(0 ~ 2^62-1),且连接中的所有流ID都是唯一的。流ID是可变长度的整数。在一条连接中,QUIC端不能复用流ID。
流ID的最低有效位(0x1)表明了流的发起者。客户端发起的流都是偶数ID(该位设为0),服务端发起的则为奇数ID(该位设为1)
导数第二低有效位(0x2)可以区分是双向流(该位设为0)还是单向流(该位设为1)
可以用下表概括:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------+----------------------------------+
| Bits | Stream Type |
+------+----------------------------------+
| 0x0 | Client-Initiated, Bidirectional |
| | |
| 0x1 | Server-Initiated, Bidirectional |
| | |
| 0x2 | Client-Initiated, Unidirectional |
| | |
| 0x3 | Server-Initiated, Unidirectional |
+------+----------------------------------+

表1: 流ID类型

每种流类型,流ID都会随着流创建递增。乱序使用的流ID会导致所有具有低编号的流ID被发起。
第一个由客户端发起的双向流,其ID为0。

2.2 发送和接收数据

STREAM帧(19.8节)封装了被应用发送的数据。一端使用流ID和STREAM帧的偏移量字段来有序替换数据。
端点必须能够以有序字节流的方式给应用发送流数据。发送有序字节流要求端点缓冲任何乱序接收到的数据,这取决于声明的流量控制限制。
QUIC协议没有明确规定无序发送流数据。但是,协议实现,可以选择为接收应用提供乱序发送数据的能力。
一端可以多次接收相同流偏移的流数据。数据被接收后可以被丢弃。如果被发送多次,数据的流偏移不能被改变。一端可以把相同流偏移下的接收到不同数据视为PROTOCOL_VIOLATION类型的连接错误。
对于QUIC,流是一个有序字节流的抽象,没有其他可见结构。当数据传输,数据包丢失后重传数据或者数据在接收器传送到应用程序时,预计不会保留STREAM帧边界。
如果没有确保在对端设置的流量控制限制范围内,端点不能在任何流上发送数据。流量控制在第4章详细描述。

2.3 流优先级

如果分配给流的资源被正确地优先化,流复用可以对应用的性能产生显著的影响。
QUIC协议不提供交换优先级信息的机制。反而是依赖于从使用QUIC的应用接收优先级信息。
一个QUIC协议的实现,应该提供应用可以指定流的相对优先级的方式。在决定将资源发送到哪个流时,协议实现应该应用提供的信息。

2.4 需要的流操作

当处理QUIC流时,有一些应用必须能够执行的操作。文档并不指定API,但任何本版本QUIC的实现必须提供QUIC流操作的能力,如下面章节所述:
在流的发送部分,应用协议需要做到:

  • 写数据,当流的流量控制数值被成功保存时,能发送写入的数据
  • 结束流(清除终端),这导致STREAM帧的FIN位被设置,同时
  • 重置(reset)流(打断终端),如果流还为处于结束状态,将导致RESET_STREAM帧发送

在流的接收部分,应用协议需要做到:

  • 读数据;并且
  • 中止流和请求的读取,可能导致STOP_SENDING帧的发送(19.5节)

应用也需要被通知流的状态变化,包括对端打开或者重置一个流,对端中止读流,新数据可用,以及由于流量控制数据是否可以被写入流

3. 流的状态

本节从发送和接收两方面来描述流。描述了两个状态机:一个用于端点传输数据的流(3.1节),两一个用于端点接收数据的流(3.2节)

单向流直接使用其中一个合适的状态机。双向流则使用两种状态机。无论流是单向还是双向的,状态机的使用大部分情况下是相同。对于双向流,打开一个流的条件稍微更复杂一些,因为发送侧或接收侧的打开都会导致流在两个方向上打开。

一个端点必须以递增的流ID来打开相同类型的流。

注意:这些状态主要是提供信息。本文档使用流状态来描述何时以及如何发送不同类型的帧以及在接收到不同类型的帧时预期的反应的规则。虽然这些状态机是为了用于实现QUIC,但这些状态并非旨在约束实现。实现可以定义不同的状态机,只要其行为与实现这些状态的实现一致即可。

3.1 发送中的流状态

图1展示流向对端发送数据的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
          o
| Create Stream (Sending)
| Peer Creates Bidirectional Stream
v
+-------+
| Ready | Send RESET_STREAM
| |-----------------------.
+-------+ |
| |
| Send STREAM / |
| STREAM_DATA_BLOCKED |
| |
| Peer Creates |
| Bidirectional Stream |
v |
+-------+ |
| Send | Send RESET_STREAM |
| |---------------------->|
+-------+ |
| |
| Send STREAM + FIN |
v v
+-------+ +-------+
| Data | Send RESET_STREAM | Reset |
| Sent |------------------>| Sent |
+-------+ +-------+
| |
| Recv All ACKs | Recv ACK
v v
+-------+ +-------+
| Data | | Reset |
| Recvd | | Recvd |
+-------+ +-------+

图1: 流发送部分的状态

端点初始化流发送的状态(客户端为类型0和2,服务端为1和3)是由应用发起(打开)的。“准备”状态表示一个新的被创建的流可以从应用接收数据。在这个状态,流数据可以被缓冲来为发送做好准备。

发送第一个STREAM帧或STREAM_DATA_BLOCKED帧导致流进入“发送”状态。协议实现可选择直到流发送第一个STREAM帧并且进入该状态时,再分配流ID,以便更好的流优先级化。

双向流的发送部分由对端初始化(服务端类型为0,客户端类型为1)并 进入“准备”状态,然后如果接收部分进入“接收”状态(3.2节)马上转换到“发送”状态

在“发送”状态,端点通过STREAM帧传输(和重传如果有必要)流数据。端点会遵守对端设定流量控制限制,并继续接收和处理MAX_STREAM_DATA帧。处于“发送”状态的端点,如果因为流或者连接的流量控制限制被阻塞发送,端点将生成STREAM_DATA_BLOCKED帧。

在应用表明所有流数据已经被发送,并且包含FIN位STREAM帧被发送之后,流的发送部分进入“数据已发送(Data Sent)”状态。从这个状态开始,端点只会根据需要重传流数据。在这个状态,端点不需要检查流量控制限制或者发送STREAM_DATA_BLOCKED帧。MAX_STREAM_DATA帧可能会被接收到,直到对端接收到最终的流偏移(stream offset)。端点可以安全地忽略任何接收到来自对端的MAX_STREAM_DATA帧。

一旦所有流数据被成功确认,流的发送部分将进入“数据已接收(Data Recvd)状态”,这是一个结束状态。

在任何“准备”,“发送”或“数据已发送”状态中,应用可以通知其希望能放弃流数据的传输。或者,端点可以接收来自对端的STOP_SENDING帧。在任何一种情况下,端点都会发送RESET_STREAM帧,这会导致流进入“重置已发送(Reset Sent)”状态。

端点可以发送RESET_STREAM帧作为标识流的第一帧;这导致流的发送部分打开然后马上转换到“重置已发送”状态。

一旦包含RESET_STREAM帧的数据包被确认,流的发送部分将进入“重置已接收(Reset Sent)”状态,这是一个结束状态。

3.2 接收中的流状态

图2展示了从对端接收数据的流状态。流的接收部分的状态只镜像对等于流的发送部分的一些状态。流的接收部分不跟踪发送部分上无法观察到的状态,例如“就绪”状态。相反,流的接收部分跟踪向应用程序传
送数据,其中一些数据不能被发送方观察到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    o
| Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM
| Create Bidirectional Stream (Sending)
| Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional)
| Create Higher-Numbered Stream
v
+-------+
| Recv | Recv RESET_STREAM
| |-----------------------.
+-------+ |
| |
| Recv STREAM + FIN |
v |
+-------+ |
| Size | Recv RESET_STREAM |
| Known |---------------------->|
+-------+ |
| |
| Recv All Data |
v v
+-------+ Recv RESET_STREAM +-------+
| Data |--- (optional) --->| Reset |
| Recvd | Recv All Data | Recvd |
+-------+<-- (optional) ----+-------+
| |
| App Read All Data | App Read RST
v v
+-------+ +-------+
| Data | | Reset |
| Read | | Read |
+-------+ +-------+

图2: 流发送部分的状态

流的接收部分由对端初始化(客户端为类型1和3,服务端为类型0和2),当收到第一个STREAM,STREAM_DATA_BLOCKED,RESET_STREAM帧时被创建。对于对端初始化的双向流,接收到MAX_STREAM_DATA和STOP_SENDING帧同样会创建流的接收部分。接收部分的初始的状态为“接收(Recv)”

当双向流的发送部分被端点初始化(客户端类型0,服务端类型1)进入“准备”状态,流的接收部分进入“接收”状态。

当从对端接收到MAX_STREAM_DATA和STOP_SENDING帧,端点打开一个双向流。对于未打开的流,接收MAX_STREAM_DATA帧,表明远端已经打开流并提供了流量控制信息(数值)。接收STOP_SENDING帧表明远端再也不希望在流上接收数据。如果数据包丢失或者重排序,这两种帧都可以在STREAM或STREAM_DATA_BLOCKED帧之前到达。

在流被创建之前,所有带有低数值流ID的同类型的流必须被创建。这保证了两端的创建顺序的一致性。

在“接收”状态,端点接收STREAM帧和STREAM_DATA_BLOCKED帧。传入的数据被缓冲并且可以被重新组装为正确顺序传递给应用程序。一旦数据被应用程序消费且缓冲空间变成可用时,端点发送MAX_STREAM_DATA帧允许对端发送更多数据。

当带有FIN位的STREAM帧被接收,可以知道流的最终大小了(见4.4节)。流的接收部分然后进入“大小一致”状态。在这个状态,端点不再需要发送MAX_STREAM_DATA帧,只接收任何重传的流数据。

一旦所有的流数据被接收,接收部分进入“数据已接收(Data Recvd)”状态。这种状态可能是由于接收到与“已知大小”的相同STREAM帧
而发生的。在所有数据被接收后,任何STREAM或STREAM_DATA_BLOCKED帧会被丢弃。

“数据已接收”状态会持续到流数据已经传递给应用程序。一旦流数据被传递,流会进入“数据读取”状态,这是一个结束状态。

在“接收”或“大小已知”状态接收RESET_STREAM帧会导致流进入“重置已接收(Reset Recvd)”状态。这会中断给应用程序的数据传递。

有可能当接收到RESET_STREAM帧时,所有流数据都接收到了(也就是,从“数据已接收”状态)。类似地,有可能接收到RESET_STREAM帧时(即“重置已接收”状态),还有剩余流数据未到达。协议实现可以自由管理这些场景。

发送RESET_STREAM帧意味着端点不再保证流数据的发送;但是没有要求收到RESET_STREAM帧时,流数据不能传递给应用。协议实现可以中断流数据的传递,丢弃任何没有被消费的数据,并且发送RESET_STREAM帧收到的通知。如果流数据被完全接收并被缓冲来让应用程序读取,RESET_STREAM帧的信号通知可以被压制(suppressed),或者保留(withheld)。如果RESET_STREAM被压制,流的接收部分仍然处于“数据已接收”状态。

一旦应用程序收到流被重置的通知,流的接收部分会转义到“重置读取(Reset Read)”状态,这是一个结束状态。

3.3 允许的帧类型

流的发送方值发送三种类型的帧,来影响流发送方和接收方的状态:STREAM帧(19.8节),STREAM_DATA_BLOCKED帧(19.13节)和RESET_STREAM帧(19.4节)

发送方不能在结束状态(数据已接收状态和重置已接收状态)发送任何这些帧。在发送RESET_STREAM帧之后,也就是在结束状态和重置已发送状态,不能发送STREAM帧或STREAM_BLOCKED帧。接收方可以在任何状态接收任何这三种类型帧,因为有携带它们的数据包发送延迟的可能。

流的接收方发送MAX_STREAM_DATA帧(19.10节)和STOP_SENDING帧(19.5帧).

接收方只能在“接收”状态发送MAX_STREAM_DATA。接收方可以在任何没有收到RESET_STREAM帧的状态(即除“重置已发送”和“重置已读”状态外的状态)发送STOP_SENDING帧。但是
在“数据已接收”状态发送STOP_SENDING帧几乎没有价值,因为已收到所有流数据。由于数据包的延迟传送,发送方可以在任何状态下接
收这两个帧中的任何一个。

3.4 双向流状态

双向流是由发送和接收部分组成。协议实现可以将发送和接收的流状态组合来表示双向流的状态。当发送或接收部分处于非结束状态时,最简单的模型将流呈现为“打开(open)”,而当发送和接收流都处于终端状态时,呈现为“关闭(closed)”。

表2展示了更复杂的双向流状态映射关系,大致对应了HTTP/2的流状态。这表明了流的发送和接收部分的多个状态被映射为相同的复合状态。注意着只是这种映射的一种可能;这种映射要求在转换到“关闭”或“半关闭”状态之前,数据是被确认的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

+-----------------------+---------------------+---------------------+
| Sending Part | Receiving Part | Composite State |
+-----------------------+---------------------+---------------------+
| No Stream/Ready | No Stream/Recv *1 | idle |
| | | |
| Ready/Send/Data Sent | Recv/Size Known | open |
| | | |
| Ready/Send/Data Sent | Data Recvd/Data | half-closed |
| | Read | (remote) |
| | | |
| Ready/Send/Data Sent | Reset Recvd/Reset | half-closed |
| | Read | (remote) |
| | | |
| Data Recvd | Recv/Size Known | half-closed (local) |
| | | |
| Reset Sent/Reset | Recv/Size Known | half-closed (local) |
| Recvd | | |
| | | |
| Reset Sent/Reset | Data Recvd/Data | closed |
| Recvd | Read | |
| | | |
| Reset Sent/Reset | Reset Recvd/Reset | closed |
| Recvd | Read | |
| | | |
| Data Recvd | Data Recvd/Data | closed |
| | Read | |
| | | |
| Data Recvd | Reset Recvd/Reset | closed |
| | Read | |
+-----------------------+---------------------+---------------------+
表2: 映射到HTTP/2的流状态所有可能的情况

如果流还没有被创建,或者如果流的接受部分处于“接收”状态但还没收到任何帧,则会被认为是“空闲(idle)”状态,

3.5 征求状态过渡(Solicited State Transitions)

如果应用程序对它在流上接收的数据不再感兴趣,它可以中止读取流并指定一个应用错误码。

如果流处于“接收”或“大小已知”状态,通道应该朝相反方向,通过发送STOP_SENDING帧来通知流的关闭。这通常表明接收中的应用程序不再从流读取数据,但是这并不保证传入的数据将会被忽略。

发送STOP_SENDING帧后收到的STREAM帧仍然会被记入连接和流的流量控制,即便这些帧被收到后会被丢弃。

STOP_SENDING帧要求接收中的端点发送RESET_STREAM帧。如果流处于准备或发送状态,接收到STOP_SENDING帧的端点必须发送RESET_STREAM帧。如果流处于数据已发送状态并且被通知丢失了任何重要的数据,端点应该发送RESET_STREAM帧来替代重传。

端点应该从STOP_SENDING帧复制错误码,并复制到发送的RESET_STREAM帧,也可以使用任何应用程序的错误码。发送STOP_SENDING帧的端点可以忽略接收到的任何RESET_STREAM帧携带的错误码。如果STOP_SENDING帧在一个处于“数据已发送”状态的流上被接收,端点希望停止重传之前发送的STREAM帧,就必须先发送RESET_STREAM帧。

STOP_SENDING帧应该只在未被对端重置的流上发送。对于在“发送”和“大小已知”状态下的流,STOP_SENDING是最有用的。

如果包含前一个STOP_SENDING帧的数据包丢失,端点应发送另一个STOP_SENDING帧,但是,一旦所有流数据或者RESET_STREAM帧已经被接收,也就是,流处于非“接收”或“大小已知”状态,没有必要发送STOP_SENDING帧。

如果端点希望结束两个方向上的流,可以通过发送RESET_STREAM结束一个方向,然后可以朝相反方向发送STOP_SENDING帧促使流结束。

4. 流量控制

有必要限制接收方可以缓冲的数据量,来防止缓慢的接收方无法承受快速的发送方,或者防止因不怀好意的发送方导致在接收方消耗大量的内存。为了启用接收方对连接的内存投入限制以及开启发送方的背压使用,流分别以独自的和作为整体的方式进行流量控制。QUIC的接收方可以随时控制发送方可以在流上发送的最大数据量,如4.1节和4.2节所述。
类似地,QUIC的端点通过控制对端可创建的累计最大流的数量,来限制连接中的并发度,如4.5节所述。
CRYPTO帧发送的数据不会像与流数据相同的方式被流量控制。QUIC依靠加密协议实现来避免过度缓冲数据;见[QUIC-TLS]。实现应该为QUIC提供接口来通知它的缓冲限额,以使在多个分层没有过度缓冲。

4.1 数据流量控制

QUIC 使用与HTTP/2[HTTP2]协议中形似的基于分数的流量控制方案,接收方通告自己在特定流上和整个连接上准备接收的字节数。这导致QUIC中两种级别的流量控制。

  • 流的流量控制,通过限制任意流上可以发送的数据量,避免了单个流消费整个连接的接收缓冲区
  • 连接的流量控制,通过限制所有流上STREAM帧发送的总数据字节量,避免发送方超过该连接的接收缓冲容量

接收方在握手期间(7.3节)通过发送传输参数来设置所有流的初始分值。接收方发送MAX_STREAM_DATA帧(19.10节)或MAX_DATA帧(19.9节)给发送方来通告额外的分值。
接收方通过发送带有流ID字段的MAX_STREAM_DATA帧来通告流的分值。MAX_STREAM_DATA帧声明了流的最大绝对字节偏移量。接收方可以使用当前的被消耗数据的偏移量来决定是否通告流量控制的偏移量。接收方可以用多个数据包发送MAX_STREAM_DATA帧,来保证在耗尽流量控制的分值前,即使其中一个数据包丢失了,发送方仍可以接收到分值更新。
接收方通过发送MAX_DATA帧来通告连接的分值,它表示所有流的最大绝对字节偏移量的总和。接收方维护了一个在所有流上接收到的累计字节数之和,用于检查流量控制是否超出限制。接收方可以使用所有流上的消费字节量之和来决定需要通告的最大数据限制。
接收方可以通过发送MAX_STREAM_DATA或MAX_DATA帧通告更大的偏移量。一旦接收方通告了一个偏移量,它可以通告一个更小的偏移量,但这并没有效果。
如果发送方超出了通告的连接或者流的数据限制,接收方必须关闭连接并带上FLOW_CONTROL_ERROR错误(11节)
发送方必须忽略不增加流量控制限制的MAX_STREAM_DATA或MAX_DATA帧。
如果发送方耗尽了流量控制分值,它将无法发送新数据,可以认为被阻塞了。发送方应该发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧来表示它有数据写入但是被流量控制限制阻塞了。通常情况下这些帧期望是偶尔地被发送,除非有助于调试或者监控目的。
发送方不应该为相同的数据限制发送多个STREAM_DATA_BLOCKED或DATA_BLOCKED帧,除非原来的帧确认丢失了。在数据限制增加后,可以发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧。

4.2 流量分值增加

文档让协议实现自己决定MAX_STREAM_DATA或MAX_DATA帧中通告的时机和字节量,但是提供一些必须考虑的因素。这些帧影响连接开销。因此不要频繁发送改变量很小的帧。同时,如果流量限制的更新频率较低,接收方需要更多提交的资源,有必要对流量限制使用更大的增量值来避免阻塞。因此当决定通告多大的流量限制时,需要在资源提交和开销之间进行权衡。
基于来回的时间估计和正在接收的应用程序的消费速率,接收方可以使用自动调节机制来调节通告额外分值的频率与数量,这类似于常见的TCP实现。作为一个优化,只有在有其他帧待发送或对端被阻塞时,发送流量控制相关的帧,来确保流量控制不会引起额外数据包被发送。
如果发送方耗尽了流量控制分值,它将无法发送新数据并认为是被阻塞了。一般认为最好不要让发送方被阻塞。为了避免阻塞发送方,以及为了合理考虑损耗的可能,接收方在它预计发送方被阻塞前的两个往返,应该发送MAX_DATA或MAX_STREAM_DATA帧。
接收方在发送MAX_STREAM_DATA或MAX_DATA帧之前,不能等待STREAM_DATA_BLOCKED或DATA_BLOCKED帧,因为如果这样做意味着发送方将被阻塞至少一个往返,如果对端选择不发送STREAM_DATA_BLOCKED或DATA_BLOCKED帧,可能会阻塞更久。

4.3 处理流的取消

端点最终需要在已消费的流量控制分值上达成共识,来避免超过流量限制或者死锁。
一旦接收到RESET_STREAM帧,端点将取消匹配的流的状态,并且忽略后续该流上到达的数据。如果在RESET_STREAM帧没有包含偏移量,连接的流量控制的字节计数量两端可以不一致。
为了纠正该问题,RESET_STREAM帧(19.4节)包含了流上发送的最终数据大小。一旦接收到RESET_STREAM帧,接收方会确定地知道在发送RESET_STREAM帧之前,有多少字节数被发送了过了。接收方必须使用流的最终大小来解释(表示)它的连接级别的流量控制器中,发送在流上的所有字节,
RESET_STREAM帧会立即终止一个方向的流。对于双向流,RESET_STREAM帧对另一个方向上的数据流没有影响。两端必须维持未终止方向上的流的流量控制状态,知道该方向进入终止状态,或者其中一个端点发送了CONNECTION_CLOSE

4.4 流的最终大小

最终大小是指流消费的流量控制分值。假设流上每个紧接的字节被发送一次,最终大小就是发送的字节数。更一般地讲,最终大小比流上发送的最大偏移量的字节数偏移更高,或者如果没有字节被发送,最终大小为0。
对于被重置的流,RESET_STREAM帧显式地携带了最终大小。否则,最终大小则是偏移量加上被FIN标记的STREAM帧的长度,或者如果是非单向流,则为0。
当流的接收部分进入“大小已知”或者“重置已接收”状态(3节),端点将知道流的最终大小。
端点不能在流上发送超过最终大小的数据。
一旦流的最终大小已知,将无法改变。如果接收到表示最终大小改变的RESET_STREAM或STREAM帧,端点应该使用FINAL_SIZE_ERROR错误响应(见11节)。即使在流关闭之后,接收方应该把接收等于或者超过最终大小的数据作为FINAL_SIZE_ERROR错误。产生这些错误不是强制性的,除非因为
要求端点产生这些错误,这意味着端点需要为关闭的流维护最终大小状态,表示是一个重要意义的状态提交。

4.5 控制并发量

端点限制对端可以开启的流的累计数量。只有流ID小于(max_stream * 4 + initial_stream_id_for_type)的流可以被打开(见表5)。初始的限制在传输参数中设置(见18.2节)后续的限制是使用MAX_STREAMS帧(19.11节)来通告的。对单向流和双向流的限制应用时独立的。
如果传输参数或者接收到MAX_STREAMS帧的max_streams值大于2^60,将导致最大的流ID不能以可变长度的整形表示(见16节)。如果任意一种情况发生,连接必须被马上关闭,并带上STREAM_LIMIT_ERROR的错误类型(见10.3)
端点不能超过对端设置的并发量限制。端点接收的帧,如果其流ID超过该端点发送的限制,则必须当做一个STREAM_LIMIT_ERROR的连接错误类型(11节)
一旦接收方使用MAX_STREAMS帧通告了流的并发限制,通告更小的限制将没有效果。接收方必须忽略任何不增加流并发限制的MAX_STREAMS帧。
对于流和连接流量控制,规范对通过MAX_STREAMS帧向对端通告的时机和流的数量不做规定。协议实现可以选择,当流关闭时增加并发量限制来保持可用流的数量与对端的粗略一致性。
因为对端的限制,不能打开新流的端点应该发送STREAMS_BLOCKED帧(19.14节)。该信号有助于调试。端点不能在通告并发量数值前等待接收该信号,这样做将意味着对端会被阻塞至少一个往返,如果对端选择不发送STREAMS_BLOCKED帧,将可能阻塞更长时间。

5. 连接

QUIC的建连将版本协商和密码与传输握手相结合来减少建连时延,如7节所述。一旦建立,在任意一端,连接可以迁移到一个不同的IP或端口,如节9所述。最终,连接可以被任意一端终止,如10节所述。

5.1. 连接ID

每个连接处理一组连接标识或者连接ID,每个都可以标识该连接。连接ID是被端点单独选择的,每个端点选择对端使用的链接ID。
连接ID的主要功能是确保更低协议层(UDP, IP)寻址的变化不会引起QUIC连接的数据包被发送到错误的端点。每个端点使用协议实现特定(可能是部署特定)的方式来选择连接ID,允许带有该连接ID的数据包被路由回到端点并被接收识别。
连接ID不能包含任何可以被外部观察方(即并非和建连发起者一起协作的一方)利用的信息来与统一连接的其他连接ID关联。举一个简单的例子,这意味着相同的连接ID不能在相同连接上被分发多次。
长头部的数据包包含源连接ID和目标连接ID字段。这些字段被用来为新连接设置连接ID;详情见7.2节。
短头部(17.3节)只包含了目标连接ID并忽略了显式的长度。端点需要知道目标连接ID字段的长度。使用负载均衡器的端点基于连接ID进行路由,端点可以与负载均衡器在连接ID的固定长度或者编码方案上达成一致
固定的部分可以编码为特定的长度,使得整体的连接ID以该长度变化,并且仍然可以被负载均衡器使用。
版本协商(17.2.1节)数据包对客户端选择的连接ID应答,来确保正确路由到客户端并且允许客户端校验数据包是否来自对初始出数据包的响应。
长度为0的连接ID可以在连接ID不需要路由到正确端点时使用。但是,在相同本地IP地址和端口上多路复用连接,使用0长度的连接ID将导致失败,出现在对端连接迁移,NAT重绑定和客户端端口重用;因此不要这么做除非端点确定这些协议特性不会被使用
当端点请求一个非0长度的连接ID,它需要确保对端提供了为发送数据包给端点所选的连接ID。这些连接ID由端点使用NEW_CONNECTION_ID帧(19.15节)来提供。

5.1.1 公布连接ID

每个连接ID有一个关联的序列号有助于消息去重。在握手期间,由端点公布的初始连接ID通过长数据包头(17.2节)的源连接ID字段发送。初始连接ID的序列号为0。如果preferred_address传输参数被发送,则提供的连接ID序列号为1。
额外的连接ID通过使用NEW_CONNECTION_ID帧(19.15节)传给对端。每个新公布的连接ID对应的序列号必须递增1。初始的数据包中的连接ID由客户端随机选择,任何由重试数据包提供的连接ID不会分配序列号,除非服务端选择将这写ID保留为初始连接ID。
当端点公布了一个连接ID,在连接期间或者直到对端通过RETIRE_CONNECTION_ID帧(19.16节)使其无效,它必须接受携带该连接ID的数据包。被公布但没有废弃的连接ID被认为是活动的(active);任何活动的连接ID可以被使用。
端点应该确保对端有足够数量的可用的且未被使用的连接ID。端点保存接收到的连接ID以供未来使用,并且使用active_connection_id_limit传输参数,通告它们愿意存储的连接ID数量。端点提供的连接ID数量不应该超过对端限制。
当接收到先前未使用的连接ID或者对端废弃了一个,端点应该提供一个新的连接ID,除非提供新的连接ID会超过对端的限制。端点应该对每个连接限制公布连接ID的频率或者总数,来避免连接ID耗尽的风险;见10.4.2节。
发起迁移且要求非0长度连接ID的端点应该确保连接ID池对对端是可用的,从而对端在迁移时可以使用新的连接ID,如果连接ID池被耗尽,对端将关闭连接。

5.1.2 消费和废弃连接ID

在连接期间的任何时候,端点可以将它给对端使用的连接ID变更为另一个可用的连接ID。对端迁移时,相应的端点会消费连接ID,见9.5节查看更多。
端点维护了一套从对端接收到的连接ID集合,当发送数据包时可以使用其中任意一个。当端点想去除使用的ID,它将发送RETIRE_CONNECTION_ID帧给对端。发送RETIRE_CONNECTION_ID帧表示该连接ID将不再被使用,并要求对端使用NEW_CONNECTION_ID帧用新的连接ID替换。
如9.5节讨论的,每个连接ID必须从唯一的本地地址发送的数据包上使用。从本地地址迁移的端点,一旦不再计划使用该地址,应该废弃所有该地址上的连接ID。
端点可以通过发送带有递增的废弃优先级(Retire Prior To)字段的NEW_CONNECTION_ID帧,让对端废弃连接ID。一旦接收,对端必须废弃对应的连接ID并且发送对应的RETIRE_CONNECTION_ID帧。在大约一个PTO时间内无法废弃连接ID会导致数据包延迟、丢失,或者导致原端点发送一个对应连接ID的无状态的重置,这无法被正确路由。
从收到NEW_CONNECTION_ID帧的应答后开始,在不少于3PTO的时间间隔后,已发出废弃请求的端点可以丢弃连接ID。
直到那时,端点应该接收包含要求废弃的连接ID的数据包。后续传入的包含该废弃的连接ID的数据包会导致相应的无状态重置的响应。

5.2 匹配数据包到连接

传入的数据包在收到后被分类。数据包可以与现存的连接关联,或者对于服务端可能会创建新的连接。
主机尝试将数据包与现存的连接关联。如果数据包有一个非0长度的目标连接ID对应到现存的连接,QUIC会相应处理该数据包。注意与连接相关联的连接ID可以有多个;见5.1节
如果目标连接ID长度为0且数据包匹配到本地地址和端口的连接,该连接使用0长度的连接ID,QUIC会作为该连接的一部分处理该数据包。
对于任何数据包无法归属于现存的连接时,端点可以发送无状态的重置(10.4节)。无状态的重置让对端在连接不可用时更快地识别。
被匹配到现存连接的数据包如果与连接的状态不一致,数据包将会被丢弃。比如,如果数据包指定了与连接中不同的协议版本,或者一旦期望的密钥可用,而数据包解密(保护)失败,将会被丢弃。
没有包保护的无效数据包,比如初始(Initial),重试(Retry)或者版本协商(Version Negotiation),可以被丢弃。端点如果在发现错误之前对状态提交了变更,则必须生成一个连接错误。

5.2.1 客户端数据包处理

发送给客户端的有效数据包总是包含一个与客户端选择数值相匹配的目标连接ID。选择接收0长度的连接ID的客户端可以使用本地地址和端口来识别连接。与现有连接不匹配的数据包会被丢弃。
由于数据包重排序或丢失,客户端可能收到连接上未被计算的密钥加密的数据包。客户端可以丢弃这些数据包,或者缓存它们以期望后续数据包可以计算出密钥。
如果客户端接收到不支持版本的数据包,它必须丢弃该数据包。

5.2.2 服务端数据包处理

如果服务端接收到不支持版本的数据包,但是该数据包足够大来初始化一个服务端支持的任意版本的新连接,服务端将会发送版本协商数据包,如6.1节所述。服务端可以对这些数据包做速率控制来避免版本协商数据包风暴。否则,服务端必须丢弃指定的不支持版本的数据包。
不支持版本的第一个数据包可以对任何特定于版本的字段使用不同的语义与编码。特别地,不同数据包保护密钥可以给不同版本使用。不支持特定版本的服务端不可能解密数据包的载荷。服务端不应该尝试解码或解密未知版本的数据包,但是可以发送版本协商数据包替代,如果数据包足够长。
带有已支持的版本号或者没有版本字段的数据包,使用连接ID被匹配到一个连接,或者对于0长度的连接ID的数据包,则使用本地地址和端口。如果数据包与现有连接不匹配,服务端将继续下面的步骤。
如果数据包是一个完全符合规范的初始数据包,服务端将进行握手操作(节7)。服务器会承诺使用客户端选择的版本。
如果服务端当前不接受任何新的连接,它应该发送一个初始(Initial)包,该数据包包含了带有SERVER_BUSY码的CONNECTION_CLOSE帧。
如果数据包是一个0-RTT的包,服务端可以缓存这些有限量的数据包,以期待后续晚到达的初始数据包。客户端无法在接收服务端响应之前发送握手数据包,因此服务端应该忽略任何此类数据包。
服务端必须丢弃其他场景下到达的数据包。

5.3 QUIC连接的生命周期

待补充。

5.4 连接上必需的操作

当与QUIC传输通道交互时,有一些应用程序必须执行的操作。协议文档不指定API,但是任何该版本的QUIC协议实现必须提供本节描述的在QUIC连接上的执行操作的能力。
当实现客户端时,应用程序应该可以:

  • 打开连接,如7节所述开始交换数据;
  • 开启0-RTT如果可用的话;
  • 当0-RTT被服务端接收或拒绝,可以被通知到。

当实现服务端时,应用程序应该可以:

  • 监听到达的建连,准备如7节所述的数据交换工作;
  • 如果支持Early Data方式,会将内嵌在TLS resumption ticket中的应用控制数据发送给客户端;
  • 如果支持Early Data方式,接收来自客户端的TLS resumption ticket中的应用控制数据,并基于该信息开启拒绝Early Data方式;

任何一端,应用程序应该可以:

  • 如在传输参数(7.3节)中沟通的,可以为允许的每一种流的初始数量配置最小值
  • 控制不同类型的资源分配,包括流量控制与每种类型的流的数量;
  • 识别是佛握手已经成功完成或者仍在进行中;
  • 避免连接静默关闭,或者通过生成PING帧(19.2节),或要求通道在空闲超时过期(10.2节)之前发送额外帧;
  • 立即关闭(10.3节)连接

6 版本协商

版本协商确保客户端和服务端在相互支持的QUIC版本上达成一致。服务端发送一个版本协商数据包,来回应每个初始化新连接的数据包;具体见5.2节。
客户端发送的第一个包的大小将决定服务端是否发送一个版本协商包。支持多QUIC版本的客户端应该将它们发送的第一个包填充为所有支持版本下的最小数据包大小的最大值。这保证了服务端如果有相互支持的版本时可以进行响应。

6.1 发送版本协商数据包

如果客户端选择的版本没有被服务端接受,服务端则用版本协商包(17.2.1节)回应。数据包包括了服务端将接受的版本列表。端点不能发送版本协商包来应答接收到的版本协商包。
该机制使得服务端不保留状态地处理不支持版本的数据包。即使响应中发送的初始数据包或者版本协商包会丢失,客户端将发送新数据包直到成功接收响应或者放弃连接的尝试。最后,客户端丢弃所有连接的状态,不再在连接上发送任何数据包。
服务端可以限制发送的版本协商包的数量。例如,可以识别0-RTT数据包的服务端可以选择不发送版本协商包来响应0-RTT数据包,以期望最终会收到一个初始数据包。

6.2 处理版本协商数据包

当客户端接收到版本协商包,它必须放弃当前的建连尝试。版本协商包被设计为允许QUIC的未来版本来协商端点间正在使用的版本。支持多QUIC版本的实现如何在尝试使用该版本建立连接时响应版本协商包的方式,QUIC的未来版本可能对此进行变更。
还剩如何执行版本协商作为QUIC未来版本定义的后续工作。特别地,这需要确保对版本降级攻击的健壮性,如21.10节所述。

6.2.1 草案版本之间的版本协商

当草案实现接收到版本协商包时,可以使用包中列出的版本来尝试创建新连接,而不是停止当前连接的尝试,如6.2节中
客户端必须检查目标与源连接ID字段与客户端发送的数据包中的目标与源连接ID字段是否匹配。如果校验失败,数据包必须被丢弃。
一旦版本协商包被确定为有效的,客户端会从服务端提供的列表中选择一个可接受的协议版本。客户端接着尝试使用该版本创建新的连接。新的连接必须使用与之前发送的ID不同的新随机目标连接ID。
注意,该机制不会保护降级攻击,且草案以外的其他实现不能使用。

6.3 使用保留版本

对于未来服务端使用新的版本,客户端需要正确处理不支持的版本。为了帮助确保这点,当生成版本协商包时,服务端应该包含一个保留版本来强制版本协商(15节中定义的0x?a?a?a?a)。
版本协商这一设计允许服务端避免维护服务端拒绝的数据包的状态.
客户端的可以使用为强制版本协商保留的版本发送数据包。这可以用于从服务器征求支持的版本列表。

7 加密与传输握手

QUIC依赖于加密与传输握手相结合的方式来使建连时延最小化。QUIC使用CRYPTO帧(19.6节)来传输加密握手。0x00000001版本QUIC使用TLS,如[QUIC-TLS]中所述;
不同的QUIC版本号表示正在使用的不同加密握手。QUIC提供可靠的,有序发送的加密握手数据。QUIC数据包保护被用于尽可能加密握手协议。加密握手必须提供下列属性:

  • 认证的密钥交换(authenticated key exchange)

    • 服务端总是经过认证的;
    • 客户端是可选认证的,
    • 每个连接生成不同且不相关的密钥;
    • 密钥的材料可用于对0-RTT和1-RTT包的数据包保护;
    • 1-RTT密钥有前向保密性
  • 对两端的传输参数进行认证,以及服务端传输参数进行加密保护(见7.3节)

  • 应用协议的认证协商(对此TLS使用ALPN [RFC7301])

端点可以在发送的第一个包中对显示拥塞通知(ECN)的支持进行验证,如13.4.2节所述。
CRYPTO帧可以在不同的包数值空间中发送。CRYPTO帧使用序列号来确保有序发送加密握手数据,每个包编号空间从0开始。
端点必须显式协商一个应用协议。这避免了使用时协议不一致的情况。

7.1 握手流程示例

TLS如何与QUIC完整性的细节在[QUIC-TLS]中给出,但是一些例子在这里给出。支持客户端地址校验的数据交换扩展在8.1.1节中给出。
一旦地址校验交换完成,加密握手就被用作协商加密密钥。加密握手通过初始包(17.2.2节)和握手包(17.2.4节)携带。
图3给出了1-RTT握手的介绍。首先每一行展示了带有包类型和包编号的QUIC数据包,接着是这些数据包中通常包含的帧。因此,第一个包是初始包,包的编号为0,包含了携带ClientHello的CRYPTO帧
注意多个QUIC包,即使是不同的加密级别,都可以被合并为单个UDP报文(见12.2节),因此握手可以由至少4个UDP报文组成。举个例子,服务端第一次传输数据包包含初始加密级别(混淆),握手级别,与来自服务端的1-RTT加密级别的0.5-RTT数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Client                                                  Server

Initial[0]: CRYPTO[CH] ->

Initial[0]: CRYPTO[SH] ACK[0]
Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
<- 1-RTT[0]: STREAM[1, "..."]

Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[0]: STREAM[0, "..."], ACK[0] ->

1-RTT[1]: STREAM[3, "..."], ACK[0]
<- Handshake[1]: ACK[0]

图3: 1-RTT握手示例

图4展示了使用0-RTT握手和0-RTT单数据包建连的例子。注意如12.3节描述的,服务端在1-RTT加密级别(level)应答0-RTT数据,客户端在相同的包编号空间发送1-RTT数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Client                                                  Server

Initial[0]: CRYPTO[CH]
0-RTT[0]: STREAM[0, "..."] ->

Initial[0]: CRYPTO[SH] ACK[0]
Handshake[0] CRYPTO[EE, FIN]
<- 1-RTT[0]: STREAM[1, "..."] ACK[0]

Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[1]: STREAM[0, "..."] ACK[0] ->

1-RTT[1]: STREAM[3, "..."], ACK[1]
<- Handshake[1]: ACK[0]

图4: 0-RTT握手示例

7.2 协商连接ID

连接ID用于确保数据包路由的一致性,如5.1节所述。长头部包含二个连接ID:目标连接ID由数据包接收方选择,用于提供路由一致性;源连接ID用于设置对端使用的目标连接ID。
在握手期间,带有长头部(17.2节)的数据包被用于建立每个端点使用的连接ID。每个端点使用源连接ID字段指定的连接ID,用于设置发送给他们的数据包的目标连接ID字段中。一旦接收到数据包,每个端点将其发送的目标连接ID设置为与它们接收的源连接ID相匹配的值。
对于之前没有收到过初始包或者重试包的客户端,当初始包被该客户端发送时,会使用一个随机值来填充目标连接ID字段。该值必须是8字节长的。直到从服务端接收到数据包,客户端必须使用相同的值(连接ID),除非客户端放弃建连尝试,并开始新的建连尝试。初始的目标建连ID被用于为初始包确定包保护密钥。
客户端使用它选择的值来填充源连接ID字段,并且设置SCID Len字段来表示长度。
0-RTT数据包的第一次传输使用与客户端第一次初始化相同的目标和源连接ID值。
一旦首先从服务端接收到初始或重试包,客户端使用服务端提供的源连接ID作为后续数据包的目标连接ID,包括任何后续的0-RTT数据包。这意味着客户端可能在建连期间改变目标连接ID二次,一次是响应重试包,另一次是响应来自服务端的初始包。一旦客户端接收到来自服务端的初始包,它必须丢弃任何接收到的不同源连接ID的数据包。
客户端必须只能改变目标连接ID的值,以响应第一个来自服务端的重试或初始数据包;服务端必须基于初始包设置数值。任何额外的变更是不允许的;如果后续这些类型的数据包包含了不同的源连接ID,必须被抛弃。这避免了对生成不同连接ID的多个初始包进行无状态处理而引起的问题。
连接ID可以在连接的生命周期内改变,尤其是响应连接迁移时(9节);详见5.1.1节。

7.3 传输参数

在建连期间,两端对他们声明的传输参数进行认证。这些声明是每个端单方面发布的。端点需要遵循这些参数隐含的约束。每个参数的描述包含了对它的处理方式。
传输参数的编码详见18节。
QUIC包含加密握手中的已编码传输参数。一旦握手完成,对端声明的传输参数就可用了。每个端点校验其对端提供的值。
每个定义的传输参数的定义都包含在18.2节中。
端点必须将接收到的具有无效值的传输参数视为TRANSPORT_PARAMETER_ERROR类型的连接错误。
端点禁止发送一次以上给定传输参数扩展中的参数。 终端应该将接收到重复的传输参数视为TRANSPORT_PARAMETER_ERROR类型的连接错误。
如果服务端发送了一个重试包来开启重试校验,它必须包含传输参数original_connection_id(18.2节),如17.2.5节所述。

7.3.1 0-RTT的传输参数值

两端存储来自连接的服务端传输参数,除了被显式排除的参数,这些参数会提供给在后续对对端的连接中发送的任意0-RTT包使用。记住的传输参数应用到新的连接直到握手完成并且客户端开始发送1-RTT数据包。一旦握手完成,客户端使用握手时建立的传输参数。
新传输参数(7.3.2节)的定义必须明确是否必须,可以,禁止为0-RTT存储。
客户端禁止使用下列记住的参数值:
original_connection_id, preferred_address, stateless_reset_token, ack_delay_exponent 和 active_connection_id_limit.
在握手时,客户端必须使用服务端的新值替代,没有新值,则使用默认值。
尝试发送0-RTT数据的客户端必须记住所有其他服务端使用的传输参数。服务端可以记住这些传输参数,或者存储在票据(ticket)中受完整性保护的值的副本,当接收0-RTT数据时可以恢复这些信息。服务端使用传输参数来决定是否接收0-RTT数据。
如果0-RTT数据由服务端接收,服务端禁止减少任何限额或者替换任何可能与客户端0-RTT数据值相违背的数值。特别地,服务端接收0-RTT数据禁止为下列参数(18.2节)设置比推荐参数值更小的数值,

  • 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和initial_max_streams_bidi和 initial_max_stream_data_bidi_remote,或者initial_max_streams_uni 和initial_max_stream_data_uni。
如果传输参数的隐含值不受支持,服务器必须拒绝0-RTT数据,或者中止握手。
当在0-RTT包中发送帧时,客户端必须只能使用记住的传输参数;重要地是,禁止使用从服务端更新的传输参数或者接收到的1-RTT数据包中帧的更新值。来自握手的传输参数的更新值只能用于1-RTT数据包。举个例子,已记住的传输参数中的流量控制限制,会应用到0-RTT数据包,即使这些数值通过握手或者在1-RTT数据包中发送的帧。服务端可以将在0-RTT中使用更新的传输参数当做PROTOCOL_VIOLATION类型的连接错误。

7.3.2 新的传输参数

新的传输参数可以被使用来协商新的协议行为。端点必须忽略它不支持的传输参数。传输参数缺少可以关闭任何使用该参数协商的可选协议特性。如18.1节所述,一些标识符被保留为了行使这一要求。
新传输参数可以根据22.1节中的规则进行注册。

7.4 加密信息缓冲

协议实现需要维护接收的乱序的CRYPTO帧数据的缓冲。因为没有CRYPTO帧的流量控制,端点可以强制对端缓存一个无限量的数据。
实现必须支持至少4096字节的乱序接收的CRYPTO帧数据的缓冲。端点可以选择在握手期间允许缓冲更多数据。握手期间更大的限额可以使得更多的密钥和证书交换。在连接的生命周期中,端点的缓冲大小不需要保持不变。
握手期间无法缓冲CRYPTO帧会导致连接失败。如果在握手期间端点的缓冲满了,可以暂时扩容来完成握手。如果端点没有扩大缓冲,它必须关闭连接,并带上CRYPTO_BUFFER_EXCEEDED错误码。
一旦握手完成,如果端点不能缓冲所有CRYPTO帧中的数据,它可以丢弃该CRYPTO帧以及所有后续接收到的CRYPTO帧,或者可以关闭连接,并带上CRYPTO_BUFFER_EXCEEDED错误码。包含丢弃的CRYPTO数据包必须被应答,因为数据包已经被接收并且被传输方处理,及时CRYPTO帧被丢弃了。

8 地址校验

QUIC使用地址校验来避免被用于流量放大攻击。在这样的攻击中,带有伪装成受害者的源地址信息的数据包被发送到服务器。如果服务端产生了更多或者更大的数据包来响应该数据包,攻击者可以利用该服务器向受害者发送比攻击者自身可以发送的更多的数据。
对放大攻击的主要防御是校验端点能在它声明的传输地址上接收数据包。地址校验在建连(8.1节)和连接迁移(8.2)期间执行。

8.1 建连期间的地址校验

连接建立隐式地为两端提供地址校验。 特别地,接收到由握手密钥保护的数据包可以确认客户端从服务器收到过初始数据包。一旦服务器成功地处理了来自客户端的握手数据包,就可以认为客户端地址已经被校验。
在验证客户端地址之前, 服务器禁止发送超过其接收字节数的三倍的字节数。 这就限制了任 何通过使用带欺骗性源地址进行的放大攻击的规模。确定了该限制,服务器只计算已经成功处理的数据包的大小。

客户端必须确保包含初始数据包的UDP数据报有至少至少1200字节的UDP载荷,根据需要对数据报中的包进行填充。发送填充的数据报可确保服务器不受放大约束的过度限制。

数据包丢失,特别是来自服务器的握手数据包的丢失,会导致当客户端没有数据要发送且达到抗放大限制时,服务器无法发送的情况。 为了避免由此引起的握手死锁,客户端应该发送基于探测超时的数据包, 如[QUIC-RECOVERY]所述。如果客户端没有要重新传输的数据,并且没有握手密钥,则应该在至少1200字节的UDP数据报中发送初始数据包。 如果客户端有握手密钥,则应该发送握手数据包。

服务器可能希望在开始加密握手之前验证客户端地址。 QUIC在完成握手之前使用初始数据包中的 令牌来提供地址验证。 此令牌在建立连接期间使用重传数据包(8.1.1节)或在之前的连接中使用NEW_TOKEN帧(8.1.2节)传递给客户端。

除了地址校验之前施加的发送规定的限制之外,服务器还受到拥塞控制器所设置的发送限制的约束。客户端仅受拥塞 控制器的约束。

8.1.1 使用重试包进行地址校验

一旦接收到客户端的初始包,服务器可以通过发送包含令牌的重试包(17.2.5节)来请求地址校验。客户端在收到重试包后,必须在其发送的所有初始包中重复此令牌。作为对包含令牌的初始处理的响应,服务器可以中止连接或允许连接继续。

只要攻击者不可能为其自己的地址生成有效的令牌(请参见8.1.3节),并且客户端能够返回该令牌,它就会向服务器证明它收到了令牌。

服务器还可以使用重试包来延迟状态并处理连接建立开销。
要求服务端提供不同的连接ID,顺带18.2节定义的传输参数original_connection_id,来强制让服务端证明服务端或者和它交互的实体,已接收到来自客户端的原始初始包。
通过为提供不同的连接ID,还可以让服务端对后续数据包的路由方式进行控制。这可以用于直连到不同的服务端实例。

图5展示的流程图表示了重试包的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
Client                                                  Server

Initial[0]: CRYPTO[CH] ->

<- Retry+Token

Initial+Token[1]: CRYPTO[CH] ->

Initial[0]: CRYPTO[SH] ACK[1]
Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
<- 1-RTT[0]: STREAM[1, "..."]

图5: 用重试包握手的列子
8.1.2 未来连接的地址校验

服务端可以在连接期间给客户端提供一个地址校验令牌,该地址校验令牌可用于后续连接。地址校验对于0-RTT尤为重要,因为服务器可能会向客户端发送大量数据以 响应0-RTT数据。

服务器使用NEW_TOKEN帧(19.7节)向客户端提供可用于校验未来的连接的地址校验令牌。客户端将此令牌包含在初始包中,以便在将来的连接中提供地址校验。除非重试或NEW_TOKEN帧将令牌替换为较新的令牌,否则客户端必须在其发送的所有初始包中包含该令牌。客户端禁止对未来的连接使用重试包中提供的令牌。服务器可以丢弃不携带预期令牌的任何初始包。
令牌应该以某种方式被构造,该方式便于将其与在发送的重试包中的相同字段携带的令牌区分开来。
令牌不能包含可以让路径上的观察者将连接和发布的内容相关联的信息。例如,不能包含连接ID或寻址信息除非是数值是加密的。
与为重试包创建的令牌不同,在令牌被创建和随后被使用的之间可能会有一段时间。因此,令牌应该有过期时间。 使用显式的过期时间或发布的时间戳,动态地 计算过期时间。服务端可以存储过期时间或者将它以加密的形式包含在令牌中。
两个不同的连接上不可以有相同的客户端端口;校验端口因此不可能成功。
如果在连接到客户端认为是先前相同服务器的连接上,客户端接收到NEW_TOKEN帧的令牌,则应在其初始包的Token字段中包含该值。包含令牌可以让 服务器无需额外的往返校验客户端地址。
令牌允许服务端在将发布令牌的连接和使用令牌的任何连接之间的活动关联起来。希望中断与服务器的身份连续性的客户端可能会放弃使用NEW_TOKEN帧提供的令牌。在重试包 中获得的令牌必须在连接尝试过程中立即使用,并且不能在后续连接尝试中使用。
客户端不应在不同的连接中重用令牌。重用令牌使得连接能被网络路径上的实体关联起来(请参见9.5节)。 如果客户端认为自上次使用令牌以来其网络附件 发生了改变,即如果其本地IP地址或网络接口发生了改变,则不得重用该令牌。如果客户端在完成握手之前迁移,则需要重新启动连接过程。

客户端可能在单个连接上收到多个令牌。除了禁止链接性,任何令牌可以被使用在任何连接尝试中。服务端可以发送附加的令牌来为多连接尝试开启地址校验,或者替换其他失效的令牌。对于客户端,大致意味着发送最近未使用的令牌很可能是有效的。尽管保存和使用更旧的令牌没有负面后果,客户端可以认为旧令牌当不太可能对服务端进行地址校验有用。

当服务器收到带有地址验证令牌的初始包时, 它必须尝试校验该令牌,除非它已经完成了地址校验。如果令牌无效,则服务器如同没有已验证的地址的客户端一样处理,包括可能发送 重试。如果验证成功,服务器应该允许握手继续。

注:将客户端视为未验证而不是丢弃包的解释是,客户端可能已在以前的连接中使用NEW_TOKEN帧接收到了令牌,如果服务器已丢失状态,则可能根本无法验证令牌,如果丢弃 包,则会导致连接失败。服务器应该对NEW_TOKEN帧提供的令牌进行编码,并以不同的方式重试包,并对后者进行更严格的验证。

无状态设计中,服务器可以使用加密的且认证过的令牌来将信息传递给客户端,然后服务器可以恢复并使用这些令牌来验证客户端地址。 令牌未集成到加密握手中,因此不会对其进行 身份验证。例如,客户端可能能够重用令牌。为了避免利用此属性的攻击,服务器可以将其令牌的使用限制为仅验证客户端地址所需的信息。

攻击者可以重放令牌,以便在DDoS攻击中将服务器用作放大器。为防止此类攻击,服务器应确保以重试包发送的令牌仅在短时间内被接受。NEW_TOKEN帧中提供的令牌(请参见19.7节) 需要更长时间有效,但不应在短时间内多次接受。鼓励服务器在可能的情况下只允许令牌使用一次。

8.1.3 地址校验令牌完整性

地址验证令牌必须很难猜测。在令牌中包含足够大的随机值就足够了,但这取决于服务器是否记住这个发送给客户端的值。
基于令牌的方案允许服务器卸载任何同针对客户端的校验相关联的状态。要使此设计正常工作,必须对令牌进行完整性保护,以防止客户端修改或伪造令牌。如果没有完整性保护,恶意客户端可能会生成或猜测服务器将接受的令牌的值。 只有服务器需要访问令牌的完整性保护密钥。
不需要为令牌使用一个定义明确的格式, 因为生成令牌的服务器也会使用它。令牌可以包含有关声明的客户端地址(IP和端口)的信息、时间戳以及服务器将来验证令牌所需的任何其他补充信息。

8.2 路径校验

迁移端点在连接迁移(参考9节和9.6节)期间使用路径校验来验证从新的本地地址到对端的可达性。在路径验证中,端点测试特定本地地址与特定对端地址之间的可达性,其中地址是IP地址和端口的两元组。

路径校验会测试数据包(PATH_CHALLENGE)是否可以从路径上的对端发送和接收(PATH_RESPONSE)。重要的是,校验从迁移的对端接收的包不携带伪造的源地址。

任何终端都可以随时使用路径校验。例如,终端可能会在一段时间的静默后检查对端是否仍然拥有其地址。

路径校验不是以NAT穿透机制设计的。虽然这里描述的机制可能对创建支持NAT穿透的NAT绑定有效,但是期望的是一个或 另一个对端在没有先在该路径上发送包的情况下能够接收包。有效的NAT穿透需要额外的同步机制,这是路径校验没提供的。

端点可以把用于路径验证的PATH_CHALLENGE和PATH_RESPONSE帧和其他帧进行捆绑。特别地,终端 可以装配一个携带PATH_CHALLENGE的包用于PMTU探测,或者终端可以把一个PATH_RESPONSE和自己的PATH_CHALLENGE捆绑起来。

在探测新路径时,终端可能希望确保其对端具有可用于响应且未使用的连接ID。 终端可以在同一个包中发送NEW_CONNECTION_ID和PATH_CHALLENGE帧。 这可确保在发送响应时,对端有未使用的连接ID可以使用。

8.3 发起路径校验

要发起路径校验,端点会在要验证的路径上发送包含随机载荷的PATH_CHALLENGE帧。

端点可能发送多个PATH_CHALLENGE帧以防止丢包。 但是,端点不应该在同一个数据包中发送发送多个PATH_CHALLENGE帧。终端不应该比初始数据包更频繁地发送PATH_CHALLENGE, 从而确保连接迁移在新路径上的负载量,与建立一个新连接相比没有更多负载。

端点必须在每个PATH_CHALLENGE帧中使用不可预测的数据,以便它可以将对端的响应与相应的PATH_CHALLENGE相关联。

8.4 路径校验的响应

在接收到PATH_CHALLENGE帧时,端点必须通过在PATH_RESPONSE帧中填入PATH_CHALLENGE帧中包含的 数据来立刻响应。

端点禁止发送超过一个PATH_RESPONSE帧来响应一个PATH_CHALLENGE帧(见13.3)。对端

为了确保包可以发送到对端和从对端接收,必须在与 触发PATH_CHALLENGE相同的路径上发送PATH_RESPONSE。 也就是说,从收到PATH_CHALLENGE的同一本地地址发到 发出这个PATH_CHALLENGE的同一个远程地址。对端希望根据需要发送更多的PATH_CHALLENGE帧,来获得额外的PATH_RESPONSE帧。

8.5 成功的路径校验

当收到符合以下标准的PATH_RESPONSE帧时,新地址才被认为是有效的:
收到对包含PATH_CHALLENGE帧的包的确认是不充分的验证,因为确认可能被恶意对端方伪造。

注意,在不同的本地地址上接收不会导致路径验证失败, 因为它可能是转发数据包(请参阅9.3.3节) 或错误路由的结果。 将来可能会收到有效的PATH_RESPONSE。

8.6 失败的路径校验

路径校验只在当尝试验证路径的端点放弃其验证路径的尝试时失败。

终端应该根据计时放弃路径验证。设置此计时器时,协议的实现会警告新路径的往返时间可能比原始路径长。 建议使用[QUIC-RECOVERY]中定义的当前探测超时(PTO)或初始超时(即2 * kInitialRtt)中较大者的三倍的值。 就是说:

1
validation_timeout = max(3*PTO, 6*kInitialRtt)

请注意,端点可能会在新路径上接收包含其他帧的数据包,但路径验证成功要求收到具有正确数据的PATH_RESPONSE帧。

当终端放弃路径校验时,即确定该路径不可用。 这并不一定意味着连接失败 - 端点可以继续在适当的情况下 通过其他路径发送数据包。如果没有可用的路径,则终端可以等待一个新路径变为可用或关闭连接。

除了失败之外,路径校验也可能因为其他原因被放弃。 这种情况主要发生于如果在旧路径上的路径校验正在 进行时启动了到新路径的连接迁移。

9 连接迁移

使用连接ID允许连接在端点地址(即IP地址和或端口)改变时存活,例如当终端迁移到新网络时。本节 介绍终端迁移到新地址的过程。

QUIC的设计依赖于端点在握手期间保留稳定的地址。端点禁止在握手确认之前发起连接迁移,如[QUIC-TLS]4.1.2节定义的。

如果对端在握手期间发送了“disable_migration”传输参数, 则端点禁止从不同的本地地址发送数据包,主动启动连接迁移。 已发送此传输参数的端点,但检测到仍迁移到其他网络的对端,必须没有产生无状态的重置的同时,丢弃在该路径上传入的数据包,或者进行路径校验并让对端迁移。
生成无状态重置或者关闭连接可以让网络中的第三方,通过伪造或者处理观察到的流量,触发连接关闭。

并非所有对端地址的更改是有目的的,或者活跃的迁移。对端可能正在进行NAT重新绑定,此时中间件地址的更改,通常是NAT,给流(flow)分配新的传出端口或甚至是新的传出IP地址而导致的地址。 但是如果终端检测到对端的IP地址任何变化,则必须执行路径校验(8.2节),除非之前就校验过这个地址。

当端点没有验证路径来发送数据包时,可以抛弃连接装填。在丢弃连接状态之前,有连接迁移能力的端点可以等待新路径可用。

除9.6中所述之外,本文档约束了将连接迁移到新客户端地址的行为。客户端负责发起所有迁移。 服务器不会向客户端地址发送非探测包(请参阅9.1节),直到服务器收到来自该地址的非探测包。 如果客户端从未知服务器地址收到包,则客户端必须丢弃这些包。

9.1 探测新的路径

端点可以在连接迁移到一个新的本地地址之前,使用8.2节所述的路径校验,探测新本地地址到对端的可达性。

链路验证失败只是表明新的链路对该连接不可用。除非没有其他可以替代的有效链路,链路验证失败并不会导致连接终止。

从新的本地地址探测时,端点使用一个新的连接ID,进一步的讨论可以参考9.5节。 使用新地址的端点必须确保对端可以提供至少一个新的连接ID, 这是通过在探测的时候添加一个NEW_CONNECTION_ID帧来实现的。

从对端接收到一个PATH_CHALLENGE帧的时候,表明该对端正在探测一条链路的可达性,端点会按照8.2节回复一个PATH_RESPONSE帧。

PATH_CHALLENGE,PATH_RESPONSE,NEW_CONNECTION_ID,和 PADDING 帧都是”探测帧“, 其他的帧都是“非探测帧”。 只包括探测帧的包是一个“探测包”, 包括其他帧的包都是“非探测包”。

9.2 发起连接迁移

端点可以通过从新本地地址发送包含非探测帧的数据包来将连接迁移到该新本地地址。

在连接建立的时候,每端都会验证对端的地址。 因此,当一个正在迁移的端点可以向对端发送消息时表明 对端在当前的地址已经准备好接收消息了。 即在迁移到新的地址的时候,端点不用先验证对端的地址。

当迁移的时候, 新的链路或许不能支持端上当前的发送速率。 因此,该端需要重置拥塞控制器,详见9.4节

新的链路或许没有同样的ECN能力,因此,端点需要验证ECN能力,如13.4节所述.

在新的链路上发送的数据收到确认的时候,表明从新地址到对端是可达的。注意由于确认可以在任何链路上收到, 在新链路上的返回可达性尚未建立。为了建立返回可达性,端点可以同时在新的链路上发起链路校验(8.2节),

9.3 对连接迁移响应

从新的对端地址收到一个非探测包的的时候,表明该端已经迁移到了新的地址。

作为对该包的回应,端点必须开始将后续的包发往新的对端地址,同时为了验证对端的未校验地址的所有权, 端点必须发起链路验证(8.2节)。

端点可以向未验证的对端地址发送内容,但是必须抵御潜在的攻击,9.3.1和9.3.2所述。如果对端的地址近期出现过,端上也可以跳过验证对端地址。

为了响应对编号最高的非探测包,端点只要改变发送数据包的地址。这样保证了在收到重排序的包的情况下, 端点不会为旧对端地址继续发送数据包。

在改变了发送非探测包的地址后, 端点就可以放弃任何其他地址的链路校验。
从新的对端地址接收到数据可能会导致NAT重绑定到对端。

在验证了新的客户端地址之后,服务端可以向客户端发送新的地址校验令牌(8节)。

9.3.1 对端地址欺骗

对端有可能欺瞒源地址,导致端点向一个错误的主机发送过量的数据。如果端点比欺骗的对端发送更大量的数据,连接迁移就会被用来对受害者进行攻击者生成的数据量的放大攻击。

如9.3所描述,端点需要验证对端的新地址,来确保对端对新地址的所有权。端点必须限制发送数据的速率, 直到对端的地址被认为是有效的。每个预估的往返时间内(kMinimumWindow, [QUIC-RECOVERY]中定义), 端上禁止发送超过最小拥塞窗口的数据。如果没有这个限制,就会有对信任的受害者发起拒绝服务攻击的风险。 需要注意的是,端点不会有对该地址的任何往返时间衡量,估算值为默认初始值(见[QUIC-RECOVERY])。

如果端上跳过了对端地址的验证, 详见Section 9.3,就不会限制发送速率。

9.3.2 链路上的地址欺骗

链路上的攻击者可以在原始数据包到达之前通过复制和发送虚假地址包的方式,引起伪造的连接迁移。带有虚假地址的包将会被认为是来自一个迁移中的连接,原始的包就会被认为是重复的然后被丢弃。 在虚假的连接迁移之后,源地址的验证会失败,因为在源地址上的实体没有必须的加密密钥来读取或者对发送给它的PATH_CHALLENGE帧响应PATH_CHALLENGE帧,即使它想要这么做。

为了保护连接免于虚假迁移导致的失败,在对端地址验证失败的时候,端点必须回退到上次校验过的对端地址。

如果端上没有上次已校验对端地址的状态,那么就必须通过丢弃所有连接状态的方式静默地关闭连接。这样在连接上的新包都会被统一处理。例如,端上可以为后续进来的包都回复一个无状态的重置。

注意接收到来自合法对端地址的带有较高数据包编号的包,会触发新的连接迁移。这样虚假迁移的地址校验就会被丢弃掉。

9.3.3 链路外的数据包转发

一个链路外可以观察包的攻击者可以将真实包的拷贝转发给端点。 如果复制的包比真实包之前到达,会表现为NAT重绑定。任何真实的包都会被认为重复而丢弃。如果攻击者能够继续转发包的话,可以造成连接迁移到一个经由攻击者的链路上。这样就会把攻击者置于链路上,使得攻击者可以观测或者丢弃后续的包了。

并不像9.3.2节描述的攻击,这样攻击者可以保证新的链路是可以被有效验证的。

这种类型的攻击依靠攻击者使用了一个跟双端直接连接近乎相同速度的链路。如果很少的包正在发送,或者攻击的同时伴随丢包情况,这种攻击就更有效。

在原始的链路上接收到一个增加最大数据包编号的非探测包的时候,端上就会迁移回该链路。在原始链路 上发送包增加了攻击失败的概率。因此,攻击性的迁移依靠触发包的交换。

为了对显式迁移的回应,端点必须必须使用PATH_CHANLLENGE帧验证之前有效的链路。这使得在该链路上发送新的数据包。如果链路是不可达的, 验证尝试就会超时而失败;如果链路是可达的,但是不再被需要,验证会成功,但是,只有导致探测包会在链路上发送。

在活跃的链路上接收到PATH_CHALLENGE报文的端点应该发送非探测包作为回应。如果非探测包在任一攻击者复制的包之前到达, 连接就会被迁移回原来的链路。任何后续的迁移都会重新开始这整个流程。

这种防御并不是完美的, 但这并不被认为是一个严重的问题。如果攻击者的链路在多次尝试 使用原始链路的情况下仍然比原始链路可靠迅速,那就不可能区分是攻击者还是路由上的改进。

端上也可以使用启发式的方式改进对这种攻击类型的探测。 例如,如果刚刚从老的链路接收到包,NAT重新绑定 是不大可能发生的,在IPV6的链路上类似的绑定也是很少的。相反,连接ID的改变更可能表明有目的的迁移而不是攻击。

9.4 丢失检测和拥塞控制

新链路的容量有可能和老链路的不同。在旧链路发送的包不应该对新链路的拥塞控制或RTT预测起作用。

在确认对端新地址的所有权的时候,端点必须立刻将新链路的拥塞控制器和RTT估算器重置到初始值(见A.3节)与[QUIC-RECOVERY]的B.3节,除非端点了解到之前的发送速率和往返估时对新链路有效。 例如,客户端会推断,客户端端口号的改变表示NAT重新绑定,意味着新的链路很可能有类似的带宽和往返时间。 但是,这种判定方式是不完善的;如果判定不正确,拥塞控制器和RTT估计器应该适应新的链路。一般来说,建议协议实现在新链路上使用之前数值时,应该小心谨慎。

在端点于迁移期间从/向多个地址发送数据和探测时,接收方有可能有明显的重排序, 因此会导致两个探测出来的链 路有不同的往返时间。 多个链路上的包接受者仍旧会为所有接收到的包发送ACK帧。

尽管在连接迁移的时候多个链路有可能被使用,单个拥塞控制上下文和单个丢失重传上下文就够用了(详见[QUIC-RECOVERY]),例如,端点会延迟切换到新的拥塞控制上下文,直到确认旧链路已经不被使用(见9.3.3节)。

发送者可以为探测包做例外的处理,使得这些包的丢失检测是单独的,并且不会过度引起拥塞控制器降低发送速率。 端点可以单独设置一个PATH_CHALLENGE帧发出的计时器,当相应的PATH_RESPONSE包收到的时候取消计时。 如果在PATH_RESPONSE包接收到之前定时器被触发, 端点可以重新发个PATH_CHALLENGE帧, 同时为定时器设置一个更长的时间.

9.5 连接迁移的隐私性含义

在不同的网路链路上使用稳定的连接ID可以使被动的观察者关联起这些链路之间的活动。在不同的网路之间移动的端点,可能不希望有除了对端以外的其他实体相关联的活动,因此从不同的本地地址发送的时 候会使用不同的连接ID,这在5.1节中讨论。端上必须保证他们提供的连接ID不会被其他实体关联到,才能保证隐私性生效。

在任何时候,端点可以将他们发往的目标连接ID改变为其他链路上未被使用的值。

如果端点发起了连接迁移(如9.2节所述)或者探测新的网络链路(9.1节所述),则必须使用新的连接ID。如果新对端地址的数据包使用了有效的未被对端使用过的连接ID,端点必须使用新的连接ID来响应对端地址的变化。

在每个新网络链路上,使用不同的连接ID来发送两个方向的数据包,为将相同连接跨网络链路的数据包进行关联时,消除了连接ID的使用。头部保护确保数据包编号不会被用来关联活动。这防止其他数据包的属性,如计时和大小,被用于关联活动。

当发送流量一段时间后的不再活动,客户端可以通过使用新的连接ID与源UDP端口,希望减少连接性。改变发送数据包的UDP端口可能引起数据包像连接迁移一样的表现。这样确保了在客户端没有经历NAT重绑定或者真实的连接迁移的时候, 支持连接迁移的机制也是可以被使用的。更新端口号会造成对端重置它的拥塞状态(详见9.4节),因此端口不应该被频繁的更新。

耗尽可用的连接ID的端点,不能探测新的链路或者发起迁移,也不能响应探测或者对端发起的迁移尝试。为了确保迁移时可能的,来自不同链路上的数据包不会被关联起来,在端点迁移前,端点应该提供新的连接ID,参考5.1.1节。如果端点可能耗尽了可用的连接ID,迁移中的端点可以在新链路上发送的所有数据包中包含NEW_CONNECTION_ID帧。

9.6 服务端的首选地址

Quic协议允许服务端在一个IP地址上接收连接, 然后尝试在握手之后将这些连接转移到另一个首选地址。 在客户端初始连接到了被多个服务端共享的地址上,但更倾向于使用单播地址来确保连接稳定性的时候,这一点尤其有用。 这部分内容描述将连接迁移到一个首选 服务端地址的协议。

将连接迁移到一个新的服务端地址的时候,对于对于后续操作而言,处于半连接的状态。如果 客户端接收到的数据包,并不是来自传输参数preferred_address制定的服务端,那么客户端应该把这些包丢弃掉。

9.6.1 商议一个首选地址

服务端通过在TLS握手中添加一个preferred_address传输参数来传达一个首选的地址。

服务端可以为每个协议簇(IPv4和IPv6)商议一个首选的地址,让客户端来自己选择最适合它们网络附件的一个。

一旦握手完成,客户端应该在服务端的两个首选地址里选择一个,然后利用在preferred_address的传输参数 里面指定的连接ID发起链路校验(详见8.2节)。

如果链路校验成功了, 客户端应该立即开始利用新的连接ID将所有的后续包发送到新的服务端地址,终止旧的服务器地址的使用。 如果链路校验失败,客户端必须继续向原始服务端地址发送后续的包。

9.6.2 对连接迁移的响应

服务端可能在它接受连接后的任何时候,接收到指向它首选的IP地址的包。如果这个包包含了一个PATH_CHALLENGE帧,服务端发送8.2节所述的PATH_RESPONSE帧。 服务端必须从其原始地址发送其它非探测帧, 直到它在自己首选地址从客户端收到了非探测包,且服务端已经校验了新的链路。

服务端必须从首选地址到客户端的链路上探测。 这有助于防卫攻击者发起的伪造的连接迁移请求。

一旦服务端完成了链路探测并且已经在首选地址上收到了一个带着新的最大包编码的非探测包, 服务端开始仅从首选地址发送非探测包给客户端。 它应该丢弃这个连接上接收到的旧 IP 地址的包, 但是可能继续处理延迟的包。

9.6.3 客户端迁移和首选地址的交互

客户端可能需要在它迁移到服务端的首选地址之前执行连接迁移。在这种场景下,客户端应该并行的执行从 客户端的新地址到原始地址和首选地址之间的链路校验。

如果到服务端首选地址的链路校验成功,客户端必须放弃原始地址的校验并且迁移到使用服务端的首选地址。 如果到服务端首选地址的链路校验失败,但是到原始地址的路径校验成功,客户端可能迁移到新的地址 并且继续发送到服务端的原始地址。

如果到服务端首选地址的连接并不是来自相同的客户端地址,服务端必须防御在9.3.1节和9.3.2节中描述的可能的潜在攻击者。除了有意的迁移之外,这也可能因为客户端的访问网络使用了针对服务端首选地址的不同NAT绑定,才会发生。

服务端应该在接收到来自不同地址的探测包的时候,发起到客户端新地址的链路校验。 服务端禁止在链路校验完成前向新的地址发送超过最小拥塞窗口的值的非探测包。

迁移去新地址的客户端应该使用与服务端相同地址族的首选地址。

9.7 IPv6流标签与迁移的使用

使用IPv6发送数据的端点应该应用符合[RFC6437]的 IPv6 流标签,除非本地的API不允许设置IPv6流标签。

IPv6流标签应该是一个源与目标地址、 源与目标UDP端口以及目标CID的伪随机函数。 流标签的生成必须设计为让与之前使用过的流标签的可连接性最小化,因为这将使得在多个链路上进行关联行为。 (详见9.5节)

一种可能的实现是将流标签作为源和目标地址、 源和目标UDP端口, 目标CID以及本地密钥的加密哈希函数来计算。

10 连接终止

一个已经建立好的QUIC连接可以用三种方式终止。

  • 空闲超时(10.2节)
  • 立即关闭(10.3节)
  • 无状态重置(10.4节)
    如果端点没有可以发送数据包的已校验链路,它可以抛弃连接状态(见8.2节)。

10.1 关闭中和释放中连接状态

关闭中和释放中状态是为了确保干净地关闭连接,并确保延迟或乱序的包被正确丢弃。 这些状态应该保持至少三倍于在[QUIC-RECOVERY] 中定义的探测超时(PTO)间隔。

当发起立即关闭流程(10.3节)时,端点进入关闭中阶段。在关闭中,端点禁止发送数据包,除非包含CONNECTION_CLOSE帧(详见10.3节)。 端点只保留足够的信息用于生成包含CONNECTION_CLOSE帧的包,以及来识别数据包是否属于该连接。端点选择的连接ID和QUIC版本就足够 来识别包属于哪个关闭中的连接;端点可以丢弃其他所有的连接状态。端点可能为读取和处理CONNECTION_CLOSE帧而保留 传入包的包保护密钥。

一旦端点收到它的对端进入关闭中或释放中状态的信号时,端点即进入释放中状态。同关闭中状态一致,处于释放中状态的端点 禁止发送任何包。一旦连接进入释放中状态,就没有必要再保存包保护密钥。

端点在收到CONNECTION_CLOSE帧或无状态重置时可能会从关闭中状态转换到释放中状态,这两种情况都表明对端也在 关闭中或释放中。当关闭中周期结束的时候释放中周期也必须结束。也就是说,端点可以使用相同的结束时间, 但是停止重传关闭中包。

在关闭中或释放中周期结束之前处理连接状态,会引起延迟或重排序的数据包产生不必要的无状态重置。
端点有某种替代方法来确保连接上延迟到达的数据包不会引起响应,比如可以关闭UDP套接字,可以使用简短的释放周期来更快的恢复资源。保留一个打开的套接字来接收新连接的服务端,不应该提早退出关闭中和释放中周期。

一旦关闭中或释放中周期结束了,端点应该丢弃此连接的所有状态。这样这个连接上新的包就能被常规处理。 比如,终端可能发送无状态重置来响应之后传入的包。

当无状态重置(10.4节)被发送时,释放中和关闭中状态不适用。

当端点处于关闭中或释放中状态时,端点不需要处理密钥的更新。密钥的更新会阻止端点从关闭中状态转换到释放中状态,

处于关闭中或释放中的终端不需要处理 (密钥?健?) 更新。 Key更新可能会阻止终端从关闭中状态转向释放中状态, 但否则没有任何影响。

端点在关闭中周期时可能接收到来自新源地址的数据包,表明连接迁移(9节)。处于关闭状态的端点 必须严格限制发送到此新地址的包的数量, 直到该地址经过(参见8.2节)。处于关闭中状态的端点可以选择丢弃从新地址 接收来的包。

10.2 空闲超时

如果启用了空闲超时,则当连接持续空闲时间超过预公布的空闲超时 (参见Section 18.1)和三倍 当前探测超时(PTO)的最大值时会被静默关闭,并且连接的 所有状态都会被抛弃。

每个终端都会给对端公布自己的闲置超时。终端 在收到并成功处理其对端发送的包后重置自己维护的 任何计时器。当收到一个包且没有发送其他ACK诱发包时, 发送任何不包含ACK或PADDING (一个ACK诱发包,参见[QUIC-RECOVERY]) 帧的包也会重置计时器。发送包的时候重置计时器 能确保在启动新活动时连接不会过早的超时。

闲置超时的值可以是不对等的。终端公布的值 仅仅时用来确定此终端上的连接是否 处于活动状态。终端在对端闲置超时周期快要结束的时候发送 的包可能会因为对端在收到包之前就进入释放状态而被丢弃。 如果对端可能在一个探测超时(PTO,参见[QUIC-RECOVERY]) 时间内超时,建议在发送任何不能安全重发的数据之前 测试连接的活动状态。

如果启用了空闲超时,则当连接保持空闲时间超过声明的的空闲超时 (见18.2节)和当前探测超时(PTO)的三倍时,会被静默关闭,并且连接的所有状态都会被抛弃。

每个端点会向对端声明自己的空闲超时。终端在收到并成功处理其对端发送的包后重启自己维护的任何计时器。当发送ack诱发包(参见[QUIC-RECOVERY])时,计时器也会重启,但前提是自从上次接收到数据包以来没有发送过其他ACK诱发包。帧的包也会重置计时器。发送包的时候重置计时器能确保在发起新活动时连接不会过早的超时。

空闲超时的值可以是不对等的。端点声明的值仅仅是用来确定此端点上的连接是否处于活动状态。端点在对端闲置超时周期快要结束的时候发送 的包可能会因为数据包到达之前对端就进入释放状态而被丢弃。 如果对端可以在一个探测超时(PTO,参见[QUIC-RECOVERY]) 时间内超时,建议在发送任何不能安全重发的数据之前 测试连接的活动状态。注意有可能只有应用或者应用协议会知道什么信息可以被重发。

10.3 立即关闭

端点可以发送CONNECTIN_CLOSE帧(19.19节)来立即终止连接。CONNECTION_CLOSE帧会让所有的流马上被关闭;可以认为打开的流会被隐式重置。

端点在发送CONNECTION_CLOSE帧之后马上进入关闭中状态。 在关闭中周期中,发送CONNECTION_CLOSE的终端应该在响应任何接收到的包的同时发送包含CONNECTION_CLOSE帧的包。为了最小化端点在关闭中维护的状态,端点可能重复发送完全一样的包。 但是端点应该限制包含CONNECTION_CLOSE帧的包数量。 例如,在发送附加数据包或者增加数据包之间的时间间隔之前,端点可以逐渐增加接收的包的数量。

注意:允许重传关闭中的包和本文档中为每个包创建新包编号的建议相矛盾。发送新的包编码主要有利于丢失恢复和拥塞控制,这与关闭的连接不相关。 重传最终包所需要更少的状态。

从未验证的地址发来的包会被用来创建放大攻击(参见8节)。为了避免此攻击,端点必须对发送到验证过的地址CONNECTION_CLOSE帧做限制,或者如果响应比接收到的包大小大三倍,端点不响应直接丢弃数据包。

端点在收到CONNECTION_CLOSE帧以后进入释放中状态。收到CONNECTION_CLOSE的端点可能在进入释放中状态之前发送一个包含CONNECTION_CLOSE的单个包,如果适合 可以使用CONNECTION_CLOSE帧和NO_ERROR码。终端禁止发送更多的包,因为这会导致直到在任意对端的关闭中周期结束,双方不停的交换CONNECTION_CLOSE帧。

在应用协议安排关闭连接之后可以使用立即关闭。 这可能是在应用协议协商优雅关闭之后。在应用程序请求关闭连接之后,应用协议交换了两端所需的信息,使得两端同意关闭连接。应用协议可以使用带有适当错误码的CONNECTION_CLOSE帧来指示关闭。

当发送CONNECTION_CLOSE帧,目标是确保对端可以处理该帧。一般地,这意味着发送带有最高级别的包保护的数据包中的帧,来避免数据包被丢弃。但是,握手期间,有可能更高级的包保护密钥对对端是不可用的,因此的该帧可以被复制到在使用更低包保护级别的包中。

在握手被确认之后,端点必须发送任何在1-RTT的包中的CONNECTION_CLOSE帧。在握手确认之前,对端可能没有1-RTT密钥,因此端点应该在握手包中发送CONNECTION_CLOSE帧。如果端点没有握手密钥,它应该在初始包中发送CONNECTION_CLOSE帧。

客户端总是知道服务端是否有握手密钥(17.2.2.1节),但是可能服务端并不知道客户端是否有握手密钥。在这种情况下,服务端应该在握手和初始包中发送CONNECTION_CLOSE帧,来确保至少一种可以被客户端处理。这些数据包可以合并为单个UDP报文(见12.2节)。

10.4 无状态重置

无状态重置是提供给无法访问连接状态的端点的最后选项。崩溃或中断可能导致对端继续向无法继续正常连接的端点发送数据。 端点可以发送无状态重置来响应接收到无法与活动的连接相关联的数据包。无状态重置不适用于发出错误条件的信号。如果端点有足够的状态,则想要传达致命连接错误的端点必须使用CONNECTION_CLOSE帧。

为了支持这个过程,端点发送一个令牌。令牌在NEW_CONNECTION_ID帧的Stateless Reset Token字段中携带。服务器可以在握手期间指定stateless_reset_token传输参数,并应用到握手时选择的连接ID上。
客户端不能使用该传输参数,因为他们的传输参数没有加密性保护。令牌受加密保护,因此只有客户机和服务器知道此值。 当令牌关联的连接ID通过RETIRE_CONNECTION_ID帧 (19.16节)停用时,令牌将失效。

接收到无法处理的数据包的端点发送数据包的布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1| Unpredictable Bits (38 ..) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ Stateless Reset Token (128) +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图6: 无状态重置包

这种设计确保无状态重置包尽可能与具有短报头的常规包没有区别。
无状态重置使用整个UDP数据报,从数据包头的前两位开始。 第一个字节的剩余部分及其后的任意数量的字节被设置为不可预测的值。数据报的最后16个字节包含一个无状态重置令牌。

对于除了指定接收者外的实体,无状态重置则是一个短头部的数据包。对于无状态重置,为了表现像一个有效的QUIC包,不可预测位字段需要包含至少38位数据(或者5字节,减去2个固定位)。

21字节的最小尺寸不能保证无状态重置难以与其他包做区分,如果接收方要求使用连接ID。为了避免无状态重置与有效数据包区分开,所有端点发送的数据包应该被填充到比端点可能使用的最小连接ID长至少22字节的长度。端点发送无状态重置来响应43字节或更小长度的包,应该发送比其响应的数据包短1字节的无状态重置。

这些值假设无状态重置令牌与最小包保护AEAD扩展相同。如果端点已经协商使用了更大的最小AEAD扩展的包保护方案,则需要额外的不可预测字节。

终端禁止发送比其接收的包大3倍及以上的无状态重置,来避免被用作放大攻击。10.4.3节描述了对于无状态重置大小的附加限制。

端点必须丢弃太小而不能作为有效的QUIC包的包。 使用[QUIC-TLS]中定义的一组AEAD函数,小于21字节的包永远无效。

端点必须发送短头部数据包格式的无状态重置包。但是端点必须将任何有效的无状态重置令牌为结尾的数据包当作无状态重置,因为其他QUIC版本可能循序使用长头部。

端点可以发送无状态重置来响应具有长报头的包。在无状态重置令牌可用于对端之前,发送无状态重置无效。在这个QUIC版本中,只有在建立连接时才使用长报头的数据包。由于无状态重置令牌在连接建立完成或接近完成之前不可用,因此忽略具有长报头的未知包可能与发送无状态重置一样有效。

端点无法从具有短报头的包中确定源连接ID,因此它无法在无状态重置包中设置目标连接ID。 因此目标连接ID将与前面包中使用的值不同。 随机目标连接ID使连接ID看起来是迁移到使用NEW_CONNECTION_ID帧 (19.15节)提供的新连接ID的结果。

使用随机连接ID会导致两个问题:

  • 数据包可能无法到达对端。如果目标连接ID对于路由到对端非常重要, 则此包可能被错误路由。这还可能触发另一个无状态重置作为响应,参见10.4.3节。 没有正确路由的无状态重置是无效的错误检测和恢复机制。 在这种情况下,端点将需要依赖于其他方法(例如计时器)来检测连接是否失败。
  • 随机生成的连接ID可用于对端以外的实体,以将其标识为潜在的无状态重置。偶尔使用不同连接id的终端可能会对此带来一些不确定性。

这种无状态重置设计是特定于QUIC版本1的。 支持多个QUIC版本的端点需要生成一个无状态重置, 该重置将被支持任何(端点可能支持的)版本(或在丢失状态之前可能已经支持的版本)的对端接受。 新版本的QUIC的设计者需要意识到这一点,或者重用这个设计, 或者使用包的一部分而不是最后16个字节来携带数据。

10.4.1 检测无状态重置

端点使用16字节结尾的UDP报文来检测潜在的无状态重置。对于最近发出的报文,端点记住所有的与连接ID及远端地址相关的无状态重置令牌。这包括来自NEW_CONNECTION_ID帧的无状态重置令牌,和服务端传输参数,除了未被使用的或者停用的与连接ID相关联的无状态重置令牌。

端点通过比较报文最后16字节与报文接收地址相关联的无状态重置令牌,来将接收到的报文识别为无状态重置。
每次传入数据包都可以执行比较。端点可以跳过该检查,如果任何来自报文的数据包都被成功处理了。但是当传入报文中的第一个包不能与连接相关联或者解密时,比较必须执行。

端点禁止对任何未使用和停用的与连接ID相关联的无状态重置令牌进行检查。

当将报文与无状态重置令牌值比较时,端点必须执行没有令牌信息泄露的比较。例如,恒定时间内的比较,可以保护各个无状态重置令牌的值免于由计时侧通道的信息泄露的影响。另一个方法是存储并比较转换过的无状态重置令牌值,而不是原始值,转换定义为使用密钥的密码安全伪随机函数 (即, 分组密码, HMAC [RFC2104])。端点不会保护关于是否包被成功解密或者有效的无状态重置令牌数量的信息,

如果报文最后的16字节与无状态重置令牌的值相同,端点必须进入释放周期并不再在连接上发送数据包。

10.4.2 计算无状态重置令牌

无状态重置令牌必须是很难猜测的。 为了创建无状态重置令牌,端点可以为它创建的每个连接随机生成[RFC4086]一个密码。 然而,当集群中有多个实例或出现端点可能会丢失状态的存储问题时,就会出现协作问题。无状态重置专门用于处理状态丢失的情况,所以这种方法不是最优的。

以静态密钥与端点选择的连接ID为输入,通过使用抗原像性的二次迭代函数生成证明,单个静态密钥可用于到同一终端的所有连接,(请参见5.1节)。 终端可以使用HMAC [RFC2104] (例如,HMAC(static_key, connection_id)) 或HKDF RFC5869。此函数的输出被截断为16字节,以生成该连接的无状态重置令牌。

丢失状态的端点可以使用相同的方法生成有效的无状态重置令牌。连接ID来自端点接收的包。

这种设计依赖于对端总是在其包中发送连接ID,以便端点可以使用包中的连接ID重置连接。使用此设计的端点必须对所有连接使用相同的连接ID长度,或者对连接ID的长度进行编码,以便可以在没有状态的情况下恢复连接ID。此外,它不能提供零长度的连接ID。

公开的无状态重置令牌允许任何实体终止连接,因此值只能使用一次。 这种选择无状态重置令牌的方法意味着连接ID和静态密钥的组合不能给另一个连接使用。 如果共享静态密钥的实例使用相同的连接ID, 或者如果攻击者可以导致数据包路由到只有相同静态密钥(请参阅21.8节)的实例,则可能发生拒绝服务攻击。 通过显示无状态重置令牌重置的连接的连接ID禁止用于共享静态密钥的节点上的新连接。

相同无状态重置令牌禁止被用于多个连接ID。不要求端点对新值与之前所有值进行比较,但是重复值可能会被当做PROTOCOL_VIOLATION类型的连接错误。
请注意,无状态重置包没有任何加密保护。

10.4.3 循环

无状态重置包的设计使得在不知道无状态重置令牌的情况下,它与有效数据包无法区分。例如, 如果一台服务器向另一台服务器发送无状态重置包,它可能会收到另一台无状态重置包作为响应 ,这可能导致无限的交换。

端点必须确保它发送的每个无状态重置包都小于触发它的数据包,除非它保持足以防止循环的状 态。在发生循环的情况下,数据包会因为太小而无法触发响应。

端点可以记住已发送的无状态重置数据包的数量,并在达到限制后停止生成新的无状态重置数据 包。对不同的远程地址使用单独的限制将确保在其他对端或连接耗尽限制时可以使用无状态重置数据包来关闭连接。

将无状态重置包的大小减小到建议的最小大小41字节以下,这意味着数据包可能会向观察者显示 它是无状态重置包,基于对端连接ID的长度。相反,拒绝响应小数据包而发送无状态重置包可能导致无状态重置包无法在 仅发送非常小的数据包导致的中断连接的情况下使用;此类故障可能只能通过其他方式来检测,如计时器。

11 错误处理

检测到错误的端点应该将该错误的存在通知其对端。传输级和应用级错误都会影响整个连接 (参见11.1节),而只有应用级错误才能被隔离到单个流中(参见11.2节)。

发出错误信号的帧中应该包含最合适的错误代码(20节)。在规范标识错误条件的地方,还标识了所使用的错误码。尽管这些成文的要求,不同的实现策略可能导致报告不同的错误。特别地,当检测到错误条件,端点可能使用任何适用的错误码;宽泛的错误码(比如PROTOCOL_VIOLATION或INTERNAL_ERROR)总是可以用于替换特定错误码。

无状态重置(10.4节)不适用于可以用CONNECTION_CLOSE或RESET_STREAM帧发出信号的任何错误。处于在连接上发送帧所需状态的端点禁止使用无状态重置。

11.1 连接错误

导致连接不可用的错误,如明显违反协议语义或影响整个连接的状态损坏,必须使用CONNECTION_CLOSE帧发出信号(19.19节)。即使错误只影响单个流,端点也可能以这种方式关闭连接。

应用协议可以使用CONNECTION_CLOSE帧的特定于应用的变体发送特定于应用的协议错误信号。特定于传输的错误,包括本文档中描述的所有错误,由CONNECTION_CLOSE帧的特定于QUIC的变体所携带。

可以在丢失的数据包中发送CONNECTION_CLOSE帧。如果端点在终止的连接上接收到更多的数据包 ,则应该准备重新传输包含CONNECTION_CLOSE帧的数据包。限制重新传输的次数和发送此最终数据包的时间,限制了在已终止的连接上所花费的工作努力。

选择不重新传输包含CONNECTION_CLOSE帧的数据包的终端,有其对端丢失第一个这样的数据包的风险。对于继续接收终止连接数据的端点来说,唯一可用的机制是使用无状态重置过程(10.4节)。

接收无效的CONNECTION_CLOSE帧的端点禁止向其对端发出错误存在的信号。

11.2 流错误

如果应用级别的错误影响了单个流,然而其他方面使连接处在可恢复状态,则端点可以发送包含适当错误代码的RESET_STREAM帧(19.4节),以终止受影响的流。

RESET_STREAM必须由使用QUIC的协议发起,RESET_STREAM帧携带应用错误代码。只有应用协议可以使流变为终止状态。应用协议的本地实例使用一次直接API调用,远端实例则使用STOP_SENDING帧,来自行触发RESET_STREAM帧。

在不知道应用协议的情况下重置流可能会导致协议进入不可恢复状态。应用协议可能需要可靠地传输某些流,以确保端点之间的状态一致。应用协议应该定义规则来处理被任意端永久取消的流。

12 数据包与帧

QUIC端点通过交换数据包来通信。数据包具有机密性和完整性保护(参见12.1节) ,并在UDP数据报中携带(参见12.2节)。

此版本的QUIC在连接建立过程中使用长数据包报头(参见17.2节)。具有长报头的数据包是初始数据包(17.2.2节)、0-RTT数据包(17.2.3节)、握手数据包(17.2.4节)和重试数据包(17.2.5节)。版本协商使用带有长报头的独立 于版本的数据包(参见17.2.1节)。

具有短报头(17.3节)的数据包是为了最小的开销而设计的,在建立连接和1-RTT密钥可用后使用。

12.1 受保护的数据包

除版本协商和重试数据包外,所有QUIC数据包都使用带有附加数据 (AEAD) [RFC5116]的身份 验证加密,以提供保密性和完整性保护。数据包保护的详细信息可在[QUIC-TLS]中找到,本节包括此过程的概述。

初始数据包使用静态派生的密钥进行保护。此数据包保护不是有效的机密性保护。初始包保护的存 在是为了确认数据包的发送方位于网络链路上。从客户端接收初始数据包的任何实体都可以恢复用于卸载 数据包保护或生成能通过验证的数据包所需的密钥。

所有其他数据包都使用从加密握手数据包派生的密钥进行保护。来自长报头的数据包类型或来自 短报头的密钥段用于标识所使用的加密级别(因此也就是密钥)。使用0-RTT和1-RTT密钥保护的 数据包应该具有机密性和数据来源身份验证;加密握手数据包确保只有通信中的端点接收相应的密钥。

12.2 合并数据包

初始包(17.2.2节)、0-RTT包(17.2.3节)和握手包(17.2.4节)包含一个长度字段, 用于确定包的结尾。该长度包括包编号和载荷字段,这两个字段都经过加密保护且初始长度未知。当卸载报头保护后, 就可以知道载荷字段的长度。

通过使用长度字段,发送方可以将多个QUIC包合并为一个UDP数据报。这可以减少完成加密握手和开始发送数据所需的UDP数据报数。这可以用于创建PMTU探测(见14.3.1节) 接收方必须能够处理经过合并处理的包。

按照加密级别增加(初始、0-RTT、握手、1-RTT)的 顺序合并数据包,能使接收方更有 可能处理在单次传输中所有的包。 短报头的包的头部不包含长度字段, 因此它只能是UDP数据报中包含的最后一个包。

按照递增的加密级别(初始包,0-RTT,握手包,1-RTT包)来合并数据包可以使接收方能够在一次传输中处理所有数据包。短报头的数据包不包含长度,因此可以成为UDP数据包中的最后一个数据包。端点不应该合并多个相同加密级别的数据包。
发送方禁止将不同连接的QUIC包合并到单个UDP数据报中。接收方应该忽略任何具有与数据报中第一个包不同的目的 连接ID的后续数据包。

每个合并入单个UDP数据报的QUIC包都是独立和完整的。合并QUIC包的接收方必须单独处理每个QUIC包并分别确认它们, 就好像它们是从不同UDP数据报的负载接收的一样。例如,如果包的解密失败(因为密钥不可用或任何其他原因), 接收方可能会丢弃或缓冲数据包以供以后处理,并且必须尝试处理剩余的包。

重试包(17.2.5节)、 版本协商包(17.2.1节) 和具有短报头的包(17.3节) 不包含长度字段,因此同一UDP数据报 中这些包后面不能跟随其他包。还要注意,不存在重试包或者版本协商包与其他包合并的情况。

12.3 包编号

包的编号是介于0到2^62-1之间的整数。 这个数字用于确定用于包保护的加密随机数。 每个端点都为发送和接收维护一个独立的包编号。

包编号被限制在此范围内,是因为它们需要在ACK帧的最大已确认字段(19.3节)中完整表示。 但是,在长或短报头中表示时,包编号会缩短并编码成1到4个字节(请参见17.1节)。

版本协商(Section 17.2.1)和重试(Section 17.2.5)包不包含包编号。

在QUIC协议中包编号被分割为三个空间:

  • 初始空间:所有初始包 (Section 17.2.2) 的包编号都在这个空间。
  • 握手空间:所有握手包 (Section 17.2.4)的包编号都在这个空间。
  • 应用数据空间: 所有0-RTT和1-RTT加密后的包 (Section 12.1)的包编号都在这个空间。

正如[QUIC-TLS]中所描述的那样,每个类型的包都使用不同的保护密钥。

从概念上讲,包编号空间是一个包可以被处理和确认的上下文。初始包只能和初始包保护密钥一起发送,并且也只能在初始包中确认。同样,握手包是经过握手加密级别加密后发送的,只能在握手包中得到确认。

这强制进行了在不同包序列编号空间中发送的 数据之间的密码学分离。每个空间中的包编号从包编号0开始。随后在同一包编号空间发送的包的编号必须至少增加一。

在同一包编号空间中存在0-RTT和1-RTT数据能使得两个包类型之间的丢失恢复算法更加容易实现。

一个QUIC终端禁止在一个连接内重用同一个包编号空间内的包编号。 如果发送的包编号达到2^62-1, 发送方必须关闭连接,而无需发送CONNECTION_CLOSE帧或任何其他包; 终端可能发送一个无状态重置(Section 10.4), 以响应接收到的其他包。

接收方必须丢弃一个新的未受保护的包,除非接收方确定没有从相同的包编号空间中处理另一个具有相同包编号的包。 在由于[QUIC-TLS]第9.3节中所述的原因去除包保护后,必须进行重复抑制。在[RFC4303]的3.4.3节中可以找到一种 有效的重复抑制算法。

发送方的包编号编码和接收方的解码在Section 17.1中进行说明。

12.4 帧与帧类型

如Figure 7中所示, 移除包保护后QUIC包的负载由完整的帧序列组成。版本协商包,无状态重置包与重试包不包含帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame 1 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame 2 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame N (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图7: QUIC载荷

包含帧的数据包载荷必须至少包含一个帧,且可能包含多个帧和多个帧类型。帧的数据必须能够塞入单个QUIC包, 并且禁止跨越多个数据包。每个帧都以“帧类型”开头,表示其类型,后跟额外依赖于类型的字段:

1
2
3
4
5
6
7
8
9
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame Type (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type-Dependent Fields (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure 8: 一般帧布局

此规范中定义的帧类型在表3中列出。ACK, STREAM, MAX_STREAMS, STREAMS_BLOCKED和CONNECTION_CLOSE帧中的帧类型被用于携带其他帧限定的标识符。对于所有其他帧,帧类型字段仅用于标识该帧。 Section 19中对这些帧进行了更详细的说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
+-------------+----------------------+---------------+
| Type Value | Frame Type Name | Definition |
+-------------+----------------------+---------------+
| 0x00 | PADDING | Section 19.1 |
| | | |
| 0x01 | PING | Section 19.2 |
| | | |
| 0x02 - 0x03 | ACK | Section 19.3 |
| | | |
| 0x04 | RESET_STREAM | Section 19.4 |
| | | |
| 0x05 | STOP_SENDING | Section 19.5 |
| | | |
| 0x06 | CRYPTO | Section 19.6 |
| | | |
| 0x07 | NEW_TOKEN | Section 19.7 |
| | | |
| 0x08 - 0x0f | STREAM | Section 19.8 |
| | | |
| 0x10 | MAX_DATA | Section 19.9 |
| | | |
| 0x11 | MAX_STREAM_DATA | Section 19.10 |
| | | |
| 0x12 - 0x13 | MAX_STREAMS | Section 19.11 |
| | | |
| 0x14 | DATA_BLOCKED | Section 19.12 |
| | | |
| 0x15 | STREAM_DATA_BLOCKED | Section 19.13 |
| | | |
| 0x16 - 0x17 | STREAMS_BLOCKED | Section 19.14 |
| | | |
| 0x18 | NEW_CONNECTION_ID | Section 19.15 |
| | | |
| 0x19 | RETIRE_CONNECTION_ID | Section 19.16 |
| | | |
| 0x1a | PATH_CHALLENGE | Section 19.17 |
| | | |
| 0x1b | PATH_RESPONSE | Section 19.18 |
| | | |
| 0x1c - 0x1d | CONNECTION_CLOSE | Section 19.19 |
+-------------+----------------------+---------------+

Table 3: Frame Types

端点必须将收到未知类型的帧当做FRAME_ENCODING_ERROR类型的错误。

在此版本的QUIC协议中,所有QUIC帧都是幂等的。也就是说,一个有效的帧被接收多次时不会引起不良的副作用或错误。

帧类型字段使用可变长度整数编码(请参见Section 16),但有一个例外。为了确保简单有效地实现帧解析, 帧类型必须使用尽可能短的编码。 虽然可以对本文档中定义的帧进行2字节、4字节或8字节的编码, 但这些帧的“帧类型”字段是以单字节编码的。例如,虽然0x4001是一个值为1的可变长度整数的 合法双字节编码,但是PING帧总是编码为单字节值0x01。 端点可能将接收到编码长度超过必要的帧类型视为 PROTOCOL_VIOLATION类型的连接错误。

13 打包和可靠性

在成功去掉包保护并且处理完包中包含的 所有帧之前禁止发送包的确认。 对于STREAM帧,这意味着数据已经排队准备 由应用程序协议接收,但这不要求确认数据已 被应用接受和处理。

一旦包被完全处理,接收方通过发送一个或多个包含 所接收包的编号的ACK帧来确认包已收到。

发送方在一个QUIC包中捆绑一个或多个帧(见12.4节)。

通过将尽可能多的帧打包到一个QUIC数据包中,发送方可以最小化单包带宽和计算开销。在发送没有最大化包装的包之前,发送方可能等待短暂的时间来将多个帧打包,来避免发送大量的小数据包。协议实现可以使用应用发送行为或者启发式的相关知识,来决定是否等待后者等待多久。等待周期由协议实现决定,协议实现应该注意进行谨慎的延迟,因为任何延迟都可能增加应用上可察觉的时延。

通过将来自多个流的STREAM帧交织入单个或多个QUIC包,实现了流的复用。单个QUIC包可以包括多个来自一个或多个流的STREAM帧。

QUIC的一个好处是避免了涉及多个流的队头阻塞。当数据包丢失时,只有在该数据包中携带数据的帧才会被阻塞,来等待重传被接收,而其他流可以继续进行。注意,当来自多个流的数据被打包成单个QUIC包时,该包的丢失会阻塞所有这些流的发送进度。建议协议实现将打包尽可能少的流打包到传出的数据包中,对填充不足的数据包没有传输效率的损失。

13.1 数据包处理

在成功去除包保护并且处理完包中包含的所有帧之前,禁止发送包的确认。 对于STREAM帧,这意味着数据已经排队准备 由应用程序协议接收,但这不要求确认数据已被发送和消费。

一旦包被完全处理,接收方通过发送一个或多个包含所接收包的编号的ACK帧来确认包已收到。

13.2 生成确认(应答)

端点确认他们接受和处理的所有数据包。但是,只有只有引诱确认的数据包会导致ACK帧在最大确认延时内被发送。非引诱确认的帧只在当ACK帧为了其他原因被发送时才会确认。

因为任何原因发送数据包时,端点可以尝试打包ACK帧,如果最近还未被发送。这样做有助于对端及时的丢包检测。

一般来说,来自接收方频繁的反馈可以改善丢失与拥塞的响应,但是不得不与发送ACK帧来响应每个诱发确认包的接收方,其产生的过度负载进行权衡。下面提供了解决这个平衡的指导。

13.2.1 发送ACK帧

每个包应该至少被确认一次,诱发确认的数据包必须在最大确认延时内被确认。端点使用max_ack_delay传输参数来通信其最大延时,见18.2节。max_ack_delay宣告了一个明确的约定:端点承诺绝不会有意对诱导确认包的确认进行超过指定时间的延迟。否则,任何对RTT的过量估计,会导致来自对端的虚假或延迟重传。

对于初始和握手包,max_ack_delay为0.发送方使用接收方的max_ack_delay值来确定基于计时器的重传超时,如[QUIC-RECOVERY]5.2.1节所述

应该至少每第二个诱发确认包,产生一个ACK帧。该建议与TCP[RFC5681]的标准做法保持一致。

为了帮助发送方进行丢失检测,端点一旦接收到乱序的诱导确认包应该立刻发送ACK帧。端点在每次接收到后续帧,可以继续发送ACK帧,但是端点应该转而在1/8 x RTT时间内确认每个其他数据包,除非更多的诱导确认包被无序接收。如果每个后续的诱导确认包乱序到达,对于每个接收到的诱导ACK包应该立即发送ACK帧。

类似地,在IP头中标记有ECN Congestion Experienced (CE)码的数据包应该立即被确认,来减少对端对拥塞时间的响应时间。

作为一个优化,在发送任何ACK帧作为响应前,接收方可能处理多个数据包。这种情况下,接收方可以决定在处理传入的数据包之后,应该马上还是延迟确认。
包含PADDING帧的数据包被认为传输时用于拥塞控制目的[QUIC-RECOVERY。只有发送PADDING会导致发送方被在没有来自接收方的确认时被拥塞控制器限制,因此,发送方应确保除填充帧外还发送其他帧,以触发来自接收方的确认。

只发送ACK帧的端点不会接收来自对端的确认,除非这些确认被和诱导确认帧一起包含在数据包中。当有新的诱导确认包需要确认时,端点应该将ACK帧和其他帧捆绑。当只有非诱导确认包需要被确认时,端点可以等待直到接收到诱导确认包,来将ACK帧与其他传出帧捆绑。

[QUIC-RECOVERY]中的算法,对于不遵循上面提供的指导的接收者是弹性的。但是,实施者只有在仔细考虑了这样做的性能影响之后,才会偏离这些要求。

只包含ACK帧的数据包不受拥塞控制,因此有对他们发送的频率的限制。端点禁止发送超过一个只有ACK帧的数据包,来响应接收到的诱导确认包。端点禁止发送非诱导确认包来响应一个诱导确认包,即使在收到的数据包之前有数据包间隙。限制ACK帧避免了确认的无限反馈循环,导致连接无法变成空闲状态。但是端点发送一个ACK帧时,会确认非诱导确认数据包。

如果端点能够检测到它收到了它没有发送的包的确认,端点应该将其视为PROTOCOL_VIOLATION类型的连接错误。

13.2.2 管理ACK范围

当发送ACK帧,会包含一个及以上被确认的数据包的范围。包含较旧的数据包可减少由丢失先前发送的ACK帧导致的伪重传的机会,但以较大的ACK帧为代价。
ACK帧应该总是确认最近收到的数据包,数据包越是乱序,快速发送更新过的ACK帧就越重要,来防止对端将数据包声明为丢失并且虚假地重传它包含的帧
13.2.3节和13.2.4节描述了用于确定在每个ACK帧中确认哪些包的示例性方法。

13.2.3 接收方追踪ACK帧

当包含ACK的数据包被发送,该帧中最大确认值会被保存。当包含ACK帧数据包被确认时,接收方可以停止确认小于或等于被该发送的ACK帧中的最大确认值的数据包。

在没有ACK帧丢失的情况下,该算法允许最少1-RTT重排。在ACK帧丢失或重排的情况下,该方法不能保证在确认不再包含到ACK帧中前每个确认被发送方看到。数据包可以被乱序接收,且所有包含它们的后续ACK帧可以丢失。在这种情况下,丢失恢复算法可以引起伪造重传,但是发送方将继续进行。

13.2.4 限制ACK范围

为了限制还没有被发送方接收的ACK范围(见19.3.1节),接收方应该追踪还没有被对端确认的ACK帧。当这些已确认的数据包不必要地影响ACK帧大小的时候,接收方应该将它们从未来的ACK帧中。当接收方只发送非诱导确认包时,可以将PING帧或其他小的诱导确认包与这些帧绑定,比如每次往返时,来为之前发送的包丢弃不需要的ACK范围和任何状态。接收方禁止捆绑诱导确认帧,未来避免确认的无线反馈循环。

为了限制接收方状态或者ACK帧的大小,接收方可以限制发送的ACK范围值。接收方可以这么做甚至不需要接收它的ACK帧的确认,同时这个会导致发送方不必要的重传数据。标准QUIC算法([QUIC-RECOVERY])在足够新的数据包被确认后声明数据包丢失。因此,接收方应该重复确认刚收到的数据包,优先于过去收到的数据包。

13.2.5 测量与报告主机延时

端点特意测量当诱发确认包被接收和对应确认被发送之间的延时。端点将该延时编码设置到ACK帧中的ACK延时字段(见19.3节)。这使得ACK的接收方对任何有意的延时进行调节,这对于当确认被延迟时,获得更佳的链路RTT估计是非常重要的。 数据包在被处理之前可以在OS内核或者其他主机上持有。端点禁止包含当填充ACK帧中的确认延时字段时,不进行控制的延迟。

13.2.6 ACK帧与包保护

ACK帧必须值能在与被确认包相同的包编号空间的数据包中携带(见12.1节)。例如,由1-RTT密钥保护的数据包必须被在同样是1-RTT密钥保护的包中确认。

客户端使用0-RTT包保护发送的数据包必须由服务端1-RTT密钥保护的包中确认。这意味着客户端不能使用这些确认,如果服务端加密握手信息被延时或丢失了。注意同样的限制适用于其他由服务端发送的1-RTT密钥保护的数据。

13.3 信息的重传

被确定为已丢失的QUIC数据包不会整包重传。这同样适用于丢失数据包中包含的帧。 但是,帧中携带的信息根据需要可以在新帧中再次发送。

新帧和包用于携带确定已丢失的信息。通常,当包含该信息的包已丢失并且包含该信息的数据包被确认时发送终止了,会再次发送该信息。

  • 根据[QUIC-RECOVERY]中的规则重新发送CRYPTO帧中发送的数据, 直到所有数据都被确认。 当相应加密级别的密钥已被丢弃时, 丢弃用于初始化和握手包中CRYPTO帧的数据。

  • 在STREAM帧中发送的应用数据在新的STREAM帧中重传, 除非终端已为该流发送RESET_STREAM。 一旦终端发送了RESET_STREAM帧, 该流此后的STREAM帧都不需要了。

  • ACK帧携带最新的确认集以及来自最大确认包中确认延时,如13.2.1节所述。延迟包含ACK帧的数据包的传输或者发送旧的ACK帧会导致对端产生大量的RTT采样或者不必要的ECN关闭。

  • RESET_STREAM帧中携带的流传输,其取消的请求,被发送,直到被确认或者直到所有流数据被对端确认 (即流的发送部分处于”重置已接收”或”数据已接收”状态)。 RESET_STREAM帧中的内容在重发时禁止更改。

  • 类似地,取消流传输的请求(同STOP_SENDING帧中编码的那样)将持续发送,直到流的接收部分进入“数据已接收”或“重置已接收”状态。参考Section 3.5.

  • 连接关闭信号,包括包含CONNECTION_CLOSE帧的数据包,在检测到数据包丢失时,不会再次发送, 而是参考Section 10。

  • 当前连接最大数据以MAX_DATA帧发送。 如果包含最近发送的MAX_DATA帧的数据包被声明丢失, 或者端点决定更新限额,则在MAX_DATA帧中发送更新的值。需要注意避免频繁发送此帧,因为这样额度可能会频繁增大 并导致发送不必要的大量MAX_DATA帧。

  • 当前最大流数据偏移量在MAX_STREAM_DATA帧中发送。与MAX_DATA类似,当包含流的最新MAX_STREAM_DATA帧的数据包丢失或更新限额时,会发送更新的值,同时注意防止帧过于频繁地发送。当流的接收部分进入“大小已知”状态时, 端点应该停止发送MAX_STREAM_DATA帧。

  • 给定类型的流的限额在MAX_STREAMS帧中发送。 与MAX_DATA类似,当包含流类型帧的最新MAX_STREAMS的数据包被声明丢失时或限额被更新时,会发送更新的值,同时注意防止该帧过于频繁地发送。

  • 阻塞的信号在DATA_BLOCKED, STREAM_DATA_BLOCKED和STREAMS_BLOCKED帧中携带。DATA_BLOCKED帧含有连接范围, STREAM_DATA_BLOCKED帧含有流范围, STREAMS_BLOCKED帧限定特定流类型。 如果包含对某个范围的最新帧的数据包丢失,则会发送新帧,但仅在端点在相应限制上被阻塞时才会发送。 这些帧始终包含在帧被传输时会引起阻塞的限额。

  • 使用PATH_CHALLENGE帧的存活或链路有效性检查结果会定期发送,直到收到匹配的PATH_RESPONSE帧, 或者直到不再需要存活或链路有效性检查。PATH_CHALLENGE帧每次发送时都包含不同的有效载荷。

  • 使用PATH_RESPONSE帧的链路校验响应仅发送一次。 如果需要发起额外的PATH_RESPONSE帧,对端将发送更多的PATH CHALLENGE帧。

  • 新的连接ID在NEW_CONNECTION_ID帧中发送,如果包含它们的数据包丢失则重新传输该包。 该帧的重传携带相同的序列号值。同样,停用的连接ID在RETIRE_CONNECTION_ID帧中发送, 如果包含它们的数据包丢失则重新传输该包。

  • 如果包含NEW_TOKEN帧的包丢失了,NEW_TOKEN帧将被重传。除了直接比较帧内容之外,没有特殊的支持来检测重新排序和复制的NEW_TOKEN帧。

  • PING和PADDING帧不包含任何信息, 因此丢失的PING或PADDING帧不需要修复。

除非应用程序指定的优先级另有说明, 否则端点应该优先通过发送新数据重新传输数据(参考 Section 2.3)。

即使鼓励发送方在每次发送数据包时组装包含最新信息的帧,也不禁止丢包重传发送旧帧的副本。 接收方必须接受包含过时帧的数据包, 例如包含最大数据量比当前旧数据包中的最大数据量小的MAX_DATA帧。

在检测到丢包时,发送方必须采取适当的拥塞控制措施。 丢失检测和拥塞控制的细节在[QUIC-RECOVERY]中描述。

13.4 显式的拥塞通知

QUIC端点可以使用显式拥塞通知 (ECN)[RFC3168]来检测和响应网络拥塞。ECN允许网络节点通过在数据包的IP报头中设置码点而不是丢弃它来指示网络中的拥塞。 如[QUIC-RECOVERY]中所述, 终端通过降低响应的发送速率来对拥塞作出反应。

要使用ECN,QUIC端点首先确定路径是否支持ECN标记, 并且对端能够访问IP标头中的ECN码点。 如果ECN标记的数据包被丢弃或ECN标记在链路上被重写, 则网络链路不支持ECN。 端点在连接建立期间和迁移到新链路时会验证ECN在链路上的使用(参考Section 9)。

13.4.1 ECN计数

在接收到具有ECT或CE码点的QUIC数据包时, 可以从封闭IP数据包访问ECN代码点的已启用ECN的终端,会增加相应的ECT(0), ECT(1)或CE计数,并在随后包含这些计数 ACK帧(见Section 13.1和Section 19.3)。 请注意,这需要能够从封闭的IP数据包中读取ECN代码点, 这在所有平台上都是不可能的。

由接收方检测为重复的数据包不会影响接收方的本地ECN代码点计数。 有关安全问题,请参阅(Section 21.8)。

如果端点在IP数据包报头中收到没有ECT或CE代码点的QUIC数据包, 它将根据Section 13.2的响应, 使用ACK帧而不增加任何ECN计数。 如果端点未实现ECN支持或无法访问收到的ECN码点, 则不会增加ECN计数。

合并的数据包(参见Section 12.2) 意味着几个数据包可以共享相同的IP报头。 在相关IP报头中接收的ECN码点的ECN计数器对于每个QUIC包递增一次, 而不是每个封闭的IP包或UDP数据报。

每个数据包编号空间都保持独立的确认状态和独立的ECN计数。 例如,如果初始包,0-RTT包, 握手包和1-RTT QUIC包被合并, 则对初始和握手包的编号空间的对应的计数将递增1, 同时1-RTT包的编号空间计数加2。

13.4.2 ECN校验

故障的网络设备可能会损坏或错误地丢弃有ECN标记的数据包。为了提供存在在此类设备情况下的连接的健壮性,每个端点独立校验ECN计数值并且如果检测到错误禁用ECN。

端点对每个网络链路上发送的数据包单独校验ECN。新连接建立时,当切换到新的服务端偏好地址时,当活动的连接迁移到新的链路上时,端点在校验ECN。

即使端点在其传输的包上不使用ECN标记,端点必须提供对从对端接收的ECN标记的反馈,如果它们是可访问的话。报告ECN计数值失败会导致对端禁用ECN标记。

13.4.2.1 发送ECN标记

为了开始ECN校验,端点应该在到对端的新链路发送包时做以下操作:

  • 对在到对端的新链路上发送的传出数据包,设置其IP头中的码点ECT(0)。
  • 如果所有带有ECT(0)的被发送的数据包最后被认为丢失了[QUIC-RECOVERY],校验就被认为失败了。

为了减少将拥塞丢失误解为由故障网络元素丢弃数据包的几率,端点可以在链路上的前十个输出包中设置ECT(0)码点,或者在3个RTT周期,以先发生为准。

协议实现可以试验并使用其他策略来使用ECN。其他探测ECN支持链路的方法是可能的,不同标记策略也是可能的。协议实现也可以使用ECT(1)码点,如[RFC8311]中指定的。

13.4.2.2 接收ACK帧

在传输的数据包设置ECT(0) 或 ECT(1)码点的端点,必须使用下面几步来接收ACK帧进行ECN校验。

  • 如果刚确认端点发送的数据包,其设置了ECT(0)或ECT(1)码点,如果没有在ACK帧中出现ECN反馈,校验失败。这一步骤保护网络元素将ECN位清零,以及不能访问ECN标记的对端,在这些情况下,对端可以没有ECN反馈的情况进行响应
  • 对于校验成功,总的ECT(0), ECT(1)和CE计数的增量必须不小于发送的在ACK帧中新确认的带有ECT码点的QUIC包的数量。这一步骤检测任何从ECT(0), ECT(1), 或CE码点到Not-ECT的网络标记
  • 任何在ECT(0)或ECT(1)计数值的增加,加上任何在CE计数的增加,必须不小于发送的,带有对应ECT码点, 在ACK帧中被新确认的数据包的数量。这一步检测任何从ECT(0)到ECT(1) (或者反过来)错误网络标记

乱序处理ECN计数会导致校验失败。端点如果ACK帧没有预先确认连接上的最大包编号,不应该执行校验。

当ACK帧丢失时断电可以不对数据包进行确认。因此有可能ECT(0), ECT(1),和CE计数总的增量会比ACK帧中已确认的数据包数量大。当发生这种情况时,如果校验成功,本地引用的计数值必须增加到ACK帧中匹配的计数值。

13.4.2.3 校验结果

如果校验失败了,端点在后续的IP包中停止发送ECN标记,以期望网络链路或对端不支持ECN。

一旦成功校验,端点在后续包可以继续设置ECT码点,以期望链路是有ECN能力的。但是网络路由和链路元素可以改变中间连接;端点必须禁用ECN如果在连接的任何点校验失败。

即使校验失败了,端点可以在任意时间后,相同链路上重新校验连接中的ECN。

14 数据包大小

QUIC包大小包括QUIC头和被保护的载荷,但是不包括UDP或者IP报头。

客户端必须确保在单个IP数据包中发送第一个初始数据包。类似地,在接收重试数据包后发送的第一个初始数据包必须在单个IP数据包中发送。

携带第一个初始包的UDP数据报的载荷必须通过向初始包添加PDDING帧和/或通过将初始包合并(参见Section 12.2)来扩展到至少1200字节。 发送此大小的 UDP 数据报可确保网络链路支持合理的最大传输单元(MTU), 并有助于降低服务器对未经校验的客户端地址的响应所导致的放大幅度攻击,参见Section 8。

如果客户端认为链路最大传输单元(PMTU)支持其选择的大小,则包含来自客户端的第一个初始数据包的数据报可能超过1200字节。

如果UDP数据报小于1200字节,服务器可以发送错误码为PROTOCOL_VIOLATION的CONNECTION_CLOSE帧,来响应它从客户端接收到的第一个初始包。它禁止发送任何其他帧类型作为响应,否则会表现为似乎有问题的数据包的任何部分被当做有效数据处理。

在验证客户端地址之前,服务器还必须限制其发送的字节数,请参见Section 8。

14.1. 链路最大传输单元(PMTU)

PMTU是整个IP数据包的最大大小,包括IP报头、UDP报头和UDP载荷。 UDP载荷包括QUIC数据包头、受保护的载荷和任何身份验证字段。PMTU可以依赖于当前链路特性。 因此,协议实现将发送的当前最大UDP载荷称为QUIC最大包大小(QUIC Maximum Packet Size)。

QUIC依赖于至少1280字节的PMTU。 这是IPv6最小大小[RFC8200],大多数现代IPv4网络也支持这一点。所有QUIC数据包(PMTU探测数据包除外)的大小都应该调整为适合最大数据包大小,以避免数据包被分段或丢弃[RFC8085]。

端点应该使用数据报包化层PMTU发现([DPLPMTUD])或 实现链路MUT发现(PMTUD)[RFC1191] [RFC8201],以判定到目的地的链路是否支持所需的消息大小而不会出现分段。

在没有这些机制的情况下,QUIC端点不应该发送大于1280字节的IP数据包。假设最小IP报头大小,这将导致IPv6的QUIC 最大数据包大小为1232字节,IPv4 的最大数据包大小为1252字节。 QUIC 实现在计算 QUIC 最大分组大小时可能更为保守,以允许未知的隧道开销或IP报头选项/扩展。

每对本地和远程地址可以具有不同的PMTU。因此,实现任何类型的PMTU发现的QUIC实现应该为每个本地和远程IP地址组合维护最大数据包大小。

如果QUIC端点确定任何一对本地和远程IP地址之间的PMTU已降至支持允许的最小最大数据包大小所需的大小以下, 则它必须立即停止在受影响路径上发送QUIC数据包(PMTU 探测数据包除外)。 如果找不到替代路径,则端点可以终止连接。

14.2 ICMP包太大消息(ICMP Packet Too Big Messages)

PMTU发现[RFC1191] [RFC8201] 依赖于IMCP消息的接收,该数据包表明当数据包由于比本地路由MTU大而被丢弃(例如: IPv6 包太大消息)。DPLPMTUD也可选的使用这些信息。ICMP消息的这种用法可能容易受到链路外攻击, 这些攻击成功猜测到了链路上使用的地址, 并且使PMTU降低到带宽低效值。

端点必须忽略要求PMTU降低到1280字节以下的ICMP消息。

生成ICMP([RFC1812],[RFC4443])的要求说明,所引用包应该在不超过对于这个IP版本的最小MTU的情况下 尽可能多的包含原始包。所引用包的大小实际上可以更小,或者是不可理解的信息,如 [DPLPMTUD] 1.1章节中的描述。

QUIC端点应该校验ICMP消息来防止在[RFC8201]以及[RFC8085]中5.2章节中指出的链路外注入。 这个校验应该使用在 ICMP载荷中提供的被引用的包来将这条消息与相对于的传输连接关联 [DPLPMTUD]。

ICMP消息校验必须包括匹配IP地址和UDP端口[RFC8085],可能的话,还要包括对应一个活动的QUIC会话的连接ID。

可以提供如下更多的校验:

  • IPv4端点可能在小比例的包中设置禁止分片(DF)位, 以便尤其当没有DF的包时,若大部分的不可用ICMP 消息抵达了,这些包因此可能被识别为可疑的。
  • 端点可以存储IP或者UDP包头中的附加信息用于校验(例如,IP ID或者UDP校验和)。

端点应该忽略所有的校验不通过的ICMP消息。

端点禁止基于ICMP消息增大PMTU。直到QUIC丢包检测算法确定所引用包已经丢失为止,任何QUIC 最大包大小的减小都可能是临时的。

14.3. 数据包包装层PMTU发现(Datagram Packetization Layer PMTU Discovery)

[DPLPMTUD]中6.4 章节中提供了实现QUIC数据包装层PMTUD(DPLPMTUD)的思考。

当实现在[DPLPMTUD]中5.3章节描述的算法时候,BASE_PMTU的初始值应该和最小QUIC包大小一致。 (IPv6为1232 字节而 IPv4位1252字节)

PING和PADDING帧可用于生成PMTU探测包。如果包含他们的探测包丢失,这些帧可能不会重传。 然而,这些帧确实消费了拥塞窗口, 这可能会延迟后续应用数据的传输。

PING帧可以被包含在一个 PMTU 探测中, 用于确保一个可用的探测已经被确认了。

如果这些消息被DPLPMTUD使用,则在之前的章节关于处理ICMP消息的思考也适用。

14.3.1 包含源连接ID的PMTU探测

依靠目标连接ID来路由QUIC数据包的端点,可能要求在PMTU探测包中包含连接ID来将任意最后的IMCP信息(14.2节)路由回正确的端点。但是,只有长头部数据包(17.2节)包含源连接ID,一旦握手完成,长头部数据包不会被对端解密或者确认。一种创建PMTU探测的方法是在单个UDP数据报中将握手包(17.2.4节)与短头部包合并(12.2节)。如果UDP数据报到达端点,握手包应该被忽略,但是短头部包应该被确认。如果UDP数据报发出ICMP消息,在一部分被引用的UDP报文中,该消息将可能包含源连接ID。

15 版本

QUIC 版本用一个32位的无符号整数标识。

版本 0x00000000 保留用于代表版本协商。 本规范的版本用 0x00000001 来标识。

其他版本的 QUIC 可能有和此版本不同的属性。 QUIC保证在所有协议版本中都一致的属性描述在 [QUIC-INVARIANTS]中。

版本 0x00000001 的 QUIC 使用 TLS 作为 加密握手协议,如[QUIC-TLS]中所描述。

版本号中最高16位被清除的版本将保留 用于未来 IETF 协商一致的文档。

遵循0x?a?a?a?a规律的版本保留用于将要实践的强制版本协商。 即,任何所有比特中的低四位是1010(二进制)的版本号。 客户端或者服务器可以建议支持这些保留的任意版本。

保留的版本号可能永远不代表一个真实的协议; 客户端可以在预期服务端将初始化版本协商的情况下使用这些中任意一个; 服务端可以建议支持这些版本中的一个, 并且预期客户端会无视这个值。

[[RFC 编辑者: 请在发布之前删除此章节。]]

此草案的最终版本号为0x00000001, 保留此版本用于此草案作为 RFC 公布时的版本。

用于标识 IETF 草案的版本号可用 草案数字加上0xff000000得到。 例如draft-ietf-quic-transport-13 可能标识为 0xff00000D.

鼓励实现者注册用于私有实验的QUIC的版本号 到Github wiki,地址为 https://github.com/quicwg/base-drafts/wiki/QUIC-Versions.

16 可变长度整型编码

QUIC包和帧通常对非负整数使用可变长度编码。这种编码确保了小整数值需要更少的空间来编码。

QUIC 可变长度整数编码保留了第一个字节的两个最高有效位,用于保存对整数长度取以2为底数的对数。 整数值以网络字节顺序编码在剩余位中。

这意味着整数编码成了1,2,4或者8字节,对应编码了6,14,30或者62位的值。表总结了编码规则。

+------+--------+-------------+-----------------------+
| 2Bit | Length | Usable Bits | Range                 |
+------+--------+-------------+-----------------------+
| 00   | 1      | 6           | 0-63                  |
|      |        |             |                       |
| 01   | 2      | 14          | 0-16383               |
|      |        |             |                       |
| 10   | 4      | 30          | 0-1073741823          |
|      |        |             |                       |
| 11   | 8      | 62          | 0-4611686018427387903 |
+------+--------+-------------+-----------------------+

         表4: 整型编码总结

例如,八字节序列c2 19 7c 5e ff 14 e8 8c(十六进制) 解码为十进制值151288809941952652; 四字节序列9d 7f 3e 7d解码为494878333; 二字节序列7b bd解码为15293; 单字节序列25解码为37。(同二字节序列40 25的值)

错误码(20节)和版本(15节)使用整数描述,但不使用这种编码。

17 数据包格式

所有数值以网络字节顺序(即大端)编码, 所有字段大小均以位为单位。使用十六进制描述字段的值。

17.1 数据包编号的编码与解码

包编号是从0到2^62-1的数字(12.3节)。在长或短包头中,以1到4个字节进行编码。 通过包含包编号的最低有效位来减少表示包编号的位数。

编码后的包编号被保护,如[QUIC-TLS]的5.4章中所描述的。

发送方使用包编号必须能够表示,最大的已确认包的编号大小和已发送包数量之间两倍以上的范围。

接收包的对端将正确地解码该包编号,除非该包延迟了,使得它在许多较高编号的包之后到达。端点应该使用足够大的包编号编码,使得包编号可以被恢复, 即使是此包在它之后发送的包之后才到达。

所以,包编号编码的大小至少比包含新包在内的连续的未确认的包数量的以2为底的对数多一位。 log2(len(包含新包在内的连续的未确认的包))+1。

例如,如果一个端点收到了0xabe8bc包的确认,在发送编号为0xac5c02的包的时候需要16位以上 的包编号编码;发送编号为0xace8fe就需要24位包编号编码。

在接收方,在恢复完整的包编号之前要移除对包编号的保护。然后,根据存在的有效位的数量、这些位的 值以及在成功验证的数据包上接收的最大数据包数,重新构造完整的数据包编号。成功移除包保护必须恢复完整的包编号。

一旦包头保护被移除,就可以通过查找最接近下一个预期包的包编号来解码包编号。下一个 预期的包是接收到的最高包编号加1。例如,如果成功校验的包中最高包编号为0xa82f30ea, 包含16位编码0x9b32的包编码将被解码位0xa82f9b32。 包解码的为代码示例在 Appendix A中提供。

17.2 长头部数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|1|1|T T|X X X X|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图 9: 长头部数据包格式

长包头用于在1-RTT密钥建立前发送的包。 一旦满足两个条件,发送方切换到使用短包头来发送包(17.3节)。长格式允许特殊的包 - 比如版本协商包 - 以这样统一固定长度的包格式进行表示。使用长包头的包包含如下字段:

头部形式(Header Form): 字节0(第一个字节)最高有效位(0x80)设置为1代表长包头。

固定位(Fixed Bit):字节0中的下一个位(0x40)被设置成1。这一位为0的包在这个版本下是无效的并且必须被丢弃。

长包类型(Long Packet Type)(T):字节0接下来的两位(掩码0x30)包含包类型。 包类型在表5中列出。

类型限定位(Type-Specific Bits)(X):字节0较低的4位(掩码0x0f)是类型限定位。

版本(Version):QUIC版本是一个紧跟着第一个字节的32位字段。这个字段指定了正在使用的QUIC版本,并确定了剩余协议字段如何解释。

DCID长度: 紧接着版本之后的字节包含目标连接ID字段的字节单位的长度。该长度被编码为一个8位无符号整数。在QUIC版本1中,这个值禁止超过20.接收版本1的大于20的长头部的端点,必须丢弃该数据包。服务端应该可以从其他QUIC协议读取更长连接ID,为了适当合成版本协商包。

目标连接ID:目标连接ID字段紧接着ID长度字段,长度为0-20字节。7.2节描述了更多此字段的细节。

SCID长度: 紧接着目标连接ID的字节包含源连接ID字段的字节单位的长度。该长度被编码为一个8位无符号整数。在QUIC版本1中,这个值禁止超过20.接收版本1的大于20的长头部的端点,必须丢弃该数据包。服务端应该可以从其他QUIC协议读取更长连接ID,为了适当合成版本协商包。

源连接ID:源连接ID字段紧跟着目标连接ID字段,长度为0-20字节。7.2节描述了更多此字段的细节。

在这个QUIC版本中,带有长头部的包类型定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------+-----------+----------------+
| Type | Name | Section |
+------+-----------+----------------+
| 0x0 | Initial | Section 17.2.2 |
| | | |
| 0x1 | 0-RTT | Section 17.2.3 |
| | | |
| 0x2 | Handshake | Section 17.2.4 |
| | | |
| 0x3 | Retry | Section 17.2.5 |
+------+-----------+----------------+

表5: 长头部包类型

长包头的头部形式位,连接ID长度字节、目标和源连接ID字段以及版本字段是版本无关的。第一个字节中其他的字段是版本特殊的。 参考[QUIC-INVARIANTS]中描述的不同版本的QUIC是如何解释包格式的。

字段和载荷的解释是特定于版本和包类型的。该版本类型特定的语义在以下各节中描述,该QUIC中一些长包头包包含这些额外的字段。

保留位(R):字节0的两位(掩码0x0c)是在多个包类型保留的。这些位使用头保护进行保护(参见章节 5.4[QUIC-TLS])。在保护之前该值必须设为0。 端点在移除包和包头保护之后,接收到这些位为非0值的数据包时,必须当做PROTOCOL_VIOLATION类型的连接错误。在仅移除包头保护之后丢弃这样的包,可能导致端点收到攻击(参见章节9.3[QUIC-TLS])。

包编号长度(P):在包含包编号字段的包类型中,字节0的最低两个有效位(掩码0x03)包含包编号的长度。 编码为2位无符号整数,该整数比包编号字段长度小1字节。也就是说,包编号字段的长度是此字段的值加1。这些位使用 包头保护进行保护(参见5.4[QUIC-TLS])。

长度:包中剩下的部分(即包编号和载荷字段)以字节为单位的长度,被编码为可变长度的整数(16节)。

包编号:包编号字段的长度为1到4字节。包编号具有独立于包保护的机密性保护,如章节5.4[QUIC-TLS] 中描述的。包编号字段的长度被编码在字节0的包编号长度位中(见上文)。

17.2.1 版本协商包

版本协商包本质上不是特定于版本的。客户端收到后,将根据版本(Version)字段的值为0将其识别为版本协商包。

版本协商包是对包含服务器不支持的版本的客户端数据包的响应,并且只由服务器发送。

版本协商包的布局为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|1| Unused (7) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..2040) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..2040) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Supported Version 1 (32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Supported Version 2 (32)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Supported Version N (32)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图10: 版本协商包

未使用字段中的值由服务端随机选择。客户端必须忽略该字段值。服务端应该该字段(0x40)的最高为设置为1,使得版本协商包有固定位(Fixed Bit)字段。

版本协商包的版本字段必须设置为0x00000000。

服务端必须将它接收的包的源连接ID字段的值包含在目标连接ID字段中。源连接ID的值必须从接收包的目标连接ID复制,该包最初是由客户机随机选择的。响应这两个连接id可以向客户端提供保证,保证服务器收到了数据包,并且版本协商包不是由偏离路径的攻击者生成的。

由于未来的QUIC版本会支持大于版本1限制的连接ID,版本协商包可以携带超过20字节的连接ID。

版本协商包的其余部分是服务端支持的32位版本列表。

版本协商包不能在ACK帧中被客户端显式地确认。接收另一个初始包隐式地确认了一个版本协商包。

版本协商包不包括使用长头部形式的其他包中出现的数据包编号和长度字段。所以,版本协商包消费整个UDP数据报。

服务器禁止发送多个版本协商包来响应单个UDP数据报。

有关版本协商过程的描述,请参见Section 6。

17.2.2 初始数据包

初始包使用类型值为0x0的长头部。 它携带客户端和服务端发送的第一个CRYPTO帧来执行密钥交换,并携带两个方向的ACK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+-+-+-+-+-+-+-+-+
|1|1| 0 |R R|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图11:初始数据包

初始包包含一个长头部以及长度和数据包编号字段。第一个字节包含保留和数据包编号长度位。在SCID和长度字段之间,有两个特定于初始包的附加字段。

令牌长度(Token Length): 可变长度整数指定了令牌字段长度(以字节为单位)。如果不存在令牌,则此值为零。服务器发送的初始数据包必须将令牌长度字段设置为零;接收具有非零令牌长度字段的初始数据包的客户端必须丢弃该数据包或生成类型为PROTOCOL_VIOLATION的连接错误。

令牌(Token): 令牌的值是由之前的重试包或NEW_TOKEN帧中提供的。

载荷(Payload): 数据包的载荷。

为了防止不明版本的中间件篡改, 初始数据包由基于连接和特定版本的密钥(初始密钥)保护,如 [QUIC-TLS]中所述。此保护不提供对路径上攻击者的保密性或完整性的保护, 但提供了对链路外攻击者的某种级别的保护。

客户端和服务端对包含初始加密握手信息的任何包使用初始包类型。这包括所有需要创建包含初始加密消息的新包的情况, 例如接收重试包后发送的包 (Section 17.2.5)。

服务端发送它的第一个初始包来响应客户端的初始包。服务器可以发送多个初始包。密钥交换可能需要多次往返或重新传输数据。

初始包的载荷包括包含加密握手信息、ACK帧或两者都包含的CRYPTO帧(或多个帧)。也允许PADDING和CONNECTION_CLOSE帧。接收包含其他帧的初始包的端点可以将该包作为伪包丢弃,也可以将其视为连接错误。

客户端发送的第一个数据包总是包含一个CRYPTO帧,该帧含有第一个加密握手消息的初始或全部内容。发送的第一个CRYPTO帧总是以偏移量0开始(见Section 7)。

注意,如果服务器发送一个HelloRetryRequest,客户端将发送另一系列的初始包。 这些初始包将继续加密握手,并将包含一个与初始包第一次发送中的CRYPTO帧 大小相匹配的起始偏移量的CRYPTO帧。

17.2.2.1 丢弃初始数据包

客户端在发送第一个握手包时停止发送和处理初始数据包。当服务器接收到它的第一个握手包时停止发送和处理初始数据包。 虽然数据包可能仍然在传输或等待确认,但在此之后不需要再互相传输任何初始包。 初始包保护密钥被丢弃,(参见[QUIC-TLS]第4.9.1节),任何丢失恢复和拥塞控制状态都将随之被丢弃(参见[QUIC-RECOVERY]第6.5节)。

当初始密钥被丢弃时,CRYPTO帧中的任何数据都将被丢弃,并且不再重新传输。

17.2.3 0-RTT

0-RTT数据包使用带有0x1类型值的长头部,后跟长度与数据包编号字段。第一个字节包含保留与数据包编号长度位。它用于在握手完成之前,将“早期”数据从客户端携带到服务端,作为第一次传输的一部分。作为TLS握手的一部分,服务器可以接受或拒绝此早期数据。

有关0-RTT数据及其限制的讨论,请参见[TLS13]的2.3节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+-+-+-+-+-+-+-+-+
|1|1| 1 |R R|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

0-RTT Packet

0-RTT保护数据包的数据包编号与1-RTT保护数据包使用相同的空间。

客户端收到重试数据包后,0-RTT数据包可能已丢失或被服务端丢弃。客户端在发送新的初始数据包后,应该尝试在0-RTT数据包中重新发送数据。

客户端禁止重置其用于0-RTT数据包的数据包编号,因为用于保护0-RTT数据包的密钥不会因响应重试数据包而更改。在这种情况下,发送具有相同数据包编号的数据包 可能会损害所有0-RTT数据包的数据包保护,因为相同的密钥和nonce可用于保护不同的内容。

一旦握手完成,客户端只接收它的0-RTT数据包的确认。因此,服务器可能期望0-RTT数据包从 数据包编号0开始。因此,在确定0-RTT数据包编码的数据包编号长度时,客户端必须假设当前数据包编号之前的所有数据包都在传输中,从数据包号0开始。因此,0-RTT数据包可能需要使用更长的数据包编号编码。

客户端禁止发送0-RTT数据包,一旦开始处理来自服务端的1-RTT数据包。这意味着0-RTT数据包不能包含任何对来自1-RTT数据包的帧的响应。例如,客户端不能再0-RTT数据包中发送ACK帧,因为只能确认1-RTT数据包。1-RTT数据包的确认必须在1-RTT数据包中携带。

服务端应该将违反保存的限制当做适当类型的连接错误(例如,超出流数据限制当做FLOW_CONTROL_ERROR错误)

17.2.4 握手数据包

握手数据包使用类型值为0x2的长头部,后跟长度和数据包编号字段。第一个字节包含保留和数据包编号 长度位。它用于传送来自服务端和客户端的确认和加密握手消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+-+-+-+-+-+-+-+-+
|1|1| 2 |R R|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图12: 握手数据包

一旦客户端从服务端收到握手数据包,它就会使用握手数据包向服务器发送后续加密握手消息和确认。

握手包中的目标连接ID字段包含由该包的接收方选择的连接ID; 源连接ID包括数据包发送方希望 使用的连接ID(参见Section 7.2)。

握手包是它们自己的包编号空间,因此服务端发送的第一个握手包中包含的包编号为0。

该数据包的载荷包含CRYPTO帧,可能包含PADDING或ACK帧。握手包可能包含 CONNECTION_CLOSE帧。 端点必须将包含其他帧的握手数据包视为连接错误。

类似初始数据包(参见Section 17.2.2.1),在握手加密阶段的CRYPTO帧中的数据在丢弃握手保护密钥时将被丢弃。

17.2.5 重试数据包

重试数据包使用类型值为0x3的长数据包头部。它携带由服务端创建的地址校验令牌。它由希望执行无状态重试的服务端使用(请参见Section 8.1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|1|1| 3 | Unused|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ODCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Original Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Retry Token (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图13: 重试数据包

重试数据包(如图13所示)不包含任何受保护的字段。未使用字段的值是由服务端随机选择的。除了长头部之外,它还包 含以下附加字段:

ODCID 长度: ODCID长度包含后跟原始目标连接ID字段的字节长度。该长度被编码为8位无符号整数。在版本1的QUIC中,该值禁止超过20字节。客户端接收到带有大于20字节的值的版本1重试包,则必须丢弃该数据包。

原始目标连接ID: 原始目标连接ID包含此重试所响应的初始数据包中的目标连接ID的值。此字段的长度在ODCID长度中给出。

重试令牌: 服务短可用于验证客户端地址的不透明令牌。
服务端将客户端在初始数据包的源连接ID中包含的连接ID填充目标连接ID。

服务器在源连接ID字段中包含其选择的连接ID。此值不能等于客户端发送的数据包的目标连接ID 字段。客户端必须丢弃包含源连接ID字段与其初始包的目标连接ID字段相等的重试包。客户端必须在其发送的后续数据包的目标连接ID中使用此连接ID。

服务器可以发送重试数据包以响应初始数据包和0-RTT数据包。服务器可以丢弃或缓冲它接收的0-RTT数据包。当服务端接收到初始或0-RTT数据包时,可以发送多个重试数据包。服务端禁止发送多个重试数据包以响应单个UDP数据报。

对于每次连接尝试,客户端必须最多只能接受并处理一个重试数据包。客户端接收并处理来自服务端的初始或重试数据包后,必须丢弃其接收的任何后续重试数据包。

客户端必须丢弃包含原始目标连接ID字段与初始数据包的目标连接ID不匹配的重试数据包。这可防止非路径攻击者注入重试数据包。

客户端用包含提供的重试令牌的初始数据包响应重试数据包,以继续建立连接。

客户端将此初始数据包的目标连接ID字段设置为重试数据包中源连接ID的值。更改目标连接ID也会导致用于保护初始数据包的密钥发生更改。它还将令牌字段设置为重试中提供的令牌。客户端禁止更改源连接ID,因为服务端可以将连接ID作为其令牌验证逻辑的一部分(请参见Section 8.1.3)。

来自客户端的下一个初始数据包使用来自重试数据包的连接ID和令牌值 (请参见Section 7.2)。除此之外,客户端发送的初始数据包受与第一个初始数据包相同的限制。客户端必须使用在该数据包中包含的相同的加密握手消息。服务端可以将包含不同加密握手消息的数据包当做连接错误并丢弃它。

客户端在收到重试数据包后,可以通过对由服务器提供的连接ID发送0-RTT数据包来尝试0-RTT。 客户端禁止改变对接收重试包进行响应的加密握手消息。

客户端在处理重试数据包之后,对任何包编号控件,禁止重置数据包编号;17.2.3节包含更过关于此的信息;

服务端使用original_connection_id传输参数来确认对连接重试数据包的使用(请参见 Section 18.1)。如果服务器发送重试数据包,则必须在传输参数中包含重试数据包的原始目标连接ID字段的值(即客户端第一个初始数据包中的目标连接ID字段)。

如果客户端接收并处理了一个重试包,它必须验证original_connection_id传输参数是否存在且正确;否则,它必须验证传输参数是否缺失。客户端必须将失败的验证视为 TRANSPORT_PARAMETER_ERROR类型的连接错误。

重试数据包不包含数据包编号,并且无法由客户端明确确认。

17.3 短头部数据包

此版本的QUIC协议定义了使用短数据包头的单个包类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|0|1|S|R|R|K|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Protected Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图14: 短头部数据包格式

短包头能在版本协商和1-RTT秘钥协商之后使用。使用短包头的包中包含了以下的这些字段:

头部格式: 短头部的第0字节中的最高有效位(0x80)设置为0

固定位: 字节0的下一位(0x40)设置为1。包含该位为0值的数据包在当前版本中不是有效数据包,必须被丢弃。

自旋位 (S): 第0字节最高第三位(0x20)是延迟自旋位,如17.3.1节所述。

保留位 (R):第0字节的后两个位(掩码为0x18的位)将被保留。这些位由头部保护来保护(请参阅第5.4节[QUIC-TLS])。在添加保护之前的值必须设置为0。端点必须将接收到在移除包和头部保护之后,保留位是非0值的数据包,当做PROTOCOL_VIOLATION类型的连接错误。在仅移除头部保护后就丢弃这样的包可能会使端点受到攻击。(参考9.3节关于[QUIC-TLS])。

秘钥段 (K): 第0字节的下一位(0x04)用于表示秘钥段,这一位可以使包的接受方识别用于保护包的包保护秘钥。 详情请阅[QUIC-TLS]。该位处于头部保护之下 (参考5.4节关于[QUIC-TLS])。

包编号长度 (P): 第0字节的最低有效两位(掩码为0x03)包含了包编号的长度,被编码为两位无符号整型,该整型小于包编号字段的长度(以字节为单位)。 也就是说,包编号字段的长度是该字段的值加1。 这些位处于头部保护之下(请参阅第5.4节[QUIC-TLS])。

目标连接ID: 目标连接ID是包的目标接收方选择的连接ID。 详细信息请参见Section 5.1。

包编号: 包编号字段的长度为1到4个字节。包编号具有独立于包保护的机密性保护, 如[QUIC-TLS]第5.4节所述。包编号字段的长度编码在包编号长度字段中。详细信息请参见Section 17.1。

被保护的负载: 短头部的包始终包含受1-RTT保护的载荷。

短头部包的头部格式位和连接ID字段与协议版本无关。其余字段特定于选定的QUIC版本。关于如何解释来自不同版本QUIC的包的详细信息,请参见[QUIC-INVARIANTS]。

17.3.1 延迟自旋位

延迟自旋位启用来自整个连接期间网络链路上的观测点的被动延时监控。自旋位只在短包头部中出现,因此通过观察握手可以测量连接的初始RTT。因此,自旋位在版本协商和建连完成之后,才是可用的。链路上的测量与延时自旋位的使用在[QUIC-MANAGEABILITY]中有更多讨论

自旋位是QUIC可选的特性。选择支持自旋位的QUIC栈必须如本节指定的方式实现它。

每个端点单方面地决定是否为连接开启或禁用自旋位。协议实现必须允许客户端和服务端的管理员全局地或者基于每个连接地禁用自旋位。甚至当自旋位没有被管理员禁用,协议实现必须为某种可能原因对给定连接禁用自旋位。随机选择过程应该被设计为平均使自旋位对至少八分之一的网络路径禁用。在连接开始时执行的选择过程应该是适用于连接使用的所有路径。

当自旋位被禁用,端点可以设置自旋位为任意值,且必须忽略任何传入值。推荐端点将自旋位设为随机值,为每个包独立地选择或为每个连接ID独立选择。

如果为连接启用了自旋位,当短头部的数据被发送时,端点维护自旋值并且将短头部中的自旋值设为当前存储的值。在连接开始时,端点中的自旋值被初始化为0。每个端点也会记住在连接上的对端可见的最高包编号。

当服务端接收到增加最大包编码的短头部数据包(来自客户端的服务端可见的)时,它将设置自旋值为接收到的数据包的自旋位相等的值。

当客户端接收到增加最大包编码的短头部数据包(来自服务端的客户端可见的)时,它将设置自旋值为接收到的数据包自旋位的相反值。

端点重置它的自旋值为0,当发送第一个带有新连接ID的给定连接的包。这减少了瞬态自旋位状态被用于跨连接迁移的连接流量或者ID变更的风险。

通过该机制,服务端反映接收到的自旋值,当客户端在1个RTT之后“自旋”。链路上的观察方可以测量两个自旋位切换事件之间的时间来估计端到端连接RTT。

18 传输参数编码

quic_transport_parameters扩展的extension_data字段在[QUIC-TLS]定义,包含了QUIC传输参数。它们作为传输参数的固定长度的序列被编码,如图15所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Length (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transport Parameter 1 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transport Parameter 2 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transport Parameter N (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图15: 传输参数序列

序列长度字段包含传输参数的序列的长度,以字节的形式。每个传输参数被编码为一个(标识,长度,值)元组,如图16所示:

1
2
3
4
5
6
7
8
9
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transport Parameter ID (16) | Transport Param Length (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transport Parameter Value (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图16: 传输参数编码

传输参数长度字段包含了传输参数值字段的长度。

QUIC将传输参数编码为字节序列,然后被包含在加密握手中。

18.1 保留的传输参数

带有 “31 * N + 27” 形式标识的传输参数被保留,用于执行位置传输参数被忽略的要求。这些传输参数没有语义,可以携带任意值

18.2 传输参数定义

本节描述本文档定义的传输参数。

此处列出的许多传输参数都具有整数值。除非另有说明,标识为整数的传输参数使用 变长度整数编码(请参阅Section 16),如果传输参数不存在,则默认值为0。

传输参数有以下定义:

original_connection_id (0x0000):
客户端发送的第一个初始数据包中的目标连接ID字段的值。此传输参数仅由服务器发送。这与重试包的原始目标连接ID字段中发送的值是相同的。如果服务端发送了重试数据包,则必须包含original_connection_id传输参数。

idle_timeout (0x0001): 空闲超时是以毫秒为单位的值,编码为整数, 参考(Section 10.2)。 如果此参数不存在或为零,则禁用空闲超时。

stateless_reset_token (0x0002): 无状态重置令牌用于验证无状态重置, 参考Section 10.4。 该参数是16个字节的序列。 此传输参数禁止由客户端发送,但可由服务端发送。握手期间没有发送传输参数的服务端不能对协商的连接ID使用无状态重置(10.4节)

max_packet_size (0x0003): 最大数据包大小参数是一个整数值,用于限制端点愿意接收的数据包的大小。这表示大于此限制的数据包将被丢弃。此参数的默认值是UDP允许的最大有效负载65527,低于1200的值无效。此限制仅适用于受保护的数据包(参考Section 12.1)。

initial_max_data (0x0004): 初始最大数据参数是一个整数值,包含可以在连接上发送的最大数据量的初始值。这相当于在完成握手后立即为连接发送MAX_DATA (Section 19.9)。

initial_max_stream_data_bidi_local (0x0005): 此参数是一个整数值,指定本地启动的双向流的初始流量控制限制。此限制适用于由发送传输参数的端点打开的新创建的双向流。在客户端传输参数中,这适用于最低有效两位设置为0x0的流; 在服务器传输参数中, 这适用于最低有效两位设置为0x1的流。

initial_max_stream_data_bidi_remote (0x0006): 此参数是一个整数值,指定对端启动的双向流的初始流控制限制。此限制适用于由接收传输参数的端点打开的新创建的双向流。在客户端传输参数中,这适用于最低有效两位设置为0x1的流; 在服务器传输参数中, 这适用于最低有效两位设置为0x0的流。

initial_max_stream_data_uni (0x0007): 此参数是一个整数值,指定单向流的初始流控制限制。此限制适用于由接收传输参数的端点打开的新创建的单向流。 在客户端传输参数中,这适用于最低有效两位设置为0x3的流; 在服务器传输参数中,这适用于最低有效两位设置为0x2的流。

initial_max_streams_bidi (0x0008): 初始最大双向流参数是整数值,其包含对端可以发起的初始最大双向流数量。如果此参数不存在或为零,则在发送MAX_STREAMS帧之前,对等方无法打开双向流。设置此参数等效于发送具有相同值的相应类型的MAX_STREAMS(Section 19.11)。

initial_max_streams_uni (0x0009): 初始最大单向流参数是整数值,其包含对端可以发起的初始最大单向流数。如果此参数不存在或为零,则对等方无法打开单向流,直到发送MAX_STREAMS帧为止。设置此参数等效于发送具有相同值的相应类型的MAX_STREAMS(Section 19.11)。

ack_delay_exponent (0x000a): ACK延迟指数是指示用于解码ACK帧中的ACK延迟字段的指数的整数值(Section 19.3)。如果此值不存在,则假定默认值为3(表示乘数为8)。默认值也用于在初始和握手数据包中发送的ACK帧。大于20的值无效。

max_ack_delay (0x000b): 最大ACK延迟是一个整数值,表示端点发送确认时延迟的最长时间(以毫秒为单位)。该值应该包括接收者在警告发送时的预期延迟。例如,如果接收者定时设置为5ms并且警告通常会延迟最多1ms,那么它应该发送6ms的max_ack_delay。 如果此值不存在,则假定默认值为25毫秒。 2 ^ 14或更大的值无效。

disable_active_migration (0x000c): 如果端点不支持主动连接迁移(Section 9),则会包括该禁用主动迁移传输参数。设置了此传输参数的端点的对端禁止从除用于执行握手的本地地址之外的本地地址发送任何数据包,包括探测数据包(Section 9.1)。 此参数是零长度值。

preferred_address (0x000d): 服务器的首选地址用于在握手结束时实现服务器地址的更改,参考Section 9.6中的描述。此传输参数的格式如图17所示。此传输参数仅由服务端发送。 服务端可以选择对另一个地址族发送全0地址和端口 (0.0.0.0:0或::.0)来实现仅发送一个地址族的首选地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv4 Address (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv4 Port (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ IPv6 Address (128) +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv6 Port (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CID Length (8)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Connection ID (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ Stateless Reset Token (128) +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图17: 首选地址格式

active_connection_id_limit(0x000e): 来自对端的端点可以存储的连接ID的最大值。该值只包括在NEW_CONNECTION_ID帧中发送的连接ID。如果参数不存在,默认认为0

如果存在,则设置初始流控制限制的传输参数 (initial_max_stream_data_bidi_local,initial_max_stream_data_bidi_remote 和initial_max_stream_data_uni) 等同于在打开之后立即在相应类型的每个流上发送 MAX_STREAM_DATA帧(Section 19.10)。 如果传输参数不存在,则该类型的流以流控制限制为0开始。

客户端禁止包含只有服务器使用的传输参数(original_connection_id, stateless_reset_token, 或
preferred_address)。 服务端必须将任何这些传输参数的接收视为TRANSPORT_PARAMETER_ERROR类型的连接错误。

19 帧类型与格式

如 Section 12.4所述,数据包包含一个或多个帧。本节描述核心QUIC帧类型的格式和语义。

19.1 PADDING帧

PADDING帧(类型=0x00)没有语义值。PADDING帧可以用来增加包的大小。PADDING帧可用于将初始客户端包增加到所需的最小大小,或为受保护的包提供对抗流量分析的保护。

PADDING帧没有内容。也就是说,PADDING帧由一个字节组成,该字节将帧标识为PADDING帧。

19.2 PING帧

端点可以使用PING帧(类型=0x01)来验证它们的对端是否仍然是活动状态,或者检查对端的可达性。PING帧不包含其他字段。

PING帧的接收者只需要确认包含该帧。

当应用程序或应用程序协议希望防止连接超时时,可以使用PING帧保持连接处于活动状态。应用程序协议应该提供关于建议生成PING的条件的指导。此指南应当指示预期发送PING的是客户端还是服务器。有两个端点都发送PING帧而不进行协调,则会产生过多的数据包,并且性能很差。

如果没有发送或接收数据包的时间超过idle_timeout传输参数中指定的时间 (参见Section 10),连接将超时。然而,在中间件中的状态可能会比这更早超时。虽然[RFC4787]中的REQ-5建议2分钟超时间隔,但经验表明, 每15到30秒发送包是必要的,以防止UDP流的大多数中间件丢失状态。

19.3 ACK帧

接收方发送ACK帧(类型为0x02和0x03)来通知发送方他们已经接收和处理了数据包。ACK帧包含一个或多个ACK范围。ACK范围标识已确认的数据包。如果帧类型是0x03, ACK帧还包含到目前为止在连接上接收到的带有相关ECN标记的QUIC包的和。QUIC实现必须正确地处理这两种类型,如果它们为发送的包启用了ECN, 则应该使用ECN部分中的信息来管理它们的拥塞状态。

QUIC确认是不可撤销的。一旦确认,包保持是确认的状态,即使它没有出现在未来的ACK帧。这与TCP SACKs不同([RFC2018])。

预计发送方将在不同的包编号空间中重用相同的包编号。ACK帧只确认发送方在接收ACK的包的相同包号空间中传输的包号。

无法确认版本协商和重试包,因为它们不包含包编号。这些包不是依赖于ACK帧,而是由客户机发送的下一个初始包隐式地确认。

ACK帧如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Largest Acknowledged (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ACK Delay (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ACK Range Count (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| First ACK Range (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ACK Ranges (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [ECN Counts] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图18: ACK帧格式

ACK帧包含以下字段:

最大已确认(Largest Acknowledged): 一个可变长度的整数,表示对端正在识别的最大包编号; 这通常是对端在生成ACK帧之前接收到的最大数据包编号。与QUIC长头部或短头部中的包号不同,ACK帧中的值不会被截断。

ACK延迟(ACK Delay): 一个可变长度的整数,表示发送此ACK的时刻与对端接收到的最大已确认包的时刻之间的时间差 (如最大已确认字段中所示)(以微秒为单位)。 ACK延迟字段的值通过将编码值乘以2的幂次乘以ACK帧的发送方设置的ack_delay_exponent传输参数的值来缩放。 ack_delay_exponent默认值为3,或者乘数为8(参见Section 18.2)。 这种方式的缩放允许以较低的分辨率为代价,以更短的编码实现更大范围的值。

ACK范围计数(ACK Range Count): 一个可变长度整数,指定帧中间隔和ACK范围字段的数量。

首个ACK范围(First ACK Range): 一个可变长整数,指示正在被确认的最大已确认包之前的连续数据包的数量。 第一个ACK范围被编码为从最大的已确认的A开始的一个ACK范围(参见Section 19.3.1)。 也就是说,范围内最小的确认包是由最大的确认包减去第一个ACK范围值来确定的。

ACK范围(ACK Ranges): 包含其他范围的数据包,这些数据包交替不被确认(Gap)和确认(ACK范围),参见Section 19.3.1。

ECN计数(ECN Counts): 三个ECN计数, 见 Section 19.3.2.

19.3.1 ACK范围

ACK范围字段由按数据包编号降序排列的间隔值和ACK范围值交替组成。间隙和ACK范围值的数量由ACK范围计数(ACK Range Count)字段确定; ACK范围计数(Range Count)字段中的每个值都对应一个值。

ACK范围结构如下组织:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Gap (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [ACK Range (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Gap (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [ACK Range (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Gap (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [ACK Range (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图19: ACK范围

ACK范围的字段格式为:

间隙(重复的): 一个可变长整数,表示比在之前的ACK范围中小于最小包编号的连续未确认包的编号。

ACK范围(重复的): 一个可变长整数,表示小于当前最大包编号连续确认包的编号,由之前的间隙确定。

间隙和ACK范围值使用相对整数编码以提高效率。 尽管每个编码的值都是正的,由于该值是被减去的,所以每个ACK范围描述了编号逐渐降低的数据包。

每个ACK范围通过指示在该范围内最大数据包编号之前的已确认数据包的编号来确认连续的数据包范围。值为零表示仅确认最大的数据包号。 ACK范围值越大表示的范围越大,该范围内最小的数据包编号对应的值越小。 因此,给定范围内的最大数据包编号,最小值由以下公式确定:

1
smallest = largest - ack_range

ACK范围确认了最小包编号和最大包编号之间的所有数据包。

ACK范围的最大值是通过累计减去前面所有前面的ACK范围和间隙的大小来确定的。

每个间隙表示了未确认的包的范围。间隙中的包的编号比间隙字段的编码值大一。

间隙字段的值使用以下公式确定后续ACK范围的最大包编号值:

1
largest = previous_smallest - gap - 2

如果任何计算出的包编号是负值,端点必须生成一个FRAME_ENCODING_ERROR类型的连接异常表示ACK帧中的错误。

19.3.2 ECN计数

ACK帧使用最低有效位(即类型0x03)来指示ECN反馈,并且在数据包的IP报头中报告收到的QUIC包的关联的ECN码点ECN(0), ECT(1), 或者CE。ECN计数仅仅在ACK帧类型是0x03的时候存在。

只有ACK帧类型是0x03的时候,ECN计数才会被解析。 有3个ECN计数,如下表示:

1
2
3
4
5
6
7
8
9
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ECT(0) Count (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ECT(1) Count (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ECN-CE Count (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

三个ECN计数分别是:

ECT(0) 计数:一个可变长的整数,表示在ACK帧的包编号空间中接收到的带有ECT(0)码点的包的总数。
ECT(1) 计数:一个可变长的整数,表示在ACK帧的包编号空间中接收到的带有ECT(1)码点的包的总数。
CE 计数:一个可变长的整数,表示在ACK帧的包编号空间中接收到的带有CE码点的包的总数。

ECN 计数对每个包编码空间进行独立的维护。

19.4 RESET_STREAM帧

端点使用RESET_STREAM帧(类型=0x04)来突然终止一个流的发送部分。

在发送RESET_STREAM帧之后,端点停止在标识的流上的数据包的传输与重传。收到RESET_STREAM的端点可以丢弃它在这个流上已经收到的任何数据。

在一个仅发送的流上接收到RESET_STREAM帧的端点必须以STREAM_STATE_ERROR异常来中断连接。

RESET_STREAM帧如下表示:

1
2
3
4
5
6
7
8
9
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Application Error Code (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Final Size (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RESET_STREAM帧包含下列字段:

流ID(Stream ID): 一个可变长整数,编码为将要终止的流的ID。

应用协议错误码(Application Error Code): 一个十六位的应用协议错误码(详见Section 20.1),表示流要被关闭的原因。

最终大小(Final Size): 一个可变长整数, 表示RESET_STREAM发送方定义的流的最终大小,以位为单位。

19.5 STOP_SENDING帧

端点使用STOP_SENDING帧(类型=0x05)来通知当收到应用请求,传入的数据将被丢弃。STOP_SENDING要求对端停止在流当中的传输。

STOP_SENDING帧可以在状态为接收和大小已知状态的流当中发送(详见Section 3.1)。在一个本地初始化之后但还未创建的流中收到STOP_SENDING帧必须当做STREAM_STATE_ERROR类型的连接错误。端点在一个仅接收的流中收到STOP_SENDING帧必须带着STREAM_STATE_ERROR错误终止连接。

STOP_SENDING帧如下说述:

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Application Error Code (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STOP_SENDING帧包含如下字段

流ID:可变长度的整数携带表示将被忽略的流的流ID。
应用错误码:可变长度整数,包含由应用程序指定的,发送方忽略此流的原因 (详见Section 20.1)。

19.6 CRYPTO帧

CRYPTO帧(类型=0x06)是用来传输加密握手信息的。它可以被包含在除了0-RTT包的所有类型的包当中被发送。CRYPTO帧为加密协议提供了有顺序的字节流。CRYPTO帧在功能上和STREAM帧相同,但是他们暴露不同的流标示;他们不受流量控制的限制;并且它们不携带 可选偏移、可选长度和流末端的标记。

CRYPTO帧如下所述:

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Offset (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Crypto Data (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图20: CRYPTO帧格式

CRYPTO帧包含如下字段:

偏移: 可变长度的整数,指定流中在此CRYPTO帧中的数据偏移。

长度: 可变长度整数,指定此CRYPTO帧中加密数据字段的长度。

加密数据: 加密信息数据。

在每一个加密级别中有各自的加密握手数据流,每个流都从偏移量0开始。这意味着每一个加密级别都被视为单独的CRYPTO数据流(stream)。

流上发送的最大便宜 - 该偏移与数据长度之和 - 不能超过2^62-1。帧的接收超过该限制时必须当做FRAME_ENCODING_ERROR类型的连接错误。

CRYPTO帧不像STREAM帧那样含有表明数据属于哪个流的流ID,CRYPTO帧在每一个加密级别上为单个流携带数据。流没有明确的结尾,所以CRYPTO帧没有FIN位。

19.7 NEW_TOKEN帧

服务器发送NEW_TOKEN帧(类型=0x07)给客户端提供一个令牌,用于在未来连接的初始包的头部的进行发送。

NEW_TOKEN帧结构如下:

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Clients MUST NOT send NEW_TOKEN frames. Servers MUST treat receipt
of a NEW_TOKEN frame as a connection error of type
PROTOCOL_VIOLATION.

NEW_TOKEN帧包含如下字段:

令牌长度:可变长度整数,指定令牌的字节长度。

令牌:一个非透明的数据块,客户端可能会用在未来的初始包中。令牌必须是非空的。端点必须将接收到带有空令牌字段的NEW_TOKEN帧当做FRAME_ENCODING_ERROR类型的连接错误。

端点可能收到多个包含相同令牌值的NEW_TOKEN帧。端点有责任丢弃重复的值,该可能被用于链路连接尝试;见8.1.2节。

客户端禁止发送NEW_TOKEN帧。服务端必须将接收到NEW_TOKEN帧当做PROTOCOL_VIOLATION类型的连接错误。

19.8 STREAM帧

STREAM帧隐式的创建一个流并携带流数据。STREAM帧采用0b00001XXX的形式(或从0x08到0x0f的一组值)。帧类型当中较低的3位决定了帧当中存在的字段。

  • OFF位(0x04)在帧类型中被设置表明帧中有偏移字段。设置为1时,偏移字段存在。设置为0时,偏移字段不存在并且流数据从偏移0开始(也就是说,此帧包含流的最初的字节数据,或者不包含数据的流末端)。
  • LEN位(0x02)在帧类型中被设置表明帧中有长度字段。设置为0时,长度字段不存在并且流数据字段延续到包的末尾。设置为1时,长度字段存在。
  • FIN位(0x01)在帧类型中被设置表明此帧包含此流最后的数据。此位被设置表明此帧标记流的结束。

当一个端点从一个仅发送的流当中收到STREAM帧时,它必须终止连接并附带STEAM_STATE_ERROR错误。

STEAM帧结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Offset (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Length (i)] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream Data (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

图21: STREAM帧格式

STEREAM帧包含如下字段:

流ID:可变长度的整数,指示此流的流ID(参见 Section 2.1)。

偏移:可变长度整数,指示STREAM帧当中对数据在流中的字节偏移。当OFF位设置为1时此字段存在。当偏移字段不存在时,偏移为0.

长度:可变长度整数,指示STREAM帧中流数据字段的长度。当LEN位设置为1时此字段存在。当LEN位设置为0时,流数据字段消费了此数据包中剩下的字节。

流数据:要传递的指定流中的字节。

当流数据字段的长度为0时,STREAM帧当中的偏移标明的是下一个将要发送的字节的偏移。

流中的第一个字节的偏移为0。在流上传递的最大偏移量 - 偏移量和数据长度之和 - 不能超过2^62,否则因为不可能为该数据提供流量控制。接收到超过该限制的帧必须当做FRAME_ENCODING_ERROR或FLOW_CONTROL_ERROR类型的连接错误。

19.9 MAX_DATA帧

在流量控制中使用MAX_DATA帧(类型0x10)来通知对端在连接上可以发送的最大数据量。

MAX_DATA帧如下:

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Maximum Data (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

MAX_DATA帧包含如下字段:

最大数据量(Maximum Data): 一个可变长度整数,以字节为单位,指示在整个连接上可以发送的最大数据量。

所有在STREAM帧中发送的数据计数受该限制。所有流(包括处于终止状态的流) 上接收到的最大偏移量的总和不得超过接收方通告的值。如果端点接收到的数据超过其发送的最大数据值,则必须使用FLOW_CONTROL_ERROR错误终止连接,除非这是初始限制中更改的结果(请参阅Section 7.3.1)。

19.10 MAX_STREAM_DATA帧

在流量控制中使用MAX_STREAM_DATA帧(类型=0x11)来通知对端在流上可以发送的最大数据量。

处于接收状态的流可以发送MAX_STREAM_DATA帧(参见Section 3.1)。 为尚未创建的本地发起的流接收MAX_STREAM_DATA帧必须被视为STREAM_STATE_ERROR类型的连接错误。对于仅接收的流接收到MAX_STREAM_DATA帧的端点必须使用错误STREAM_STATE_ERROR终止连接。

MAX_STREAM_DATA帧如下:

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Maximum Stream Data (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

MAX_STREAM_DATA帧包含以下字段:

流ID (Stream ID): 受影响的流的流ID编码为可变长度整数。

最大的流数据 (Maximum Stream Data): 可变长度整数,以字节为单位,指示可在标识的流上发送的最大数据量。

当将数据计数到这个限制时,端点占有流上发送或接收的数据的最大接收偏移量。丢失或重新排序意味着流上接收到的最大偏移量可能大于该流上接收到的数据的总大小。 接收STREAM帧不会增加最大的接收偏移量。

在流上发送的数据不能超过接收方通告的最大的最大流数据值。如果端点接收到的数据多于它为受影响的流发送的最大最大流数据值, 则必须使用FLOW_CONTROL_ERROR错误终止连接, 除非这是初始限制更改的结果(请参见Section 7.3.1)。

19.11 MAX_STREAMS帧

MAX_STREAMS帧(类型=0x12和0x13)通知对端允许打开的给定类型的流的累计数量。类型为0x12的MAX_STREAMS帧应用于双向流,类型为0x13的MAX_STREAMS帧应用于单向流。

MAX_STREAMS帧如下:

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Maximum Streams (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

MAX_STREAMS帧包含以下字段:

最大流(Maximum Streams): 在连接的整个生存期内可以打开的相应类型的流的累计数量的计数。

流ID不能超过2^62-1,因此不可能将大于该值的流ID进行编码。接收到允许打开超过该限制的流的帧必须当做FRAME_ENCODING_ERROR错误。

丢失或重新排序可能导致接收MAX_STREAMS帧,该帧声明流限制低于端点之前接收到的流限制。 不增加流限制的MAX_STREAMS帧必须被忽略。

端点禁止打开超过其对端设置的当前流限制的流。 例如,接收单向流限制为3的服务器可以打开流3、7和11,但不能打开流15。 如果对端打开多于允许的流数,端点必须使用STREAM_LIMIT_ERROR错误终止连接。

注意,这些帧(以及相应的传输参数)并不描述可以并发打开的流的数量。 该限制包括已关闭的流和已打开的流。

19.12 DATA_BLOCKED帧

当发送方希望发送数据但由于连接级流量控制而无法发送数据时(参见Section 4), 它应该发送DATA_BLOCKED帧(类型=0x14)。 DATA_BLOCKED帧可以用作流量控制算法调优的输入(参见Section 4.2)。

DATA_BLOCKED帧如下:

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data Limit (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

DATA_BLOCKED帧包含以下字段:

数据限制(Data Limit): 一个可变长度整数,指示发生阻塞时的连接级别限制。

DATA_BLOCKED frames contain the following fields:

Data Limit: A variable-length integer indicating the connection-
level limit at which blocking occurred.

19.13 STREAM_DATA_BLOCKED帧

发送方当希望发送数据但是因为流级别的流量控制不能发送的时候应该发送一个STREAM_DATA_BLOCKED帧(类型是0x15)。 这个帧是和DATA_BLOCKED(Section 19.12)类似的。

对于一个仅发送的流,接受到STREAM_DATA_BLOCKED帧的端点必须以STREAM_STATE_ERROR的异常终止连接。

STREAM_DATA_BLOCKED帧结构如下:

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream Data Limit (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STREAM_DATA_BLOCKED帧包含以下字段:

流ID(Stream ID): 一个可变长度整数,标识被流控阻塞的流。
流数据限制(Stream Data Limit): 一个可变长度的整数,标识流阻塞出现的偏移量。

19.14 STREAMS_BLOCKED帧

当希望打开一个流,但是因为对端设置的最大流上限(详见Section 19.11)无法打开的时候, 发送方应该发送一个STREAMS_BLOCKED帧(类型0x16或者0x17)。 类型0x16的STREAMS_BLOCKED帧用于表示到达了双向流上限,类型0x17的STREAMS_BLOCKED帧用于表示到达了单向流上限。

STREAMS_BLOCKED帧不打开流,但是告知对端需要一个新的流并且流限制阻止了流的创建。

STREAMS_BLOCKED帧的结构如下:

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream Limit (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STREAMS_BLOCKED帧包含以下字段:

流上限(Stream Limit): 一个可变长度整数,表示当前被发送的帧的流上限。流ID不能超过2^62-1,因此不可能对比该值更大的流ID进行编码。接收到编码更大的流ID的帧时必须当做STREAM_LIMIT_ERROR或FRAME_ENCODING_ERROR错误。

19.15 NEW_CONNECTION_ID帧

端点发送NEW_CONNECTION_ID帧(类型0x18)来给对端提供可用于在连接迁移的时候中断可连接性(详见Section 9.5)的代替连接ID。

NEW_CONNECTION_ID 帧结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Retire Prior To (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (8) | |
+-+-+-+-+-+-+-+-+ Connection ID (8..160) +
| ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ Stateless Reset Token (128) +
| |
+ +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

NEW_CONNECTION_ID 帧包含以下字段:

序列号(Sequence Number): 由发送方赋值给连接ID的序列号。详见Section 5.1.1。

先前废弃(Retire Prior To): 一个可变长度证书表示应该被废弃的连接ID。见5.1.2节

长度(Length): 一个8位无符号的整数,包含连接ID的长度。小于1和大于20的值都是无效的而且必须当做PROTOCOL_VIOLATION类型的连接错误。

连接ID(Connection ID): 指定长度的连接ID。

无状态重置凭证(Stateless Reset Token): 一个128位的值,用于关联的连接ID被使用时的无状态连接重置(详见Section 10.4)。

如果现在需要对端使用0长度的目标连接ID发送包,端点应禁止发送这个帧。从0长度或到0长度的对连接ID的长度的改变将会使得很难辨别何时连接ID的值发生了改变。 以0长度目的连接ID发送包的端点必须将接收到NEW_CONNECTION_ID帧当做PROTOCOL_VIOLATION类型的连接错误。

传输错误,超时和重传可能导致相同的NEW_CONNECTION_ID帧被收到多次。多次相同NEW_CONNECTION_ID帧的接收禁止被处理为连接异常。接收方可以使用提供在NEW_CONNECTION_ID帧中的序列号来辨别新旧链接ID。

如果端点接收到了一个与之前发出的连接ID拥有不同的无状态重置凭证或不同序列号,或者如果序列号用于不同连接ID的NEW_CONNECTION_ID帧, 端点可以当做PROTOCOL_VIOLATION类型的连接错误。

先前废弃字段时向对端请求废弃所有序列号小于指定值的连接ID。对端应该废弃相应的连接ID并且及时地发送相应的RETIRE_CONNECTION_ID帧

先前废弃字段必须小于或等于序列号字段。接收到比序列号更大的值必须当做FRAME_ENCODING_ERROR类型的连接错误。

一旦发送方指示先前废弃的值,在后续NEW_CONNECTION_ID帧中的更小的值将无效。发送方必须忽略任何没有增加接收到的最大先前废弃值的先前废弃字段。

19.16 RETIRE_CONNECTION_ID帧

端点发送RETIRE_CONNECTION_ID帧(类型=0x19)来表示它将不再会使用对端发出的连接ID。 这可能包括握手期间提供的连接ID。 发送RETIRE_CONNECTION_ID帧也用于请求对端发送额外的连接ID供未来使用(详见 Section 5.1)。 新连接ID可以用NEW_CONNECTION_ID帧来发送到对端。(Section 19.15)

销毁连接ID也无效化了关联这个连接ID的无状态重置凭证。

RETIRE_CONNECTION_ID帧结构如下:

1
2
3
4
5
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RETIRE_CONNECTION_ID 帧包含以下字段:

序列号(Sequence Number): 将要销毁的连接ID的序列号。详见Section 5.1.2.

端点接收到包含大于任何之前发送到对端的序列号的RETIRE_CONNECTION_ID帧可以当做PROTOCOL_VIOLATION类型的连接错误。

在RETIRE_CONNECTION_ID帧中指定的序列号禁止引用包含此帧的包的目标连接ID字段。对端可以当做PROTOCOL_VIOLATION类型的连接错误。

如果对端已经提供过了0长度的连接ID,端点不能发送此帧。 提供0长度的连接ID的端点若收到RETIRE_CONNECTION_ID帧,必须当做PROTOCOL_VIOLATION类型的连接错误。

19.17 PATH_CHALLENGE帧

端点可以使用PATH_CHALLENGE帧(类型=0x1a)来检查到对端的可达性以及连接迁移期间的路径验证。

PATH_CHALLENGE帧如下所示:

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ Data (64) +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

PATH_CHALLENGE帧包含如下字段:

数据: 这个8字节的字段包含任意数据。

包含难以猜测的8个字节的PATH_CHALLENGE帧足以确保接收数据包比正确猜测该值更容易。

此帧的收件人必须生成包含相同数据的PATH_RESSION帧 (Section 19.18)。

19.18 PATH_RESPONSE帧

PATH_RESPONSE帧(type=0x1B)作为PATH_CHALLENGE帧的响应发送。其格式与PATH_CHALLENGE帧(Section 19.17) 相同。

如果PATH_RESPONSE帧的内容与先前由端点发送的PATH_CHALLENGE帧的内容不匹配,则端点可以生成PROTOCOL_VIOLATION类型的连接错误。

19.19 CONNECTION_CLOSE帧

端点发送CONNECTION_CLOSE帧(type=0x1c或0x1d)通知其对端连接正在关闭。帧类型为0x1c的CONNECTION_CLOSE帧仅用于在QUIC层发出错误信号,或表示没有错误(带有NO_ERROR码)。 类型为0x1d的CONNECTION_CLOSE帧用于向使用QUIC的应用发出错误信号。

如果有未显式关闭的开放流,则当连接关闭时,它们将隐式关闭。

CONNECTION_CLOSE帧如下所示:

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Error Code (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [ Frame Type (i) ] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reason Phrase Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reason Phrase (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

CONNECTION_CLOSE帧包含以下字段:

错误码: 一个可变长度整数错误码,表示关闭此连接的原因。类型为0x1c的CONNECTION_CLOSE帧使用Section 20中定义的空间中的代码。类型为0x1d的CONNECTION_CLOSE帧使 用来自应用协议错误码空间的代码,请参见Section 20.1

帧类型: 一个可变长度整数,用于编码触发错误的帧的类型。当帧类型未知时,使用值0(相当于提到PADDING帧)。CONNECTION_CLOSE的应用特定变体(类型0x1d)不包括此字段。

原因短语长度: 以字节为单位指定原因短语长度的可变长度整数。由于连接CONNECTIOIN_CLOSE帧不能在数据包之间拆分,因此对数据包大小的任何限制也会限制原因短语的可用空间。

原因短语: 对连接关闭原因的人类可读的解释。如果发送方选择不提供错误代码以外的详细信息, 则长度可以为零。这应该是UTF-8编码的字符串 [RFC3629]。

19.20 扩展帧

QUIC帧不使用自描述编码。因此,端点需要了解所有帧的语法,才能成功处理数据包。这允许对帧进行有效的编码,但这意味着端点不能发送对端未知类型的帧。

如果QUIC的扩展希望使用新类型的帧,则必须首先确保对端能够理解该帧。端点可以使用传输参数来表示它愿意接收带有一个传输参数的一个或多个扩展帧类型。

扩展帧必须是拥塞控制的,并且必须触发ACK帧发送。替换或补充ACK帧的扩展帧除外。除非在扩展中指定,否则扩展帧不包括在流量控制中。

IANA注册表用于管理帧类型的分配,请参见Section 22.2。

20 传输错误码

QUIC错误码是62位无符号整形数。
本节列出了可能在CONNECTION_CLOSE帧中使用的QUIC错误码的定义。这些错误可能在整个连接过程中发生。

NO_ERROR (0x0): 端点通过使用携带这个错误码的CONNECTION_CLOSE帧来表明连接在没有发生任何错误的情况下突然关闭。

INTERNAL_ERROR (0x1): 端点遇到内部错误无法继续维持连接。

SERVER_BUSY (0x2): 服务器当前繁忙且不会接受任何新的连接。

FLOW_CONTROL_ERROR (0x3): 端点收到的数据超过其被通知的数据限制所允许的数量(请参见4节)。

STREAM_LIMIT_ERROR (0x4): 端点接收的帧,其超出了对单个流标识下的其被通知的对应流类型的限额。

STREAM_STATE_ERROR (0x5): 端点在允许接收该帧的状态之外的状态接收到该帧(请参见3节)。

FINAL_SIZE_ERROR (0x6):
端点收到的STREAM帧包含的数据超过了先前确定的最终大小。或者端点接收的STREAM帧或RESET_STREAM帧的 最终大小小于已接收的流数据的大小。或者,终端接收的STREAM帧或RESET_STREAM帧中包含与已建立的最终大小值不同。

FRAME_ENCODING_ERROR (0x7): 端点收到格式错误的帧。 例如,未知类型的帧, 或者确认范围大于包可携带的剩余部分的ACK帧。

TRANSPORT_PARAMETER_ERROR (0x8): 端点接收的传输参数格式错误、包含无效值、 是必填的但不缺失了、 出现了不允许的值,或在其他情况下出错。

PROTOCOL_VIOLATION (0xA): 端点检测到的错误在协议范围内 没有更具体的错误代码。

CRYPTO_BUFFER_EXCEEDED (0xD): 端点在CRYPTO帧中接收到比它可以缓冲的更多的数据。

CRYPTO_ERROR (0x1XX): 加密握手失败。保留256个值的范围用于传送特定于使用的加密握手的错误代码。 在[QUIC-TLS]的第4.8节中介绍了将TLS用于加密握手时会出现的错误码。

有关注册新错误码的详细信息,请参阅22.3节。

在定义的这些错误码中,一些原则需要遵守。错误条件会要求在给定唯一码的接收部分上有特定的行为。表示常见条件的错误是给定的限定码。不再这些条件中的,错误码则用于标识通用的栈功能,像流量控制或者传输参数处理。最后,为条件提供的通用错误,协议实现不能或者不愿意使用更特定的码

20.1 应用协议错误码

应用协议的错误码是62位无符号整数,但应用错误码的管理留给应用协议负责。应用协议错误码用于RESET_STREAM帧 (19.4节)和 类型为0x1d(19.19节)的 CONNECTION_CLOSE帧。

21 安全注意事项(todo)

21.1 服务的握手拒绝

As an encrypted and authenticated transport QUIC provides a range of
protections against denial of service. Once the cryptographic
handshake is complete, QUIC endpoints discard most packets that are
not authenticated, greatly limiting the ability of an attacker to
interfere with existing connections.

Once a connection is established QUIC endpoints might accept some
unauthenticated ICMP packets (see Section 14.2), but the use of these
packets is extremely limited. The only other type of packet that an
endpoint might accept is a stateless reset (Section 10.4) which
relies on the token being kept secret until it is used.

During the creation of a connection, QUIC only provides protection
against attack from off the network path. All QUIC packets contain
proof that the recipient saw a preceding packet from its peer.

The first mechanism used is the source and destination connection
IDs, which are required to match those set by a peer. Except for an
Initial and stateless reset packets, an endpoint only accepts packets
that include a destination connection that matches a connection ID

the endpoint previously chose. This is the only protection offered
for Version Negotiation packets.

The destination connection ID in an Initial packet is selected by a
client to be unpredictable, which serves an additional purpose. The
packets that carry the cryptographic handshake are protected with a
key that is derived from this connection ID and salt specific to the
QUIC version. This allows endpoints to use the same process for
authenticating packets that they receive as they use after the
cryptographic handshake completes. Packets that cannot be
authenticated are discarded. Protecting packets in this fashion
provides a strong assurance that the sender of the packet saw the
Initial packet and understood it.

These protections are not intended to be effective against an
attacker that is able to receive QUIC packets prior to the connection
being established. Such an attacker can potentially send packets
that will be accepted by QUIC endpoints. This version of QUIC
attempts to detect this sort of attack, but it expects that endpoints
will fail to establish a connection rather than recovering. For the
most part, the cryptographic handshake protocol [QUIC-TLS] is
responsible for detecting tampering during the handshake.

Endpoints are permitted to use other methods to detect and attempt to
recover from interference with the handshake. Invalid packets may be
identified and discarded using other methods, but no specific method
is mandated in this document.

21.2 放大攻击

An attacker might be able to receive an address validation token
(Section 8) from a server and then release the IP address it used to
acquire that token. At a later time, the attacker may initiate a
0-RTT connection with a server by spoofing this same address, which
might now address a different (victim) endpoint. The attacker can
thus potentially cause the server to send an initial congestion
window’s worth of data towards the victim.

Servers SHOULD provide mitigations for this attack by limiting the
usage and lifetime of address validation tokens (see Section 8.1.2).

21.3 乐观ACK攻击

An endpoint that acknowledges packets it has not received might cause
a congestion controller to permit sending at rates beyond what the
network supports. An endpoint MAY skip packet numbers when sending
packets to detect this behavior. An endpoint can then immediately

close the connection with a connection error of type
PROTOCOL_VIOLATION (see Section 10.3).

21.4 Slowloris攻击

The attacks commonly known as Slowloris [SLOWLORIS] try to keep many
connections to the target endpoint open and hold them open as long as
possible. These attacks can be executed against a QUIC endpoint by
generating the minimum amount of activity necessary to avoid being
closed for inactivity. This might involve sending small amounts of
data, gradually opening flow control windows in order to control the
sender rate, or manufacturing ACK frames that simulate a high loss
rate.

QUIC deployments SHOULD provide mitigations for the Slowloris
attacks, such as increasing the maximum number of clients the server
will allow, limiting the number of connections a single IP address is
allowed to make, imposing restrictions on the minimum transfer speed
a connection is allowed to have, and restricting the length of time
an endpoint is allowed to stay connected.

21.5 流碎片和重组攻击

An adversarial sender might intentionally send fragments of stream
data in order to cause disproportionate receive buffer memory
commitment and/or creation of a large and inefficient data structure.

An adversarial receiver might intentionally not acknowledge packets
containing stream data in order to force the sender to store the
unacknowledged stream data for retransmission.

The attack on receivers is mitigated if flow control windows
correspond to available memory. However, some receivers will over-
commit memory and advertise flow control offsets in the aggregate
that exceed actual available memory. The over-commitment strategy
can lead to better performance when endpoints are well behaved, but
renders endpoints vulnerable to the stream fragmentation attack.

QUIC deployments SHOULD provide mitigations against stream
fragmentation attacks. Mitigations could consist of avoiding over-
committing memory, limiting the size of tracking data structures,
delaying reassembly of STREAM frames, implementing heuristics based
on the age and duration of reassembly holes, or some combination.

21.6 流耗费攻击

An adversarial endpoint can open lots of streams, exhausting state on
an endpoint. The adversarial endpoint could repeat the process on a
large number of connections, in a manner similar to SYN flooding
attacks in TCP.

Normally, clients will open streams sequentially, as explained in
Section 2.1. However, when several streams are initiated at short
intervals, loss or reordering may cause STREAM frames that open
streams to be received out of sequence. On receiving a higher-
numbered stream ID, a receiver is required to open all intervening
streams of the same type (see Section 3.2). Thus, on a new
connection, opening stream 4000000 opens 1 million and 1 client-
initiated bidirectional streams.

The number of active streams is limited by the
initial_max_streams_bidi and initial_max_streams_uni transport
parameters, as explained in Section 4.5. If chosen judiciously,
these limits mitigate the effect of the stream commitment attack.
However, setting the limit too low could affect performance when
applications expect to open large number of streams.

21.7 服务的对端拒绝

QUIC and TLS both contain messages that have legitimate uses in some
contexts, but that can be abused to cause a peer to expend processing
resources without having any observable impact on the state of the
connection.

Messages can also be used to change and revert state in small or
inconsequential ways, such as by sending small increments to flow
control limits.

If processing costs are disproportionately large in comparison to
bandwidth consumption or effect on state, then this could allow a
malicious peer to exhaust processing capacity.

While there are legitimate uses for all messages, implementations
SHOULD track cost of processing relative to progress and treat
excessive quantities of any non-productive packets as indicative of
an attack. Endpoints MAY respond to this condition with a connection
error, or by dropping packets.

21.8 显示拥塞通知攻击

An on-path attacker could manipulate the value of ECN codepoints in
the IP header to influence the sender’s rate. [RFC3168] discusses
manipulations and their effects in more detail.

An on-the-side attacker can duplicate and send packets with modified
ECN codepoints to affect the sender’s rate. If duplicate packets are
discarded by a receiver, an off-path attacker will need to race the
duplicate packet against the original to be successful in this
attack. Therefore, QUIC endpoints ignore the ECN codepoint field on
an IP packet unless at least one QUIC packet in that IP packet is
successfully processed; see Section 13.4.

21.9 无状态重置Oracle

Stateless resets create a possible denial of service attack analogous
to a TCP reset injection. This attack is possible if an attacker is
able to cause a stateless reset token to be generated for a
connection with a selected connection ID. An attacker that can cause
this token to be generated can reset an active connection with the
same connection ID.

If a packet can be routed to different instances that share a static
key, for example by changing an IP address or port, then an attacker
can cause the server to send a stateless reset. To defend against
this style of denial service, endpoints that share a static key for
stateless reset (see Section 10.4.2) MUST be arranged so that packets
with a given connection ID always arrive at an instance that has
connection state, unless that connection is no longer active.

In the case of a cluster that uses dynamic load balancing, it’s
possible that a change in load balancer configuration could happen
while an active instance retains connection state; even if an
instance retains connection state, the change in routing and
resulting stateless reset will result in the connection being
terminated. If there is no chance in the packet being routed to the
correct instance, it is better to send a stateless reset than wait
for connections to time out. However, this is acceptable only if the
routing cannot be influenced by an attacker.

21.10 版本降级

This document defines QUIC Version Negotiation packets Section 6,
which can be used to negotiate the QUIC version used between two
endpoints. However, this document does not specify how this
negotiation will be performed between this version and subsequent
future versions. In particular, Version Negotiation packets do not

contain any mechanism to prevent version downgrade attacks. Future
versions of QUIC that use Version Negotiation packets MUST define a
mechanism that is robust against version downgrade attacks.

21.11 路由定向攻击

Deployments should limit the ability of an attacker to target a new
connection to a particular server instance. This means that client-
controlled fields, such as the initial Destination Connection ID used
on Initial and 0-RTT packets SHOULD NOT be used by themselves to make
routing decisions. Ideally, routing decisions are made independently
of client-selected values; a Source Connection ID can be selected to
route later packets to the same server.

22 IANA 注意事项

22.1 QUIC传输参数注册表

IANA [应当增加/已增加]一个在”QUIC协议”打头的用于QUIC传输参数的注册表。
“QUIC传输参数”注册表管理着一个16位的空间。 这个空间被分为由不同策略管理的两个空间。 第一个字节在0x00到0xfe(十六进制)范围内的值通过标准策略 [RFC8126]分配。 第一个字节0xff的值被保留为为隐私使用[RFC8126]。

注册必须包括以下字段:

值:分配的数值 (范围在0x0000~0xfeff).
参数名:参数的缩写名称。
规范:关于这个值的公开可用的规范的引用。

被提名的专家认证一个规范存在且是可访问的。鼓励专家偏向于批准注册,除非注册是滥用,轻浮或积极有害的(不仅在美学上令人不快或在架构上令人怀疑)

注册表的初始内容在表6。

+--------+-------------------------------------+---------------+
| Value  | Parameter Name                      | Specification |
+--------+-------------------------------------+---------------+
| 0x0000 | original_connection_id              | Section 18.2  |
|        |                                     |               |
| 0x0001 | idle_timeout                        | Section 18.2  |
|        |                                     |               |
| 0x0002 | stateless_reset_token               | Section 18.2  |
|        |                                     |               |
| 0x0003 | max_packet_size                     | Section 18.2  |
|        |                                     |               |
| 0x0004 | initial_max_data                    | Section 18.2  |
|        |                                     |               |
| 0x0005 | initial_max_stream_data_bidi_local  | Section 18.2  |
|        |                                     |               |
| 0x0006 | initial_max_stream_data_bidi_remote | Section 18.2  |
|        |                                     |               |
| 0x0007 | initial_max_stream_data_uni         | Section 18.2  |
|        |                                     |               |
| 0x0008 | initial_max_streams_bidi            | Section 18.2  |
|        |                                     |               |
| 0x0009 | initial_max_streams_uni             | Section 18.2  |
|        |                                     |               |
| 0x000a | ack_delay_exponent                  | Section 18.2  |
|        |                                     |               |
| 0x000b | max_ack_delay                       | Section 18.2  |
|        |                                     |               |
| 0x000c | disable_active_migration            | Section 18.2  |
|        |                                     |               |
| 0x000d | preferred_address                   | Section 18.2  |
|        |                                     |               |
| 0x000e | active_connection_id_limit          | Section 18.2  |
+--------+-------------------------------------+---------------+

       表6: 初始的QUIC传输参数条目

另外,格式 31 * N + 27 对于整数N每个值,也就是(27, 58, 59,…),不能由IANA赋值。

22.2 QUIC帧类型注册表

IANA [应当增加/已增加]一个在”QUIC协议”打头的用于QUIC帧种类的注册表。
QUIC 帧种类注册处管理着一个62位的空间。 这个空间划分为由不同策略管理的三块空间。 在0x00到0x3f(十六进制)之间的值通过标准行为或者IESG审核策略[RFC8126]分配。 在0x40到0x3fff之间的值通过声明必须政策[RFC8126]进行操作分配。 所有其他值由私有策略[RFC8126]分配。

注册必须包含以下字段:
值: 分配的数值(注册会在0x0000到0xfeff之间)。这区间内的值可能会被赋予。
帧名称:一个表示参数的简短词语。
规范:关于这个值的公开可用的规范的引用。

提名专家校验证规范存在且便利易读。对于新的注册的规范需要描述可识别的帧类型的方法。 终端可能决定它可以发送指定种类的帧。 大部分注册都希望伴随着传输参数注册(详见22.1节)。 规范需要描述帧中的所有帧中字段的格式和语义。
鼓励专家偏向于批准注册,除非注册是滥用,轻浮或积极有害的(不仅在美学上令人不快或在架构上令人怀疑)
注册表的初始内容列在表3中。

22.3 QUIC 传输错误码注册表

IANA [应当增加/已增加]一个在”QUIC协议”打头的QUIC传输错误码的注册表
“QUIC 传输错误码”注册表管理着一个62位的空间。 这个空间被分为由不同策略管理的三个空间。 在0x00到0xfe(十六进制)范围内的值通过标准行为或IESG预览策略 [RFC8126]分配。 0x40到0x3fff根据规范要求策略[RFC8126]操作。所有其他值分配为隐私使用[RFC8126]。

注册表必须包含以下字段:

值: 分配的数值(注册会在0x0000到0xfeff之间)。
错误码:一个表示参数的简短词语。
描述:一个错误码语义的简短描述,如果提供了规范引用,可能是一个概述。
规范:关于这个值的公开可用的规范的引用。
注册处的初始内容列在Table 7中。 在0xFF00 到 0xFFFF 之间的值保留用于私有[RFC8126]。
被提名的专家认证一个规范存在且是可访问的。鼓励专家偏向于批准注册,除非注册是滥用,轻浮或积极有害的(不仅在美学上令人不快或在架构上令人怀疑)

初始的注册表内容如表7所示。

+——+—————————+—————-+—————+
| Valu | Error | Description | Specification |
| e | | | |
+——+—————————+—————-+—————+
| 0x0 | NO_ERROR | No error | Section 20 |
| | | | |
| 0x1 | INTERNAL_ERROR | Implementation | Section 20 |
| | | error | |
| | | | |
| 0x2 | SERVER_BUSY | Server | Section 20 |
| | | currently busy | |
| | | | |
| 0x3 | FLOW_CONTROL_ERROR | Flow control | Section 20 |
| | | error | |
| | | | |
| 0x4 | STREAM_LIMIT_ERROR | Too many | Section 20 |
| | | streams opened | |
| | | | |
| 0x5 | STREAM_STATE_ERROR | Frame received | Section 20 |
| | | in invalid | |
| | | stream state | |
| | | | |
| 0x6 | FINAL_SIZE_ERROR | Change to | Section 20 |
| | | final size | |
| | | | |
| 0x7 | FRAME_ENCODING_ERROR | Frame encoding | Section 20 |
| | | error | |
| | | | |
| 0x8 | TRANSPORT_PARAMETER_ERROR | Error in | Section 20 |
| | | transport | |
| | | parameters | |
| | | | |
| 0xA | PROTOCOL_VIOLATION | Generic | Section 20 |
| | | protocol | |
| | | violation | |
| | | | |
| 0xD | CRYPTO_BUFFER_EXCEEDED | CRYPTO data | Section 20 |
| | | buffer | |
| | | overflowed | |
+——+—————————+—————-+—————+

表7: 初始QUIC传输错误码条目