Quic 协议详解--包格式

前言

上一篇<Quic 协议详解--简介>中简单的讲述了讲述了Quic的一些特性,本篇将讲述quic的包类型与格式。Quic有两种类型的包:特殊包(Specail Packets)和常规包(Regular Packets)。其中特殊包有两类:版本协商包(Version Negotiation Packets)和公共重置包(Public Reset Packets)。 常规包 包含了帧(Frame)信息。所有的Quic 包的大小应该不大于MTU,以避免ip 分片。当前的Quic实现在ipv6环境每个包 最大限制为1350的字节,ipv4环境下为1370,这两个限制都不包含ip 和 udp 头。

1,Quic 公共包头(Public Packet Header)

所有的Quic包都是以一个1~51字节的公共头开始的。其格式如下

0 1 2 3 4 8

+--------+--------+--------+--------+--------+--- ---+

| Public | Connection ID (64) ... | ->

|Flags(8)| (optional) |

+--------+--------+--------+--------+--------+--- ---+

9 10 11 12

+--------+--------+--------+--------+

| QUIC Version (32) | ->

| (optional) |

+--------+--------+--------+--------+

13 14 15 16 17 18 19 20

+--------+--------+--------+--------+--------+--------+--------+--------+

| Diversification Nonce | ->

| (optional) |

+--------+--------+--------+--------+--------+--------+--------+--------+

21 22 23 24 25 26 27 28

+--------+--------+--------+--------+--------+--------+--------+--------+

| Diversification Nonce Continued | ->

| (optional) |

+--------+--------+--------+--------+--------+--------+--------+--------+

29 30 31 32 33 34 35 36

+--------+--------+--------+--------+--------+--------+--------+--------+

| Diversification Nonce Continued | ->

| (optional) |

+--------+--------+--------+--------+--------+--------+--------+--------+

37 38 39 40 41 42 43 44

+--------+--------+--------+--------+--------+--------+--------+--------+

| Diversification Nonce Continued | ->

| (optional) |

+--------+--------+--------+--------+--------+--------+--------+--------+

45 46 47 48 49 50

+--------+--------+--------+--------+--------+--------+

| Packet Number (8, 16, 32, or 48) |

| (variable length) |

+--------+--------+--------+--------+--------+--------+

其中:

public flag:

0x01 = PUBLIC_FLAG_VERSION ,表示版本协商包,

0x02 = PUBLIC_FLAG_RESET 表示 Public Reset包

0x04 表示包头包含了32字节的 Diversification Nonce

0x08 表示header中包含8字节的connection id 这个标志位在所有的包中都应该被设置

在0x30上的两位表示packet number 的直接数,其中

0x30表示packet number 有6字节

0x20表示packet number 有4字节

0x10表示2个字节

0x00表示1个字节

0x40 保留

0x80 未使用

connection ID: 64位无符号整形,由client随机产生。标示一个连接

Quic Version : 32位 表示Quic 协议版本.只有当PUBLIC_FLAG_VERSION 被设置才会存在这个字段。server 只有在client提出的版本不支持的情况下,才会置PUBLIC_FLAG_VERSION。

Packet Number: sender 会给每一个 常规包分配一个packet number,对端发送的第一个包的 number 应该是1,其后的packet number 是递增的

public flag 处理流程

图1 public flag 处理流程

2,特殊包(Special Packets)

2.1 版本协商包(Version Negotiation Packet)

0 1 2 3 4 5 6 7 8

+--------+--------+--------+--------+--------+--------+--------+--------+--------+

| Public | Connection ID (64) | ->

|Flags(8)| |

+--------+--------+--------+--------+--------+--------+--------+--------+--------+

9 10 11 12 13 14 15 16 17

+--------+--------+--------+--------+--------+--------+--------+--------+---...--+

| 1st QUIC version supported | 2nd QUIC version supported | ...

| by server (32) | by server (32) |

+--------+--------+--------+--------+--------+--------+--------+--------+---...--+

版本协商包只会由server发送,且public flag 必须置位PUBLIC_FLAG_VERSION,此外还有8直接的connection id ,余下的部分,每4个字节表示server 所支持的Quic版本。

2.2 公共重置包(PublicResetPacket)

0 1 2 3 4 8

+--------+--------+--------+--------+--------+-- --+

| Public | Connection ID (64) ... | ->

|Flags(8)| |

+--------+--------+--------+--------+--------+-- --+

9 10 11 12 13 14

+--------+--------+--------+--------+--------+--------+---

| Quic Tag (32) | Tag value map ... ->

| (PRST) | (variable length)

+--------+--------+--------+--------+--------+--------+---

这个包格式的解释暂且放下

3,常规包(Regular Packets)

常规包是经过认证和加密的,公共头(Public Header)只经过认证,并没有加密,包的剩余部分都是经过加密的。一个常规包,包含一系列的Frames

帧包(Frame Packet)

帧包包含了一系列的type 前缀的frame, 一般的帧包格式如下:

+--------+---...---+--------+---...---+

| Type | Payload | Type | Payload |

+--------+---...---+--------+---...---+

4,Quic 连接的生命周期

4.1连接的建立

Quic连接的建立由client发起,连接的建立过程中包含了版本协商和加密的握手信息 以减少建连的时延。client发给server的每一个初始包都需要置version flag 并且指定具体使用的版本号,直到收到server的包取消设置version flag 位。server 收到了client发送的第一个未设置version flag 的包后,其它所有置位 version flag 的包都将被忽视。

当server收到一个包 带一个新的 Connection id,他会将client的版本与它所能支持的版本进行比较,如果server支持,那么server在整个连接的生命周期内都将使用这个版本号,其后server发送的所有的包都会取消置位version flag

如果server 不支持 client 的版本号,1个rtt 的时延将会产生。此时server 将会发送一个版本协商包(version negotiation packet)给client。此包将会置位version flag 并且携带一系列server 所支持的版本.

当client收到server的版本协商包的时候,他会选择一个自己能支持的版本,然后使用这个版本重发所有的包。这些包必须置位version flag 并且携带版本号。之后client会收到server的常规包,它将意味着版本协商结束。此后,client发送的包就可以不用置位version flag了。

为了防止降级攻击,client在第一个包中指定的协议版本和server应答的一系列支持的版本号都必须作为加密握手包的数据部分。client需要去验证 握手过程中的server提供的版本列表和版本协议包中的列表是匹配的。server也需要验证,握手过程中client的版本,server确实不支持。此外,在连接建立的过程中,握手的时候必须协商各种传输参数,

4.2,数据传输

Quic实现了连接的可依赖,拥塞控制,流量控制。Quic连接所有数据(Ack除外)的传输,包括握手包都是作为stream的数据部分传输。

4.3,Quic流(stream)的生命周期

流(stream)是双向独立的数据,被封装在stream frame里面。streams 既可以由client创建,也可以由server端创建,也可以和其它的stream交织在一起被发送,甚至可以被取消。stream的创建是隐式的,在指定的流上发送STREAM Frame。为了避免Stream id 冲突,如果是server初始化流,stream id必须是偶数,如果是client初始化流,那么stream id 必须是奇数。0是无效的stream id,1为握手保留,当使用http2 over quic stream id为3 也是保留的。

每个方向的stream id 都必须是单调递增的。比如说stream id 为2 的流可能后与 stream id 为3的流创建,但是stream id 为 7的流,必须先于stream id为 9的流创建。此外对端收到的流可能是乱序的。比如:server 可能先收到了 stream id 为9 的第10个包, 然后再收到 stream 7 第 9个包。

如果对端收到了一个STREAM Frame 但是又不想接收,它可以立马响应一个 RTS_STREAM Frame。此后收到的数据都将被忽视。一旦流被创建,它既可以被用于接收数据,也可以用来发送数据。quic协议的任意一方都可以正常的终止一条流,有如下三种方式终止流:

1,正常终止:因为stream是双向的,所以它可以半关闭和关闭,当一方发送了一个Frame并且在此Frame中置位Fin,该stream 进处于半关闭状态,Fin 意味着发送Fin方,在这条流上不会再有数据发送。当Quic的两端都发送且收到了Fin,此时这条流就处于关闭状态。

2,突然中止 : client和server 可以再任意时刻发送RST_STREAM, RST_STREAM帧包含了一个错误码,表示失败的原因,如果RST_STREAM帧由该流的初创者发送,它表示这条流出错了,后续不会再有数据发送,如果RST_STREAM帧是由流的接收者发送,那么发送方应该停止发送数据。有这么一种情况,发送方已经发送了数据(在路上,未被确认),同时又收到了RST_STREAM, 为了确保连接级别的流量控制,即使收到了RST_STREAM帧,发送者需要确保:Fin或者或有的数据都被对方收好了,或者RST_STREAM帧被对方收到,这也同时以为着,RST_STREAM的发送方需要以适当的WINDOW_UPDATEs 继续响应到来的帧,以保证发送方发送Fin的时候不会被阻塞。

3,当连接断开的时候,流也会终止

4.4,连接终止

连接应该是一直打开的,直到连接空闲时间超过双方协商的时间。当server决定终止一个空闲连接的时候,它不应该通知client,一个Quic连接一旦被建立,可以通过如下两种方式终止

1,显示的关闭(Explicit Shutdown): 一端发送CONNECTION_CLOSE帧给 初始化连接终止的另一端,一端可能会发送GOAWAY 帧给另一端表示连接将会马上终止。GOAWAY帧的接收方依然可以处理任何活跃的流,但是发送方将不会初始化任何新的流,同时也不会接收任何新到来的流。活跃流被终止的时候,可能是收到了CONNECTION_CLOSE如果一方发送了CONNECTION_CLOSE ,但是未终止的流依然是活跃的,对端必须认为这条流是未完成的,是异常终止的

2,隐式的关闭(Implicit Shutdown): Quic连接默认的空闲超时时间是30s, 最大值为10min,如果在超时时间范围内没有活跃的流,连接将会被关闭。默认会发送一个CONNECTION_CLOSE帧。

5,帧类型与格式(Frame Type and formats)

Quic的帧包(Frame Packet)由 frame填充,包有特殊包和常规包,帧类型也有特殊帧(Special Frame Type)和常规帧(Regular Frame Type)。

5.1帧类型

5.1.1 Special Frame Type

+------------------+-----------------------------+

| Type-field value | Control Frame-type |

+------------------+-----------------------------+

| 1fdooossB | STREAM |

| 01ntllmmB | ACK |

| 001xxxxxB | CONGESTION_FEEDBACK |

+------------------+-----------------------------+

5.1.2 Regular Frame Type

+------------------+-----------------------------+

| Type-field value | Control Frame-type |

+------------------+-----------------------------+

| 00000000B (0x00) | PADDING |

| 00000001B (0x01) | RST_STREAM |

| 00000010B (0x02) | CONNECTION_CLOSE |

| 00000011B (0x03) | GOAWAY |

| 00000100B (0x04) | WINDOW_UPDATE |

| 00000101B (0x05) | BLOCKED |

| 00000110B (0x06) | STOP_WAITING |

| 00000111B (0x07) | PING |

+------------------+-----------------------------+

5.2 格式

5.2.1 STREAM Frame

STREAM Frame既用于隐式创建一条流,也用于发送数据,格式如下

0 1 … SLEN

+--------+--------+--------+--------+--------+

|Type (8)| Stream ID (8, 16, 24, or 32 bits) |

| | (Variable length SLEN bytes) |

+--------+--------+--------+--------+--------+

SLEN+1 SLEN+2 … SLEN+OLEN

+--------+--------+--------+--------+--------+--------+--------+--------+

| Offset (0, 16, 24, 32, 40, 48, 56, or 64 bits) (variable length) |

| (Variable length: OLEN bytes) |

+--------+--------+--------+--------+--------+--------+--------+--------+

SLEN+OLEN+1 SLEN+OLEN+2

+-------------+-------------+

| Data length (0 or 16 bits)|

| Optional(maybe 0 bytes) |

+------------+--------------+

STREAM Frame中各个字段的说明如下:

Type: type是一个8bit的值,包含各种flags(1fdooossB).最左边的bit 必须设置为1,表示这是一个STREAM frame, 'f' bit位是 Fin 位,当该位被设置为1时,表示发送方已经发完了,连接将处于半关闭状态。'd' bit表示在STREAM header中是否包含有数据长度,当被设置为0时,表示 知道包尾都是STREAM Frame,后续的三个'ooo'bit表示Offset 长度,分别为 0,16,24,32,40,48,58,64位 ,再接下来的两个'ss'表示stream id的长度,取值为8,16,24,32

Stream ID: 可变的无符号整形,唯一区分这条流

Offset: 可变长度,表示这块数据在整个stream 中的偏移。

Data Length: 可选的16位无符号整形,表示在这个stream frame中数据的长度。

5.2.2 ACK Frame

发送ack frame用来通知对端哪些包收到了,哪些包可能丢失了。ack 帧包括1~256个ack blocks, ack block是包的确认区间,类似于TCP的SACK blocks ,如果一个未发送的包被确认了,发送方应该总是关闭这个链接,这种机制能够防止潜在的攻击。

5.2.3 STOP_WATTING Frame

STOP_WATING Frame 帧告诉对端,它不应该再等包序号低于某个指定值的数据包了,包序号可由1,2,4,6个直接编码表示,和公共包头中的包序号长度相同。帧格式如下:

0 1 2 3 4 5 6

+--------+--------+--------+--------+--------+-------+-------+

|Type (8)| Least unacked delta (8, 16, 32, or 48 bits) |

| | (variable length) |

+--------+--------+--------+--------+--------+--------+------+

Frame Type:必须被设置为0x06表示这个是一个STOP_WATING Frame

Least Unacked Delta:可变长的包序号差值(packet number delta), 用公共头中的包序号减去它可以得到最小的未确认号(the least unacked,这个结果是发送方等待的最小的ack确认号,如果接收方丢失了比这个结果更小值的包序号,接收方应该认为这些包不可撤销的丢失了。

5.2.4 WINDOW_UPDATE Frame

WINDOW_UPDATE Frame用于通告对端接收窗口增加了,如果Stream ID 等于0,表示这是连接级别的流量控制,否则是流级别的流量控制,表示在指定的流上应该增加其流量控制窗口。帧格式如下:

0 1 4 5 12

+--------+--------+-- ... --+-------+--------+-- ... --+-------+

|Type(8) | Stream ID (32 bits) | Byte offset (64 bits) |

+--------+--------+-- ... --+-------+--------+-- ... --+-------+

Frame Type:8bit,必须设置成0x40,表示他是WINDOW_UPDATE Frame

Stream ID: 0表示连接级别的流量控制,非0 指定的流

Byte offset: 64bit 表示指定流上数据的绝对偏移。如果是连接级别的流量控制,表示所有流总数据的绝对偏移

byte offset 表示WINDOW_Update Frame 帧的接收方 在指定的stream 上只能发送这么多的数据,如果发多了,对端可能会关闭连接。如果再指定的stream id 上收到了多个WINDOW_UPDATE帧,接收方只需要记录最大最大byteoffset的帧。流和连接级别的流量控制窗口初始时都是16KB,在握手阶段会增加。在握手的过程中双方会协商SFCW(Stream Flow Control Wind) 和 CFCW(Connection Flow Control Wind)参数。

5.2.5 BLOCK Frame

BLOCK Frame 告诉对端,我有数据要发送,但是被阻塞了,它纯粹是一个通知信息帧,在调试的时候有用。接收方收到BLOCK Frame 后会打印一些日志信息,然后直接丢弃这个包。其帧结构如下:

+--------+--------+--------+--------+--------+

|Type(8) | Stream ID (32 bits) |

+--------+--------+--------+--------+--------+

Frame Type: 必须设置成0x05

5.2.6 RST_STREAM Frame

RST_STREAM Frame可以异常终止一条流,当该帧又流的创建者发送时,表示创建者希望取消这条流,当接收方发送该帧时,表示出现了错误,或者接收者不想接收这条流,应该这条流应该被关闭。帧格式如下:

0 1 4 5 12 8 16

+-------+--------+-- ... ----+--------+-- ... ------+-------+-- ... ------+

|Type(8)| StreamID (32 bits) | Byte offset (64 bits)| Error code (32 bits)|

+-------+--------+-- ... ----+--------+-- ... ------+-------+-- ... ------+

帧的各个字段:

Frame Type: 8bit 必须设置为0x01

Byte offset:最后数据部分在流的绝对偏移

Error Code: 32位错误码

CONNECTION_CLOSE Frame

用来告知对方,连接正在被关闭。如果还有流是活跃的,那么当连接被关闭的时候,所有的流也将会断开。理想情况下,应该发送一个GOAWAY帧 给予足够的时间,让所有的流主动断开。帧格式如下:

0 1 4 5 6 7

+--------+--------+-- ... -----+--------+--------+--------+----- ...

|Type(8) | Error code (32 bits)| Reason phrase | Reason phrase

| | | length (16 bits) | (variable length)

+--------+--------+-- ... -----+--------+--------+--------+----- ...

字段说明:

Frame type: 必须设置为0x02,表示该帧为CONNECTION_CLOSE Frame

Error Code : 关闭的原因

Reason Phase Length: 原因说明字段的长度

Reason Phrase: 原因说明

5.2.7 GOAWAY Frame

告诉对方,连接马上很可能被中断,应该停止使用该连接。活跃的流还可以继续处理,但是不应该再初始化创建任何流了,也不应该再接受新的流了。帧格式如下:

0 1 4 5 6 7 8

+--------+--------+-- ... -----+-------+-------+-------+------+

|Type(8) | Error code (32 bits)| Last Good Stream ID (32 bits)| ->

+--------+--------+-- ... -----+-------+-------+-------+------+

9 10 11

+--------+--------+--------+----- ...

| Reason phrase | Reason phrase

| length (16 bits)|(variable length)

+--------+--------+--------+----- ...

帧的各字段如下:

Frame tyep: 必须设置为0x03

Last Good Stream ID:GOAWAY帧发送者所接受的最后一个Stream ID.没有设置为0.

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券