LoginSignup
0
1

More than 1 year has passed since last update.

QUICのRFCを説明する(パケット/フレーム概要編)

Last updated at Posted at 2022-12-18

注意

現在記事を工事中です。不自然なつながり、唐突な話題転換等があるかもしれませんのでご了承下さい。

はじめに

今までにHTTP/1,HTTP/2を実装したことがある(というのもおこがましいような出来のものを作ったことがある)のだが、
今年6月(追記:この記事を書き始めたのは2022の末でしたがその後2023年まで持ち越して書いています)あたりについにHTTP/3がIETFで標準化されたと聞いて、実装してみたくなった。
ところが、HTTP/3はTCPではなくQUIC上で動くというので、まずQUICを実装せねばならない。
よし、実装するぞと早速RFC9000を見始めたのだった...

記事を読む前に

この記事(と後々の記事)は基本的にRFC9000,RFC9001,RFC9002をベースにしています。

QUICの実装はすでに世の中にたくさんあり、

などの実装や

実装方法を解説する記事などもごまんとあるので実装したければそれらを見たほうが早いと思います。

読む前に以下の点(言い訳)をご了承ください

  • この記事は筆者がRFCについてより理解を深める手助けとして書いているものです。
  • 筆者がQUICのRFCを読む前に読んだことがあったのはHTTP2とHPACKのRFCだけでした。
  • 筆者はRFCを読んで趣味で実装しているだけの素人ですので、不正確な記述があったりする可能性があります。(指摘があれば適宜訂正します)
  • そもそも文章を書くのが下手なので読みづらい可能性があります。
  • 特定のプログラミング言語による実装の詳細については触れません。

(以下文章は常体になります。)

最低限の知識

ラウンドトリップやハンドシェイクといった用語や、TLSについての基礎知識、TCPの仕組みになどついては説明しないのでわからない人は別の文献をあたってほしい。また、あらかじめ下記の記事などを読んでQUICの概要について理解しておくと良いだろう。

RFC9000

まずQUICバージョン1の基本となるRFC9000はストリームについての記述に始まり、フロー制御、コネクション、バージョンネゴシエーション、ハンドシェイク、(中略)、実際のバイナリフォーマットの順に(厳密ではないが)上位レイヤーの方から記述されている。

もちろん、実装する上ではストリームの状態遷移とかから実装しても何も問題はないが、RFC初見ではなんの説明もなしにフレームだのPROTOCOL_VIOLATIONだのACK-Elicitingだのという記述が出てきてよくわからないと思う
(筆者はわからなかった。一応see Section xxのような記述があったがそこを見てもsee Section (元の場所)とか書いてあってますます混乱した。筆者はバカだったのである)。

個人的にはまず最初はバイナリフォーマットの解析と生成の部分を実装してから上位レイヤーを実装していくと良いと思う。

パケットとフレーム

上に書いた通り筆者はバイナリフォーマットを先に説明したほうが理解しやすいと思うので、まずパケットとフレームについて書く

QUICの通信はパケットの交換によって行われる。
このパケットというのは情報の1単位という意味に過ぎない。
このパケット(以下QUICパケット)はさらにUDPデータグラムに包まれて送信され、そのUDPデータグラムはIPパケットに包まれて送信される。
UDPデータグラムには複数のQUICパケットを含めることができる。
また、IP(プロトコル)にはフラグメンテーションを行う機能があるが、
QUICではこれを禁止している。1

QUICはまた情報の単位としてフレームと言うものを用いる。
これもパケットと同じで情報の1単位という意味に過ぎない。
フレーム(以下QUICフレーム)はパケットのペイロードとして複数のフレームがまとめて送信される。

ここまでの記述を図で表すと以下のようになる

┌IP Packet──────────────────────────┐  ┌IP Packet──────────────────────────┐
│ ┌UDP Datagram───────────────────┐ |  │ ┌UDP Datagram───────────────────┐ |
│ │ ┌QUIC Packet─┐ ┌QUIC Packet─┐ │ |  │ │ ┌QUIC Packet────────────────┐ │ |
│ │ │┌QUIC Frame┐│ │┌QUIC Frame┐│ │ |  │ │ │┌QUIC Frame┐  ┌QUIC Frame─┐│ │ |
│ │ │└──────────┘│ │└──────────┘| │ |  | | |└──────────┘  └───────────┘│ │ |
│ │ │            │ │┌QUIC Frame┐| │ |  │ │ │┌QUIC Frame───────────────┐│ │ |
│ │ │            │ │└──────────┘| │ |  | | |└─────────────────────────┘│ │ |
│ │ └────────────┘ └────────────┘ | |  │ │ └───────────────────────────┘ | |
│ └───────────────────────────────┘ |  │ └───────────────────────────────┘ |
└───────────────────────────────────┘  └───────────────────────────────────┘

パケットの種類

QUICパケットにはいくつか種類があり
RFC9000 17.2. Long Header Packetsの表にShort Header Packets(1-RTT Packet)VersionNegotiation PacketStateless Reset(厳密にはパケットではない)を加えて以下の種類がある。

 +=======+======+=====================+================+
 | Kind  | Type | Name                | Section        |
 +=======+======+=====================+================+
 | Long  | 0x00 | Initial             | Section 17.2.2 |
 +-------+------+---------------------+----------------+
 | Long  | 0x01 | 0-RTT               | Section 17.2.3 |
 +-------+------+---------------------+----------------+
 | Long  | 0x02 | Handshake           | Section 17.2.4 |
 +-------+------+---------------------+----------------+
 | Long  | 0x03 | Retry               | Section 17.2.5 |
 +-------+------+---------------------+----------------+
 | Long  | N/A  | Version Negotiation | Section 17.2.1 |
 +-------+------+---------------------+----------------+
 | Short | N/A  | 1-RTT               | Section 17.3.1 |
 +-------+------+---------------------+----------------+
 | N/A   | N/A  | Stateless Reset     | Section 10.3   |
 +-------+------+---------------------+----------------+

これらのうちLong Packetは接続確立(以下ハンドシェイク)時に用いられ、Short Packetはハンドシェイク後に用いられる。

フレームの種類

また、フレームにもいくつか種類があり、これはRFC9000 12.4. Frames and Frame Typesに一覧が載っている。

+============+======================+===============+======+======+
| Type Value | Frame Type Name      | Definition    | Pkts | Spec |
+============+======================+===============+======+======+
| 0x00       | PADDING              | Section 19.1  | IH01 | NP   |
+------------+----------------------+---------------+------+------+
| 0x01       | PING                 | Section 19.2  | IH01 |      |
+------------+----------------------+---------------+------+------+
| 0x02-0x03  | ACK                  | Section 19.3  | IH_1 | NC   |
+------------+----------------------+---------------+------+------+
| 0x04       | RESET_STREAM         | Section 19.4  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x05       | STOP_SENDING         | Section 19.5  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x06       | CRYPTO               | Section 19.6  | IH_1 |      |
+------------+----------------------+---------------+------+------+
| 0x07       | NEW_TOKEN            | Section 19.7  | ___1 |      |
+------------+----------------------+---------------+------+------+
| 0x08-0x0f  | STREAM               | Section 19.8  | __01 | F    |
+------------+----------------------+---------------+------+------+
| 0x10       | MAX_DATA             | Section 19.9  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x11       | MAX_STREAM_DATA      | Section 19.10 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x12-0x13  | MAX_STREAMS          | Section 19.11 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x14       | DATA_BLOCKED         | Section 19.12 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x15       | STREAM_DATA_BLOCKED  | Section 19.13 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x16-0x17  | STREAMS_BLOCKED      | Section 19.14 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x18       | NEW_CONNECTION_ID    | Section 19.15 | __01 | P    |
+------------+----------------------+---------------+------+------+
| 0x19       | RETIRE_CONNECTION_ID | Section 19.16 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x1a       | PATH_CHALLENGE       | Section 19.17 | __01 | P    |
+------------+----------------------+---------------+------+------+
| 0x1b       | PATH_RESPONSE        | Section 19.18 | ___1 | P    |
+------------+----------------------+---------------+------+------+
| 0x1c-0x1d  | CONNECTION_CLOSE     | Section 19.19 | ih01 | N    |
+------------+----------------------+---------------+------+------+
| 0x1e       | HANDSHAKE_DONE       | Section 19.20 | ___1 |      |
+------------+----------------------+---------------+------+------+

また、さらに別のRFCなどで新たな種類のフレームを定義できる。
例えば、RFC9221ではDatagramフレームが定義されている。

可変長整数エンコーディング

ここからは実際の具体的なバイナリフォーマットについて説明していく。
まずは、QUICパケットやQUICフレーム、トランスポートパラメーター(後述)をパースするのに必要な可変長引数エンコーディングを説明する。

以下はRFC9000 16. Variable-Length Integer Encodingに載っている図だ。

+======+========+=============+=======================+
| 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 |
+------+--------+-------------+-----------------------+        

QUICの可変長整数の長さは最上位2ビットの値によって決まり、
ビッグエンディアンで格納される。
例えば

[0x81,0x3f,0x4b,0x20]

のようなバイト列があった場合、最上位2ビットは0x81=0b1000,0001なので10となり4バイト整数として最上位2ビット分を削って0x013f4b20=20925216と解釈される。
逆に

0x3214

という整数値は2byteでエンコードするとしたら、最上位2bitを01にセットすると0x7214=0b0111,0010,0001,0100となり

[0x72,0x14]

という風にエンコードされる。
基本的に実装は各数値を最小の長さになるようにエンコードするとは思うが、必ずしも最小でなくても良い。
ただし、これには例外があってQUICフレームのTypeフィールド(後述)だけは必ず最小の長さでエンコードする必要がある。2

フレームの基本構造

ここでフレームの基本的な構造について説明する。
個々のフレームの役割については別記事で書くが、どのフレームも以下の構造をもつ。

Frame {
    Frame Type (i),
    Type-Dependent Fields (..),
}

ここで

フィールド名 (i)

はフィールドが可変長整数であることを示す。

Frame Typeというのが、可変長整数のところで述べたQUICフレームのTypeフィールドに当たるところであり、フレームの種類の表のType Valueの数値がエンコードされる。
Type-Dependent FieldsFrame Typeによって決まるフィールドである。

この設計では、未知のFrame Typeのフレームを解析することはできないため、新しいフレームを使用したいときは別途トランスポートパラメーターなどを使用して相手がそのフレームに対応しているか確認する必要がある。

トランスポートパラメーターとTLS

ここで、トランスポートパラメーターとQUICとTLSの(バイナリフォーマットにおける)関係について簡単に説明する。
トランスポートパラメーターとはQUICのハンドシェイク時に交換され、相手の動作を制限(ex:送信可能なデータの上限)したり情報を伝達したり(ex:アイドルタイムアウト(接続の自動切断までの期間))するための手段である。

トランスポートパラメーターはRFC9000 18. Transport Parameter Encodingに書かれているようにエンコードされる。

Transport Parameter {
    Transport Parameter ID (i),
    Transport Parameter Length (i),
    Transport Parameter Value (..),
}

Transport Parameter IDはID、Transport Parameter Lengthはデータ長、Transport Parameter Valueが実際のデータである。
RFC9000 18.2. Transport Parameter Definitionsなどに示されるようにIDごとにデータの意味が決められていて、それに基づいてデータは解釈される。
また、この構造はフレームとは違ってIDが未知でも全体の長さがわかり、また新たなフレームなど未対応の要素を無視して接続を続行できるようにするため、未知のIDは無視する必要がある。3

トランスポートパラメーターとTLSとの関係

このトランスポートパラメーターはどのように運ばれるかというと、8.2. QUIC Transport Parameters Extensionで定義されたquic_transport_parametersというTLS拡張の一種として運ばれる。

ここでTLS拡張について軽く説明しておく。TLS拡張とはHandshakeメッセージ中で様々な拡張情報(鍵交換方式、署名アルゴリズムなど)を伝えるものである。
TLS1.2では必須ではく省略可能だった。
しかしTLS1.3ではTLS1.2でClientHelloの通常のフィールドの一部だったバージョン情報がsupported_version拡張にKey-Exchangeメッセージがkey_share拡張に変更されているため、いくつかの拡張は必須である。

quic_transport_parameters拡張はQUICで通信するときには必須の拡張であり、その中身はTransport Parameterを任意の数つなぎ合わせたものである。

また、QUICではquic_transport_parameters拡張の他にapplication_layer_protocol_negotiation (ALPN)という拡張も必須で追加しなければならない。4

TLSとQUICの関係

では、TLSのHandshakeメッセージはどのように運ばれるかというと、CRYPTOフレームを用いて運ばれる。
RFC9000 19.6. CRYPTO Framesの図が以下である。

CRYPTO Frame {
    Type (i) = 0x06,
    Offset (i),
    Length (i),
    Crypto Data (..),
}

Typeフレームの基本構造のFrame Typeにあたり、
Offsetはデータ全体に対するオフセット
LengthCrypto Dataの長さ
Crypto DataがTLSのHandshakeメッセージのデータである。

TLSの実装は一連の(分割されていない)Handshakeメッセージを生成し、QUICの実装はそれを適切な長さに分割して送信するという流れである。

また、TLSにはRecordレイヤーがというものがあるが、QUICではそれを使わず、Handshakeメッセージを直に使用する。

RFC9001 2.1. TLS Overviewの図とRFC9001 3. Protocol Overviewの図を比較してみる。
Figure 1: TLS Layers

             +-------------+------------+--------------+---------+
   Content   |             |            |  Application |         |
   Layer     |  Handshake  |   Alerts   |     Data     |   ...   |
             |             |            |              |         |
             +-------------+------------+--------------+---------+
   Record    |                                                   |
   Layer     |                      Records                      |
             |                                                   |
             +---------------------------------------------------+

Figure 3: QUIC Layers

   +--------------+--------------+ +-------------+
   |     TLS      |     TLS      | |    QUIC     |
   |  Handshake   |    Alerts    | | Applications|
   |              |              | |  (h3, etc.) |
   +--------------+--------------+-+-------------+
   |                                             |
   |                QUIC Transport               |
   |   (streams, reliability, congestion, etc.)  |
   |                                             |
   +---------------------------------------------+
   |                                             |
   |            QUIC Packet Protection           |
   |                                             |
   +---------------------------------------------+

TLS LayersのHandshakeのメッセージがQUIC LayersのTLS Handshake(CRYPTO Frame)として送信される。

Initialパケットの構造

ここで少し話を変えて、

まず、最初に使うのはInitial Packetだ。
RFC9000 17.2.2. Initial Packetから図を引用する。

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..),
}

ここで軽く見方を説明すると、

フィールド名 (ビット数)

と言う形式が基本で、

フィールド名 (i)

という表記が先程説明した、可変長整数という意味、

フィールド名 (0..160)

というのは0ビットから160ビットの間の長さ
(0..160)などは実際にはバイト単位でしか長さを変えられないので0バイトから20バイトの間の長さと捉えれば良い。

フィールド名 (..)

は任意長。

上記2つの形式には長さを示すフィールドが前についている場合が多い。例えばInitial Packetの場合だとToken LengthTokenの長さとなっている。

また、

フィールド名 (1) = 1

など = 1などとあるのはそのフィールドの値は1で固定という意味である。

さて、話を戻そう。
InitialPacketの構造は以下のようになっている。

Header Form (1) = 1

まず、これはロングヘッダパケットかショートヘッダパケットかを示すフラグである。
Header Form1のときはロングヘッダパケットであると決められてるので2、InitialPacketはロングヘッダパケットとわかる。これは他のパケットでも同様である。

Fixed Bit (1) = 1

これは固定ビットだ。とりあえず1に設定しておけば良い。4

Long Packet Type (2) = 0

これは他の種類のロングヘッダパケットと区別するためのビットだ。
ロングヘッダパケットは全部で4種類なので2bitあれば十分なのである。
RFC9000 17.2. Long Header Packetsから図を引用する
QUICバージョン1では以下のように対応している3

+======+===========+================+
| 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 |
+------+-----------+----------------+
           

他のパケットについてはおいおい説明していこう。

Reserved Bits (2)

これは予約ビットだ。このビットは将来のために予約されているがバージョン1では00で埋めておかなければならない。
ちなみにロングヘッダパケットではこのフィールドと次のPacket Number Lengthフィールドは「ヘッダー保護」の対象になっている。これは後ほど説明しよう。

Packet Number Length (2)

これはPacket Numberフィールドの長さである。
パケット番号というのは各パケットごとにつけられる番号で、これでパケットが到達したかを調べるのに使われる。(TCPのシーケンス番号に似た役割である)

ちなみにここまででQUICのヘッダはまだ1バイト分である。
QUICはTCPのようにヘッダにいろいろ詰め込むのではなく、パケットとフレームで役割を分担しているのである。5

Version (32)

これはQUICのバージョンを表す4バイトの整数である。バージョン1は0x00000001で表す。6

Destination Connection ID Length (8)
Destination Connection ID (0..160)

これは宛先コネクションIDの長さとIDそのものである。
コネクションIDというのはQUIC上で接続を識別するものである。
QUICはUDP上で動作するのだがこれによって同じIPアドレスやポートでもからの接続を多重化することができる。
また逆に、UDPのIPアドレスとポート以外にコネクションIDを設けることでIPアドレスとポートが変わってもコネクションIDを使って異なるIPアドレスとポートでも同じ接続として扱えるのである。
詳しいことは別の記事で説明する。

Source Connection ID Length (8)
Source Connection ID (0..160)

これは送信元コネクションIDの長さとIDそのものである。

Token Length (i)
Token (..)

これは0RTT用のトークンの長さとトークンの値である。
今回は使用しないので割愛する。

Length (i)

これは、Lengthフィールド以降のパケットの長さである。
InitialパケットとZeroRTTパケットとHandshakeパケットにはこのフィールドが存在し、これによって1つのUDPデータグラムに複数のQUICパケットを入れるということが可能になる。
なお、これを計算するときにはいくつか注意があるのがそれは次回の記事で説明する。

Packet Number (8..32)

これは、パケット番号である。先ほど説明したのでそちらを参照していただきたい。なお、このパケット番号の長さは1,2,3,4バイトのいずれかであり、Packet Number Lengthフィールドの値に1を足すことで求められる。なお、ちなみにこの部分もヘッダー保護の対象であり、暗号化される。

なぜPacket Number LengthPacket Numberが暗号化されるかというと、これらのフィールドを暗号化することでこれを復号しない限りパケット番号がわからないようにすることができるのである。
これはTCPシーケンス番号予測攻撃のように第三者がパケット番号を予測して接続を乗っ取るということができなくなるということにつながる。

Packet Payload (8..)

これは、パケットのペイロードである。
この中身にフレームを並べたものが入る。
Rfc9000 12.4. Frames and Frame Typesから図を引用する。

Packet Payload {
  Frame (8..) ...,
}

このようなイメージである。(8..)とあるのは最低1バイトはなければならないという意味である。ただし、実際には暗号化の都合によって20バイト以上なければならないのだがこれは次回述べる。

次回予告

さて、ここまでで

  • フレームとパケットの基本
  • フレームとパケットの種類一覧
  • 可変長整数
  • フレームの構造
  • トランスポートパラメーターについて
  • TLSとQUICの関係の概要
  • CRYPTOフレームの役割

については説明した。
次回はTLSとQUICの関係の詳細を説明する。

  1. https://tex2e.github.io/rfc-translater/html/rfc9000.html#14--Datagram-Size の記述 "UDP datagrams MUST NOT be fragmented at the IP layer."

  2. https://tex2e.github.io/rfc-translater/html/rfc9000.html#16--Variable-Length-Integer-Encoding の記述 "Values do not need to be encoded on the minimum number of bytes necessary, with the sole exception of the Frame Type field" 2

  3. https://tex2e.github.io/rfc-translater/html/rfc9000.html#7-4-2--New-Transport-Parameters の記述 "An endpoint MUST ignore transport parameters that it does not support." 2

  4. https://tex2e.github.io/rfc-translater/html/rfc9001.html#8-1--Protocol-Negotiation の記述 "Unless another mechanism is used for agreeing on an application protocol, endpoints MUST use ALPN for this purpose." 2

  5. RFC9000 17.2. Long Header Packets参照

  6. バージョン1ではとわざわざ書いたのは、バージョン2では違うマッピングになる予定であるからである。QUIC バージョン2で検索してみてほしい。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1