QUICをゆっくり解説(3):QUICパケットの構造
2021年08月11日 水曜日
CONTENTS
前回の説明では、「Initial パケット」や「Version Negotiation パケット」といった用語を未定義で使いました。今回は、こういった「パケット」や「フレーム」が、どのような構造を持っているかについて説明します。
古典的なパケット
IP、UDP、およびTCPでデータをやり取りする基本単位は、すべて「ヘッダ+ペイロード」という構造を持っています。このヘッダ+ペイロードという単位は、それぞれ以下のように呼ぶのが慣習です。
- IP – パケット
- UDP – データグラム
- TCP – セグメント
すべてパケットと呼んでも間違いではありません。UDPの場合、IPペイロードが「UDPデータグラム(UDPヘッダ+UDPペイロード)」になります。同様にTCPの場合、IPペイロードがTCPセグメント(TCPヘッダ+TCPペイロード)になります(下図)。
以下に、RFC793より抜粋したTCPセグメントの構造を示します。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
dataと書かれているのがTCPペイロードで、それより前はTCPヘッダです。TCPヘッダには、たくさんのフィールドがあることが分かります。
IPヘッダ、UDPヘッダ、およびTCPヘッダに共通して言えることは、フィールド長が固定長であり、しかも32ビットの境界を守って設計されていることです。
こういった書式は、これらのプロトコルの実装によく使われているC言語と親和性が高いと言えます。すなわち、ヘッダの構造を構造体で表現し、これらのパケットをアラインメントを守ってメモリに配置すれば、キャストするだけでヘッダがパースできます。
QUICパケット
QUICも、ヘッダ+ペイロードの構造を持ち、その構造はパケットと呼ばれます。すなわち、UDPペイロードがQUICパケット(QUICヘッダ+QUICペイロード)になります。
TCPヘッダとの比較のために、QUICのショートヘッダパケットの構造を眺めてみましょう。RFC9000では、構造の書式の表現に、草稿28から採用された表記が用いられています。
Short Header 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 (..),
}
私のようなロートルには、この表現が直観的ではないと感じます。そこで、草稿27からパケットの構造を抜粋します(草稿27以降、パケットの構造は変更されていません)。
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 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 1バイト目はフラグです。
- その後に、終点コネクションID(Destination Connection ID)が続きます。長さは0から20バイトです。
- その次は、1バイトから4バイトのパケット番号(Packet Number)です。
- 最後は、暗号化されたペイロード(Protected Payload)です。
このように、長さが可変のフィールド(右端が...
)がありますし、4バイト境界も守られていません。このような構造は、先頭から1バイトずつ読みながらパースする必要があります。
説明が遅くなりましたが、このショートヘッダパケットは、TLSでいうハンドシェイクが成功し、QUICのコネクションが確立した後に用いられます。1-RTT用のパケットと言ってもよいでしょう。
ショートヘッダパケットに対して、後述のロングヘッダパケットもあります。これらは先頭のビットで区別します。先頭ビットが0ならショートヘッダパケット、そうでないならロングヘッダパケットです。
QUICパケットを格納したUDPデータグラムは、IPのレベルで分割されてはなりません。すなわち、QUICパケットは複数のIPパケットにまたがることはありません。
QUICのショートヘッダパケットとTCPヘッダを見比べてみてください。ショートヘッダパケットには、以下のような情報が欠落しています。
- 確認応答(ACK)番号
- SYNフラグ、FINフラグ、およびACKフラグ
- ウインドウ (フロー制御情報)
これらは、どこに行ってしまったのでしょうか?
QUICフレーム
(後述のVersion Negotiation パケットとRetryパケットを除いた)QUICパケットのペイロードには、1つ以上のフレームが入ります。フレームとは、情報を格納する単位です。
フレームは、フレーム型(Frame Type)を示す値の後に、そのフレーム独自のフィールド(Type-Dependent Fields)が続きます。
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 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
詳細は述べませんが、フレームの例を紹介しましょう。確認応答の情報は、ACKフレーム(フレーム型の値は 0x02 または 0x03)に入ります。
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] ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
いわゆるアプリケーションのデータは、STREAMフレーム(フレーム型の値は 0x08 から 0x0f)に格納されます。
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 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
TCPのFINにあたる情報は、なんとフレーム型を示す値に符号化されます。フレーム型が1バイトの表現を使っている場合、フラグを図示するとこうなります。
+-+-+-+-+-+-+-+-+
| | | | | |O|L|F|
|0|0|0|0|1|F|E|I|
| | | | | |F|N|N|
+-+-+-+-+-+-+-+-+
オフセット(Offset)や長さ(Length)のフィールドが存在するかも、フラグで示されます。フレームは、QUICペイロードの大きさを超えてはなりません。大きなアプリケーションデータを送信する場合は、適当な大きさに分割し、それぞれの断片のオフセットや長さを指定することになります。
フロー制御の情報は、MAX_DATAフレーム(など)に入ります。
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) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ロングヘッダパケット
ロングヘッダパケットは、前述のように先頭ビットが1のパケットで、その名が示すように長いヘッダを持っています。以下に、ロングヘッダパケットの共通部分を抜粋します。
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) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ロングヘッダパケットは、5種類あります。
- Version Negotiation パケット – クライアントが指定したQUICのバージョンが受け入れられないときに、サーバがバージョンの候補を返すためのパケット。フレームは格納されない
- Initial パケット – ハンドシェイクにおいて、Initial段階の情報を運ぶためのパケット
- 0-RTT パケット – 0-RTT データを運ぶためのパケット
- Handshake パケット – ハンドシェイクにおいて、Handshake段階の情報を運ぶためのパケット
- Retry パケット – クライアントのIPアドレスを検証するために、サーバがクライアントにInitailパケットをもう一度送るように指示するためのパケット。フレームは格納されない
例として、Initial パケットの構造を示します。
+-+-+-+-+-+-+-+-+
|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 (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
クイズ
ショートヘッダパケットをもう一度眺めてください。可変長のフィールドとして、終点コネクションIDとパケット番号があります。パケット番号の長さは、PPフラグから得られます。一方で、終点コネクションIDの長さに関する情報はどこにもありません。
ショートヘッダパケットは、どうやってパースするのでしょうか?
通信相手が指定する終点コネクションIDは、ハンドシェイクの際に自分が生成したものです。ですので、ショートヘッダパケットを受け取ったパーサは、前提として終点コネクションIDの長さを知っているのです。