QUICをゆっくり解説(3):QUICパケットの構造

2021年08月11日 水曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(3):QUICパケットの構造」のイメージ

前回の説明では、「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の長さを知っているのです。

山本 和彦

2021年08月11日 水曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事