前言
QUIC版本众多,主要有谷歌家的google QUIC,以及IETF致力于将QUIC标准化,即IETF QUIC(iQUIC),还有Facebook家的mvfst。早期各家的QUIC都有自己定制的字段,但总体是大同小异。本文结合抓包分析google quiche项目中的报文头部信息
并通过google quiche 报文封装和序列化来深入学习quic的个协议各字段的意义和特征
Long Header Packets
Long Header Packet {
Header Form (1) = 1,
Fixed Bit (1) = 1,
Long Packet Type (2),
Type-Specific Bits (4),
Version (32),
Destination Connection ID Length (8),
Destination Connection ID (0..160),
Source Connection ID Length (8),
Source Connection ID (0..160),
Type-Specific Payload (..),
}
Figure 13: Long Header Packet Format
-
其内存分布如下:
长报头用于在1-RTT密钥建立之前发送的数据包。一旦1-RTT密钥可用,发送方切换到使用短报头发送数据包(章节17.3)。长格式允许特殊的数据包——比如版本协商包——以这种统一的固定长度的数据包格式表示。使用长报头的数据包包含以下字段
其中第一个字节定义包类型相关信息,google quiche对它的定义如下:
enum QuicPacketHeaderTypeFlags : uint8_t {
....
// Bit 6: the 'QUIC' bit.
FLAGS_FIXED_BIT = 1 << 6,
// Bit 7: Indicates the header is long or short header.
FLAGS_LONG_HEADER = 1 << 7,(bit 7置位)
};
- 以上定义在quiche/quic/core/quic_types.h当中
- 以上为google 定义,其中bit 7和 bit6和RFC是对得上的
- Type-Specific Bits (4)(bit0~bit3)为特殊定义,这里用于定义紧跟Source Connection ID之后的Type-Specific Payload (..)所对应的长度信息
- 而对于长报头格式的,bit4和bit5(Long Packet Type (2))两位定义了长报文类型如下:
Type | Name | Section |
---|---|---|
0x00 | Initial | Section 17.2.2 |
0x01 | 0-RTT | Section 17.2.3 |
0x02 | Handshake | Section 17.2.4 |
0x03 | Retry | Section 17.2.5 |
Table 5: Long Header Packet Types
- 以下以Initial包为例进行说明
Initial Packet {
Header Form (1) = 1,
Fixed Bit (1) = 1,
Long Packet Type (2) = 0,
Reserved Bits (2),
Packet Number Length (2),
Version (32),
Destination Connection ID Length (8),
Destination Connection ID (0..160),
Source Connection ID Length (8),
Source Connection ID (0..160),
Token Length (i),
Token (..),
Length (i),
Packet Number (8..32),
Packet Payload (8..),
}
Figure 15: Initial Packet
Packet Number Length:对应第一个字节的bit[0~1],表示Packet Number所占长度(字节)- 1,比如一个字节bit[0~1] = 0x00
Version:四个字节,包括一个 32 位版本字段。 RFC9000的Version是1,其实也可以理解这是QUIC标准化后的第一个版本
Destination Connection ID Length:目标连接 ID 字段的字节长度。 此长度编码为 8 位无符号整数
Destination Connection ID:目标连接 ID 字段,长度在 0 到 160个字节之间
Source Connection ID Length:源连接 ID 字段的字节长度。 此长度编码为 8 位无符号整数
Source Connection ID:源连接 ID 字段,长度在 0 到 160个字节之间
Token Length (i):一个可变长度的整数,指定Token字段的长度,单位为字节。如果不存在令牌,则该值为0。服务器发送的初始数据包必须设置令牌长度字段为0;客户端接收到一个带有非零令牌长度字段的初始数据包必须丢弃该数据包或产生一个连接错误
Token (..):token的内容
Packet Number (8..32):包ID(支持8~32bit),Initial包默认使用1字节
Packet Payload (8..):至少一个字节,由FRAME组成
-
以下是以Initial报文为例抓的案例报文:
以上报文第一字节,bit[7]=1,标识long header packets type,bit[4~5] = 0x00,标识为Initial报文类型,bit[0~1]=0x00表示Packet Number Length为1字节
版本号等于1表示RFCv1
目标连接ID的长度为8个字节,目标连接ID的内容为e9 cc 63 49 52 3b 1d 8a
源连接ID的长度为0,所以内容没有
Token 长度为0,所以没有内容
长度:1232字节
Packet Payload (8..):表示内容。。
Short Header Packets
- 1-RTT报文使用短报文头。在版本和1-RTT密钥协商后使用
1-RTT Packet {
Header Form (1) = 0,
Fixed Bit (1) = 1,
Spin Bit (1),
Reserved Bits (2),
Key Phase (1),
Packet Number Length (2),
Destination Connection ID (0..160),
Packet Number (8..32),
Packet Payload (8..),
}
- 带有短包头的 QUIC 数据包的第一个字节的高位设置为 0
- 带有短包头的 QUIC 数据包包括紧跟在第一个字节之后的目标连接 ID。 短包头不包括目标连接 ID 长度、源连接 ID 长度、源连接 ID 或版本字段。 目标连接 ID 的长度没有编码在具有短包头的数据包中,并且不受本规范的限制
- 数据包的其余部分具有特定于版本的语义
google quic 报文封装和序列化介绍
通过上面的学习,对quic报文已经有一个初步的认识,为了更深入的学习google quiche,本节打算从google quic包的创建以及序列化来继续深入学习
google quic中创建报文是在QuicPacketCreator完成,而封包序列化以及加密是在QuicFramer中完成的
-
首先我们大致介绍一下如何创建一个Quic包,需要经历怎样的流程
以上是以Initial报文为例创建一个SerializedPacket并将其序列化大致如上图流程
其中QuicPacketCreator主要是负责两外部模块传递过来的QuicFrame聚合到queued_frames_队列当中,以及填充QuicPacketHeader
而QuicFramer负责按照RFC协议,序列化QUIC包头(写内存),以及将应用层的payload信息进行加密和序列化处理
最终通过FlushCurrentPacket()函数处理后得到的是一个SerializedPacket包
接下来逐一分析每个函数的实现..
QuicPacketCreator::FillPacketHeader()分析
bool QuicPacketCreator::SerializePacket(QuicOwnedPacketBuffer encrypted_buffer,
size_t encrypted_buffer_len,
bool allow_padding) {
QuicPacketHeader header;
// FillPacketHeader increments packet_number_.
FillPacketHeader(&header);
..
return true;
}
- 首先定义QuicPacketHeader并使用FillPacketHeader()对其填充
void QuicPacketCreator::FillPacketHeader(QuicPacketHeader* header) {
header->destination_connection_id = GetDestinationConnectionId();
header->destination_connection_id_included =
GetDestinationConnectionIdIncluded();
header->source_connection_id = GetSourceConnectionId();
header->source_connection_id_included = GetSourceConnectionIdIncluded();
header->reset_flag = false;
header->version_flag = IncludeVersionInHeader();// initial、handleshake、和0RTT包都为true
if (IncludeNonceInPublicHeader()) {
QUICHE_DCHECK_EQ(Perspective::IS_SERVER, framer_->perspective())
<< ENDPOINT;
header->nonce = &diversification_nonce_;
} else {
header->nonce = nullptr;
}
packet_.packet_number = NextSendingPacketNumber();
header->packet_number = packet_.packet_number;
header->packet_number_length = GetPacketNumberLength();// (initial报文初始化为1字节,最后似乎会被修正)
header->retry_token_length_length = GetRetryTokenLengthLength();
header->retry_token = GetRetryToken();
header->length_length = GetLengthLength();
header->remaining_packet_length = 0;
if (!HasIetfLongHeader()) {
return;
}
header->long_packet_type =
EncryptionlevelToLongHeaderType(packet_.encryption_level);
}
- 假设以Initial报文为例,以上就是对Initial Long header type 的头部进行数据填充,主要包括目标连接ID,源ID,packet_number,packet_number_length,包长度以及首个字节进行填充
QuicFramer::BuildDataPacket()分析
-
通过对代码进行梳理,该函数的大致流程如下:
bool QuicPacketCreator::SerializePacket(QuicOwnedPacketBuffer encrypted_buffer,
size_t encrypted_buffer_len,
bool allow_padding) {
....
size_t length;
absl::optional length_with_chaos_protection =
MaybeBuildDataPacketWithChaosProtection(header, encrypted_buffer.buffer);
if (length_with_chaos_protection.has_value()) {
length = length_with_chaos_protection.value();
} else {// Initial 包走这
length = framer_->BuildDataPacket(header, queued_frames_,
encrypted_buffer.buffer, packet_size_,
packet_.encryption_level);
}
..
return true;
}
- QuicPacketCreator模块中持有QuicFramer指针,在QuicPacketCreator模块的SerializePacket()函数中调用QuicFramer::BuildDataPacket()函数并以queued_frames_、上一步中的QuicPacketHeader、分配好的buffer*作为参数进行传递
size_t QuicFramer::BuildDataPacket(const QuicPacketHeader& header,
const QuicFrames& frames, char* buffer,
size_t packet_length,
EncryptionLevel level) {
// 以buffer为参数构造writer,然后基于该buffer序列化写入数据
QuicDataWriter writer(packet_length, buffer);
size_t length_field_offset = 0;
if (!AppendIetfPacketHeader(header, &writer, &length_field_offset)) {
QUIC_BUG(quic_bug_10850_16) << "AppendPacketHeader failed";
return 0;
}
// 本文默认支持http3
if (VersionHasIetfQuicFrames(transport_version())) {
if (AppendIetfFrames(frames, &writer) == 0) {
return 0;
}
if (!WriteIetfLongHeaderLength(header, &writer, length_field_offset,
level)) {
return 0;
}
return writer.length();
}
...
}
- 以上代码简单精炼,首先将已经赋值的QuicPacketHeader头信息写入内存buffer
- 其次通过调用AppendIetfFrames()将QuicPayload信息也就是QuicFrame写入到内存buffer
AppendIetfPacketHeader()
bool QuicFramer::AppendIetfHeaderTypeByte(const QuicPacketHeader& header,
QuicDataWriter* writer) {
uint8_t type = 0;
// Initital,handshake和0-RTT包为true,除此之外为false
if (header.version_flag) {
type = static_cast(
FLAGS_LONG_HEADER | FLAGS_FIXED_BIT |
LongHeaderTypeToOnWireValue(header.long_packet_type, version_) |
PacketNumberLengthToOnWireValue(header.packet_number_length));//
} else {
type = static_cast(
FLAGS_FIXED_BIT | (current_key_phase_bit_ ? FLAGS_KEY_PHASE_BIT : 0) |
PacketNumberLengthToOnWireValue(header.packet_number_length));
}
return writer->WriteUInt8(type);
}
- 以上函数为写入头部首字节信息到buffer
- 其中函数PacketNumberLengthToOnWireValue()的返回值为PacketNumber所占的字节数-1,也就是假设PacketNumber使用一个字节标识,那么就是0,对于长类型的报文,对应的是首字节的bit[0~1]
- 其中函数LongHeaderTypeToOnWireValue()函数在RFCv1中全部返回0
bool QuicFramer::AppendIetfPacketHeader(const QuicPacketHeader& header,
QuicDataWriter* writer,
size_t* length_field_offset) {
QuicConnectionId server_connection_id =
GetServerConnectionIdAsSender(header, perspective_);
// 1) 写入头部首字节到buffer
if (!AppendIetfHeaderTypeByte(header, writer)) {
return false;
}
// Initital,handshake和0-RTT包为true,除此之外为false
if (header.version_flag) {
// Append version for long header.
// 2) 写入RFCv1或RFCv2等,对应协议中的Version字段
QuicVersionLabel version_label = CreateQuicVersionLabel(version_);
if (!writer->WriteUInt32(version_label)) {
return false;
}
}
// Append connection ID.
//3) 序列化写入connection id信息,包括目标和源 ()
if (!AppendIetfConnectionIds(
header.version_flag, version_.HasLengthPrefixedConnectionIds(),
header.destination_connection_id_included != CONNECTION_ID_ABSENT
? header.destination_connection_id
: EmptyQuicConnectionId(),
header.source_connection_id_included != CONNECTION_ID_ABSENT
? header.source_connection_id
: EmptyQuicConnectionId(),
writer)) {
return false;
}
last_serialized_server_connection_id_ = server_connection_id;
// TODO(b/141924462) Remove this QUIC_BUG once we do support sending RETRY.
if (QuicVersionHasLongHeaderLengths(transport_version()) &&
header.version_flag) {
if (header.long_packet_type == INITIAL) {
QUICHE_DCHECK_NE(quiche::VARIABLE_LENGTH_INTEGER_LENGTH_0,
header.retry_token_length_length)
<< ENDPOINT << ParsedQuicVersionToString(version_)
<< " bad retry token length length in header: " << header;
// Write retry token length.
// 4) 对于initial包需要写入token长度,上面抓包案例中长度为0
if (!writer->WriteVarInt62WithForcedLength(
header.retry_token.length(), header.retry_token_length_length)) {
return false;
}
// Write retry token.
// 4) 如果token长度不为0,并且retry_token不为空,这里写入token
if (!header.retry_token.empty() &&
!writer->WriteStringPiece(header.retry_token)) {
return false;
}
}
if (length_field_offset != nullptr) {
*length_field_offset = writer->length();
}
// 5) Length (i)字段
// Add fake length to reserve two bytes to add length in later.
writer->WriteVarInt62(256);
} else if (length_field_offset != nullptr) {
*length_field_offset = 0;
}
//6) 写入packet number Append packet number.
if (!AppendPacketNumber(header.packet_number_length, header.packet_number,
writer)) {
return false;
}
last_written_packet_number_length_ = header.packet_number_length;
....
return true;
}
- 该函数的操作完全是按照协议文档中定义顺序来进行写入的,假设以Initial包为例,则整理如下:
- 1)写入头的首1个字节,对应长类型,Initial类型,packetNumber用多少字节来描述(1个字节所以为0)
- 2)写入Version字段表示Quic使用哪个协议版本当前为RFCv1
- 3)写入 Destination Connection ID Length (8),Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160)字段信息
- 4)写入Token Length (i),Token (..)字段信息
- 5)写入 Length (i)字段,表示该包的长度信息,这里是写了一个256,该字段占两个字节,这里是先预留空间,长度信息是在后面添加(https://www.rfc-editor.org/rfc/rfc9000.html#name-variable-length-integer-enc)
- 6)写入Packet Number (8..32)信息,这里是实际的number编号
- 接下来再看QuicFrames真正的payload是怎么写入进内存的
AppendIetfFrames()
size_t QuicFramer::AppendIetfFrames(const QuicFrames& frames,
QuicDataWriter* writer) {
size_t i = 0;
for (const QuicFrame& frame : frames) {
// Determine if we should write stream frame length in header.
const bool last_frame_in_packet = i == frames.size() - 1;
if (!AppendIetfFrameType(frame, last_frame_in_packet, writer)) {
QUIC_BUG(quic_bug_10850_30)
<< "AppendIetfFrameType failed: " << detailed_error();
return 0;
}
switch (frame.type) {
...
case CRYPTO_FRAME:
if (!AppendCryptoFrame(*frame.crypto_frame, writer)) {
QUIC_BUG(quic_bug_10850_50)
<< "AppendCryptoFrame failed: " << detailed_error();
return 0;
}
break;
....
}
++i;
}
return writer->length();
}
- 对代码进行删减,只保留CRYPTO_FRAME的写入
- 首先调用AppendIetfFrameType()写入Frame类型字段(uint8_t)占一个字节,然后再根据不同的Frame类型将实际数据进行写入,其中AppendCryptoFrame()的实现如下:
bool QuicFramer::AppendCryptoFrame(const QuicCryptoFrame& frame,
QuicDataWriter* writer) {
// 1) 写入该frame在包中的内存偏移
if (!writer->WriteVarInt62(static_cast(frame.offset))) {
set_detailed_error("Writing data offset failed.");
return false;
}
// 2) 写入该frame的长度信息
if (!writer->WriteVarInt62(static_cast(frame.data_length))) {
set_detailed_error("Writing data length failed.");
return false;
}
// 3) 写入该frame的payload信息
if (data_producer_ == nullptr) {
if (frame.data_buffer == nullptr ||
!writer->WriteBytes(frame.data_buffer, frame.data_length)) {
set_detailed_error("Writing frame data failed.");
return false;
}
} else {
QUICHE_DCHECK_EQ(nullptr, frame.data_buffer);
if (!data_producer_->WriteCryptoData(frame.level, frame.offset,
frame.data_length, writer)) {
return false;
}
}
return true;
}
- 以上函数引入了QuicStreamFrameDataProducer模块,事实上quic 业务层的数据在封包的过程中是没有传递到QuicPacketCreator和QuicFramer模块的,而是缓存在更上的业务QuicConnection模块当中,而QuicSession模块是由QuicStreamFrameDataProducer模块派生出来的,同时QuicFramer和QuicPacketCreator都属于QuicSession的成员变量,并且在初始化QuicSession的时候会将QuicFramer指针传递到QuicPacketCreator当中
- 本节结合QuicCryptoFrame协议中的定义来分析其写入过程
CRYPTO Frame {
Type (i) = 0x06,(8bit)
Offset (i),->(64bit)
Length (i),->(64bit)
Crypto Data (..),
}
- Figure 30: CRYPTO Frame Format
- Type:表示该类型frame的值(在AppendIetfFrameType函数中写入)
- 1)Offset:表示的是该frame在该报文中内存偏移的启始地址
- 2)Length:表示该frame的具体长度信息
- 3)Crypto Data:表示真实的Payload..
WriteIetfLongHeaderLength()
在介绍该函数前,首先通过RFC9000了解一下这个长度信息的编码机制
QUIC数据包和帧通常使用可变长度编码来表示非负整数值。这种编码确保较小的整数值需要较少的字节来编码。QUIC变长整数编码保留第一个字节的两个最高位,以字节为单位对整数编码长度的以2为基数的对数进行编码。整数值以网络字节顺序在剩余的位上编码。
这意味着整数以1、2、4或8字节编码,可以分别编码6位、14位、30位或62位的值。表4总结了编码属性。
2MSB | Length | Usable Bits | Range |
---|---|---|---|
00 | 1 | 6 | 0-63 |
01 | 2 | 14 | 0-16383 |
10 | 4 | 30 | 0-1073741823 |
11 | 8 | 62 | 0-4611686018427387903 |
- 为啥在上面的分析中有出现WriteVarInt62xx这种,原理就在这,8字节,其中高两位为0x11
bool QuicFramer::WriteIetfLongHeaderLength(const QuicPacketHeader& header,
QuicDataWriter* writer,
size_t length_field_offset,
EncryptionLevel level) {
// 通过协议版本判断是否支持这个
if (!QuicVersionHasLongHeaderLengths(transport_version()) ||
!header.version_flag || length_field_offset == 0) {
return true;
}
// Length字段是在token之后,packet number之前
if (writer->length() < length_field_offset ||
writer->length() - length_field_offset <
quiche::kQuicheDefaultLongHeaderLengthLength) {
set_detailed_error("Invalid length_field_offset.");
QUIC_BUG(quic_bug_10850_14) << "Invalid length_field_offset.";
return false;
}
// 这里其实就是得到packet number 和 payload的长度,
// https://www.rfc-editor.org/rfc/rfc9000.html#name-long-header-packets
// This is the length of the remainder of the packet (that is, the Packet Number and Payload fields) in bytes, encoded as a variable-length integer (Section 16).
size_t length_to_write = writer->length() - length_field_offset -
quiche::kQuicheDefaultLongHeaderLengthLength;
// Add length of auth tag.
length_to_write = GetCiphertextSize(level, length_to_write);
QuicDataWriter length_writer(writer->length() - length_field_offset,
writer->data() + length_field_offset);
if (!length_writer.WriteVarInt62WithForcedLength(
length_to_write, quiche::kQuicheDefaultLongHeaderLengthLength)) {
set_detailed_error("Failed to overwrite long header length.");
QUIC_BUG(quic_bug_10850_15) << "Failed to overwrite long header length.";
return false;
}
return true;
}
- 参数length_field_offset就是在上面AppendIetfPacketHeader()函数中得出来的,记录了Length字段在Buffer中的偏移
- 到这一步骤其实所有的信息(除这个length字段空了两个字节)其他所有信息已经全部被序列化
- 得到Packet Number and Payload 的长度,根据RFC9000,这两个域的长度会被编码为可变长度整数
- length_writer.WriteVarInt62WithForcedLength写入内存
总结
- 通过本文学习主要可以了解quic 报文的相关协议,以及各类包的定义规则和内存分布
- 同时通过google quiche的源码分析来了解如果封装一个quic报文以及序列化一个报文
- 本文对后续对google quiche框架深入学习有十分重要的意义
参考文献
- RFC9000
- IETF QUIC v1 Design