QUICをゆっくり解説(9):コネクションの終了

2021年09月22日 水曜日


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

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

「QUICをゆっくり解説(9):コネクションの終了」のイメージ

今回は、QUICのコネクションを終了させる手順について説明します。

正常な終了

QUICのコネクションを正常に終了させるには、CONNECTION_CLOSEフレーム(フレーム型の値は0x1c)を送信します。CONNECTION_CLOSEフレームの構造を以下に示します。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Error Code (i)                      ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Frame Type (i)                      ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Reason Phrase Length (i)                 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Reason Phrase (*)                    ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

各フィールドの意味は以下のとおりです。

  • エラーコード(Error Code)は、その名のとおりエラーコードの値を入れます。正常終了の場合は 0 を入れます
  • フレーム型(Frame Type)は、原因となったフレーム型の値を入れます。原因が不明の場合は、0を入れます
  • 理由フレーズの長さ(Reason Phrase Length)には、理由フレーズのバイト数を入れます。理由フレーズを省略する場合は、0を入れます
  • 理由フレーズ(Reason Phrase)は、終了の原因の説明です。UTF-8の文字列を入れます

送信側は、CONNECTION_CLOSEの送信の後にコネクションのほとんどの状態を消去します。受信側は、CONNECTION_CLOSEの受信後、ただちにパケットの送信をやめます。

TCPのコネクションでも、QUICのストリームでも、FINに対してACKが返ってくるので、相手が終了を了解したことが確認できます。しかしQUICのコネクションでは、CONNECTION_CLOSEを受け取るとACKさえも送信できなくなります。

受信側がCONNECTION_CLOSEを受け取ったことを、送信側はどうやって確認するのでしょうか?

「送信側が一定期間待つ」が答えです。CONNECTION_CLOSEを受け取ったならパケットを返してこなくなるはずなので、そのことを確認するのです。一定期間後にもパケットが送られてくるようなら、CONNECTION_CLOSEが欠落したと判断し、同じパケットを再送します。

まだ、ロス検知や再送について説明していませんが、一般的にQUICでは欠落したと判断されたパケットそのものは再送されません。欠落したパケットの中にある情報を格納した新しいパケットが生成され、新しいパケット番号が付与され、さらに暗号化されて送信されます。QUICで再送されるのは、情報であってパケットではありません。

しかしながら、CONNECTION_CLOSEフレームを含むパケットだけは例外です。CONNECTION_CLOSEの送信後は、暗号化に必要な情報も削除されています。そこで終了の手続きにおいてのみ、(暗号化された状態での)パケットの再送が許されています。

エラー終了

QUICのコネクションをエラー終了させる方法は2つあります。終了の原因がQUICに起因したものであれば、前述のCONNECTION_CLOSEフレームに適切なエラーコードを入れます。終了の手続きは、正常終了の場合と同じです。以下にエラーコードの一部を示します。

  • 0x01 – 内部エラー
  • 0x02 – コネクションの拒否
  • 0x03 – フロー制御のエラー

ハンドシェイク時にはTLSのアラートが発生することがあります。TLSのアラートもCONNECTION_CLOSEに収められて運ばれます。エラーコードは、アラートの値に 0x0100を足した値が用いられます。たとえば、TLSのハンドシェイク失敗(handshake_failure)は 40 (0x28)なので、QUICでのエラーコードは 0x0128 になります。

終了の原因がアプリケーションの場合は、以下のようなCONNECTION_CLOSEフレーム(フレーム型の値は0x1d)が利用されます。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Error Code (i)                      ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Reason Phrase Length (i)                 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Reason Phrase (*)                    ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

フレーム型のフィールドがないことを除いては、最初に述べたQUICに起因するCONNECTION_CLOSEフレームと同じです。HTTP/3で定義されているエラーコードの一部を以下に示します。

  • 0x100 – エラーなし (この場合は正常終了)
  • 0x101 – HTTP3 の一般的なプロトコルエラー
  • 0x102 – HTTP3 の内部エラー
  • 0x103 – HTTP3 ストリームの生成に関するエラー

タイムアウト

パケットを一定時間受信しない場合、タイムアウトとしてコネクションを終了できます。タイムアウトの時間は、ハンドシェイクの際に交渉により決定されます。

リセット

何らかの理由でサーバがリブートし、短時間でサービスを再開したとしましょう。クライアントはコネクションを継続しようとしますが、サーバはコネクションに関する情報を失ってしまいます。

これまでの終了方法だけでは、タイムアウトを待つしかありません。しかし、タイムアウトだと帯域やリソースが無駄に使用されてしまいます。この問題を解決するのがリセットです。

TCP のリセット

TCPで相手のコネクションをリセットする際は、RSTフラグの立ったTCPセグメントを送ります。現在のTCPではセキュリティが厳しくなっており、リセットの場合に指定するシーケンス番号は、相手の期待している値でなければなりません。

状態を失ったというのに、どうやって正しいシーケンス番号を得るのでしょうか?

通常のTCPでは、最初のSYNパケット以外は、ACK情報が付加されています。ですので、ACK情報から期待されているシーケンス番号を取り出せます(下図)。

QUIC のリセット

QUICでは、TCPよりも話が難しくなります。なぜなら、QUICパケットは暗号化されているからです。あるコネクションに対して、サーバは暗号化/復号用の鍵を失っています。この問題を解決するために、QUICではステートレス・リセット・トークンを使います。

ステートレス・リセット・トークンは、そのサーバだけが静的に生成でき、コネクションIDごとに異なるバイト列です。たとえば、ハッシュ関数に「サーバの秘密鍵+コネクションID」を入力することで、このようなトークンを生成できるでしょう。

QUICでは、最初のInitailパケットを除くと、相手が送ってくるパケットには、終点のIDとして自分の生成したコネクションIDが平文で格納されています。ですので、たとえコネクションに対する復号用の鍵を失っていたとしても、任意のパケットを受け取ると、ステートレス・リセット・トークンを生成できます。

サーバは、ハンドシェイクの際に、自分が生成したコネクションIDに対するステートレス・リセット・トークンを相手に伝えることができます。また、コネクションが確立した後は、両者とも NEW_CONNECTION_ID フレームを使って、新しいコネクションIDとそれに対するステートレス・リセット・トークンを相手に通知することもできます。

コネクションをリセットするときは、以下のようなパケットを生成し、相手に送信します。

 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|               Unpredictable Bits (38 ..)                ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                   Stateless Reset Token (128)                 +
|                                                               |
+                                                               +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

この書式の要点は2つです:

  • リセット用のパケットは、1-RTTパケットに擬態しています
  • ステートレス・リセット・トークンは、パケットの末尾16バイトに埋め込まれています

受信側は、このパケットの復号に失敗します。その際に、末尾の16バイトを調べ、相手から通知されたステートレス・リセット・トークンに一致した場合、コネクションを直ちに終了します(下図)。

所感

今回は、QUICのコネクションを終了させる手順について説明しました。個人的には、TCPの知識が邪魔をして、この手順を正しく理解するまでに、とても時間がかかってしまいました。相互接続性試験の項目にコネクションの終了があるのですが、クライアントがCONNECTION_CLOSEフレームを送っても、サーバはACKを返さないので、「どうやって確認するんだ!」と長い間悩んでいたのです。

山本 和彦

2021年09月22日 水曜日

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

Related
関連記事