前言
QUIC版本眾多,主要有谷歌家的google QUIC,以及IETF致力于將QUIC標準化饲宛,即IETF QUIC(iQUIC)栗精,還有Facebook家的mvfst馁龟。早期各家的QUIC都有自己定制的字段缠诅,但總體是大同小異解阅。本文結合抓包分析google quiche項目中的報文頭部信息
并通過google quiche 報文封裝和序列化來深入學習quic的個協(xié)議各字段的意義和特征
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
-
其內存分布如下:
quic_rfc9000_packet_01.png
長報頭用于在1-RTT密鑰建立之前發(fā)送的數(shù)據包墩衙。一旦1-RTT密鑰可用换吧,發(fā)送方切換到使用短報頭發(fā)送數(shù)據包(章節(jié)17.3)折晦。長格式允許特殊的數(shù)據包——比如版本協(xié)商包——以這種統(tǒng)一的固定長度的數(shù)據包格式表示。使用長報頭的數(shù)據包包含以下字段
其中第一個字節(jié)定義包類型相關信息,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..),
}
Packet Number Length:對應第一個字節(jié)的bit[0~1],表示Packet Number所占長度(字節(jié))- 1,比如一個字節(jié)bit[0~1] = 0x00
Version:四個字節(jié)沾瓦,包括一個 32 位版本字段满着。 RFC9000的Version是1,其實也可以理解這是QUIC標準化后的第一個版本
Destination Connection ID Length:目標連接 ID 字段的字節(jié)長度贯莺。 此長度編碼為 8 位無符號整數(shù)
Destination Connection ID:目標連接 ID 字段风喇,長度在 0 到 160個字節(jié)之間
Source Connection ID Length:源連接 ID 字段的字節(jié)長度。 此長度編碼為 8 位無符號整數(shù)
Source Connection ID:源連接 ID 字段缕探,長度在 0 到 160個字節(jié)之間
Token Length (i):一個可變長度的整數(shù)魂莫,指定Token字段的長度,單位為字節(jié)爹耗。如果不存在令牌耙考,則該值為0。服務器發(fā)送的初始數(shù)據包必須設置令牌長度字段為0;客戶端接收到一個帶有非零令牌長度字段的初始數(shù)據包必須丟棄該數(shù)據包或產生一個連接錯誤
Token (..):token的內容
Packet Number (8..32):包ID(支持8~32bit),Initial包默認使用1字節(jié)
Packet Payload (8..):至少一個字節(jié)潭兽,由FRAME組成
-
以下是以Initial報文為例抓的案例報文:
quic_rfc9000_packet_02.png 以上報文第一字節(jié)倦始,bit[7]=1,標識long header packets type,bit[4~5] = 0x00,標識為Initial報文類型,bit[0~1]=0x00表示Packet Number Length為1字節(jié)
版本號等于1表示RFCv1
目標連接ID的長度為8個字節(jié),目標連接ID的內容為e9 cc 63 49 52 3b 1d 8a
源連接ID的長度為0山卦,所以內容沒有
Token 長度為0鞋邑,所以沒有內容
長度:1232字節(jié)
Packet Payload (8..):表示內容。怒坯。
Short Header Packets
- 1-RTT報文使用短報文頭炫狱。在版本和1-RTT密鑰協(xié)商后使用
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 數(shù)據包的第一個字節(jié)的高位設置為 0
- 帶有短包頭的 QUIC 數(shù)據包包括緊跟在第一個字節(jié)之后的目標連接 ID。 短包頭不包括目標連接 ID 長度剔猿、源連接 ID 長度视译、源連接 ID 或版本字段。 目標連接 ID 的長度沒有編碼在具有短包頭的數(shù)據包中归敬,并且不受本規(guī)范的限制
- 數(shù)據包的其余部分具有特定于版本的語義
google quic 報文封裝和序列化介紹
通過上面的學習酷含,對quic報文已經有一個初步的認識,為了更深入的學習google quiche汪茧,本節(jié)打算從google quic包的創(chuàng)建以及序列化來繼續(xù)深入學習
google quic中創(chuàng)建報文是在QuicPacketCreator完成椅亚,而封包序列化以及加密是在QuicFramer中完成的
-
首先我們大致介紹一下如何創(chuàng)建一個Quic包,需要經歷怎樣的流程
quic_rfc9000_packet_05.png 以上是以Initial報文為例創(chuàng)建一個SerializedPacket并將其序列化大致如上圖流程
其中QuicPacketCreator主要是負責兩外部模塊傳遞過來的QuicFrame聚合到queued_frames_隊列當中舱污,以及填充QuicPacketHeader
而QuicFramer負責按照RFC協(xié)議呀舔,序列化QUIC包頭(寫內存),以及將應用層的payload信息進行加密和序列化處理
最終通過FlushCurrentPacket()函數(shù)處理后得到的是一個SerializedPacket包
接下來逐一分析每個函數(shù)的實現(xiàn)..
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字節(jié)霜瘪,最后似乎會被修正)
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 的頭部進行數(shù)據填充惧磺,主要包括目標連接ID,源ID颖对,packet_number,packet_number_length,包長度以及首個字節(jié)進行填充
QuicFramer::BuildDataPacket()分析
-
通過對代碼進行梳理磨隘,該函數(shù)的大致流程如下:
quic_rfc9000_packet_06.png
bool QuicPacketCreator::SerializePacket(QuicOwnedPacketBuffer encrypted_buffer,
size_t encrypted_buffer_len,
bool allow_padding) {
....
size_t length;
absl::optional<size_t> 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()函數(shù)中調用QuicFramer::BuildDataPacket()函數(shù)并以queued_frames_、上一步中的QuicPacketHeader番捂、分配好的buffer*作為參數(shù)進行傳遞
size_t QuicFramer::BuildDataPacket(const QuicPacketHeader& header,
const QuicFrames& frames, char* buffer,
size_t packet_length,
EncryptionLevel level) {
// 以buffer為參數(shù)構造writer,然后基于該buffer序列化寫入數(shù)據
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<uint8_t>(
FLAGS_LONG_HEADER | FLAGS_FIXED_BIT |
LongHeaderTypeToOnWireValue(header.long_packet_type, version_) |
PacketNumberLengthToOnWireValue(header.packet_number_length));//
} else {
type = static_cast<uint8_t>(
FLAGS_FIXED_BIT | (current_key_phase_bit_ ? FLAGS_KEY_PHASE_BIT : 0) |
PacketNumberLengthToOnWireValue(header.packet_number_length));
}
return writer->WriteUInt8(type);
}
- 以上函數(shù)為寫入頭部首字節(jié)信息到buffer
- 其中函數(shù)PacketNumberLengthToOnWireValue()的返回值為PacketNumber所占的字節(jié)數(shù)-1,也就是假設PacketNumber使用一個字節(jié)標識,那么就是0白嘁,對于長類型的報文坑鱼,對應的是首字節(jié)的bit[0~1]
- 其中函數(shù)LongHeaderTypeToOnWireValue()函數(shù)在RFCv1中全部返回0
bool QuicFramer::AppendIetfPacketHeader(const QuicPacketHeader& header,
QuicDataWriter* writer,
size_t* length_field_offset) {
QuicConnectionId server_connection_id =
GetServerConnectionIdAsSender(header, perspective_);
// 1) 寫入頭部首字節(jié)到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等膘流,對應協(xié)議中的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;
}
- 該函數(shù)的操作完全是按照協(xié)議文檔中定義順序來進行寫入的耕魄,假設以Initial包為例,則整理如下:
- 1)寫入頭的首1個字節(jié)彭谁,對應長類型吸奴,Initial類型,packetNumber用多少字節(jié)來描述(1個字節(jié)所以為0)
- 2)寫入Version字段表示Quic使用哪個協(xié)議版本當前為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则奥,該字段占兩個字節(jié),這里是先預留空間狭园,長度信息是在后面添加(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)占一個字節(jié)唱矛,然后再根據不同的Frame類型將實際數(shù)據進行寫入,其中AppendCryptoFrame()的實現(xiàn)如下:
bool QuicFramer::AppendCryptoFrame(const QuicCryptoFrame& frame,
QuicDataWriter* writer) {
// 1) 寫入該frame在包中的內存偏移
if (!writer->WriteVarInt62(static_cast<uint64_t>(frame.offset))) {
set_detailed_error("Writing data offset failed.");
return false;
}
// 2) 寫入該frame的長度信息
if (!writer->WriteVarInt62(static_cast<uint64_t>(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;
}
- 以上函數(shù)引入了QuicStreamFrameDataProducer模塊罚舱,事實上quic 業(yè)務層的數(shù)據在封包的過程中是沒有傳遞到QuicPacketCreator和QuicFramer模塊的,而是緩存在更上的業(yè)務QuicConnection模塊當中绎谦,而QuicSession模塊是由QuicStreamFrameDataProducer模塊派生出來的管闷,同時QuicFramer和QuicPacketCreator都屬于QuicSession的成員變量,并且在初始化QuicSession的時候會將QuicFramer指針傳遞到QuicPacketCreator當中
- 本節(jié)結合QuicCryptoFrame協(xié)議中的定義來分析其寫入過程
CRYPTO Frame {
Type (i) = 0x06,(8bit)
Offset (i),->(64bit)
Length (i),->(64bit)
Crypto Data (..),
}
- Figure 30: CRYPTO Frame Format
- Type:表示該類型frame的值(在AppendIetfFrameType函數(shù)中寫入)
- 1)Offset:表示的是該frame在該報文中內存偏移的啟始地址
- 2)Length:表示該frame的具體長度信息
- 3)Crypto Data:表示真實的Payload..
WriteIetfLongHeaderLength()
QUIC數(shù)據包和幀通常使用可變長度編碼來表示非負整數(shù)值包个。這種編碼確保較小的整數(shù)值需要較少的字節(jié)來編碼。QUIC變長整數(shù)編碼保留第一個字節(jié)的兩個最高位冤留,以字節(jié)為單位對整數(shù)編碼長度的以2為基數(shù)的對數(shù)進行編碼碧囊。整數(shù)值以網絡字節(jié)順序在剩余的位上編碼恃锉。
這意味著整數(shù)以1、2呕臂、4或8字節(jié)編碼破托,可以分別編碼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 |
- 為啥在上面的分析中有出現(xiàn)WriteVarInt62xx這種谜洽,原理就在這萝映,8字節(jié),其中高兩位為0x11
bool QuicFramer::WriteIetfLongHeaderLength(const QuicPacketHeader& header,
QuicDataWriter* writer,
size_t length_field_offset,
EncryptionLevel level) {
// 通過協(xié)議版本判斷是否支持這個
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;
}
- 參數(shù)length_field_offset就是在上面AppendIetfPacketHeader()函數(shù)中得出來的,記錄了Length字段在Buffer中的偏移
- 到這一步驟其實所有的信息(除這個length字段空了兩個字節(jié))其他所有信息已經全部被序列化
- 得到Packet Number and Payload 的長度实束,根據RFC9000,這兩個域的長度會被編碼為可變長度整數(shù)
- length_writer.WriteVarInt62WithForcedLength寫入內存
總結
- 通過本文學習主要可以了解quic 報文的相關協(xié)議奥秆,以及各類包的定義規(guī)則和內存分布
- 同時通過google quiche的源碼分析來了解如果封裝一個quic報文以及序列化一個報文
- 本文對后續(xù)對google quiche框架深入學習有十分重要的意義