💨

HTTP/3 分かるよその気持ち

#tech#network

2024-3-12

HTTP/3

HTTP/3というやつがあり,ある.これについて記事を書きたい気持ちになってきたので,書く.

そもそもHTTP/1.1に代わってHTTP/2が出現した背景としては,Web上で行われる通信量の増大に伴い,高速化が求められたからである.しかし依然としてHTTP/2には,例えば次のような課題がある:

  • ハンドシェイク時のラウンドトリップ回数が多い
  • クライアントのIPアドレスやポート番号変更が生じるとコネクションが切断される
  • TCPにおいてパケットロスが生じると,後続のパケットにもこのせいで生じる遅延が波及する(TCP Head of Line Blocking)

こういった課題を解決するべく,HTTP/3が考案され,一部で実際に使用されている.指標として,Why HTTP/3 is eating the worldなどを見ると,近年急速にHTTP/3を介した通信の比率は増えてきていることが分かる(2023年前半で3割ほど).

QUIC

QUICという,HTTP/3においてTCPの代わりに用いられるプロトコルがある.これはUDPベースのプロトコルであり,後述する様々な点において通信上の問題を解消しようとしている.

コネクション管理の方法

HTTP/2では,IPアドレスに基づいて通信が管理されていた.しかしこれでは通信途中でIPアドレスやポートが変更された場合[1]に接続が中断されてしまうため,この点に課題があった.これに対しQUICでは,Connection IDと呼ばれる,通信を管理する専用のIDを発行し,それに基づいて通信を行う.これは大まかにはSCID(Source Connection ID)とDCID(Destination Connection ID)との2つのペアと考えることができ,前者は送信元のID,後者は宛て先のIDを示す.パケットロスや輻輳制御,RTTの推定などもこのConnection IDを単位として行われる.

コネクションが確立される際には,クライアントがSCIDとDCIDを内包したパケット(Initialパケット)を送付する.この際,DCIDはクライアントからすると自明ではないため,ランダムな値が埋め込まれる.この値はInitialパケットを暗号化する際に用いられる鍵を計算するのに用いられる.この暗号化についてはRFC 9001のここを読むと良い.

Intialパケットを受け取ったサーバーは,そのSCIDを自身のDCIDとしてInitialパケットを生成し,返す.これによりコネクションが確立する.このようなIDベースでの通信を行うと,通信途中でIPアドレスやポートが変更されても通信を継続できる(connection migration)といったメリットがある.

しかしこれだけでは,Connection IDを偽造すれば簡単に通信を偽造できてしまいそう[2]である.そのため,QUICにおいては通信再開後の新しいIPアドレスを用いて,その上でセキュリティ的に安全であるかどうかを検証する専用のパケットを送受信することにより,通信の改竄に対処している.この検証(path validation)が完了するまでは,サーバーは新しいIPアドレスから送信されてくるパケットについて受信制限を設ける[3]

path validationにおいては,まずクライアントがnon-probing frame[4]を送信し,それを受け取ると,サーバーはPATH_CHALLENGEフレームを送信する.この中には8バイトのランダムな値が含まれている.クライアントはこのランダムな値をフレーム中から取り出し,それをPATH_RESPONSEフレームとして返す.これが問題なく帰ってくることが確認できれば,path validationは完了する.この間は二者間で共有されている暗号化鍵を用いた通信を行っているため,第三者が介入することはできない.

path validationの流れ
path validationの流れ

ヘッダーの圧縮法

HTTP/2では,HPACKという,ハフマン符号とインデックステーブル(静的テーブル+動的テーブル)を用いたヘッダーの圧縮手法が用いられていた[5]

静的テーブルには使用頻度の高いヘッダーフィールドが格納されていて,動的テーブルは通信によって生じる使用済みのヘッダーフィールドが格納される.HPACKにおいては静的テーブルが1~61番を,動的テーブルが62番以降を用いるという風にインデックスが共有されている.

一方HTTP/3では,HPACKをそのままヘッダー圧縮に用いると,ストリームの順番が入れ替わって届いた際に,動的テーブルによって圧縮された部分が先に届いてしまい,解釈できない場合が考えられる.

これに対処するため,QPACKというヘッダーの圧縮手法が考案されている.これはHPACKと比べて

  • encoderとdecoderと呼ばれる機構を用いて動的テーブルの管理を行う
  • 動的テーブルの管理には絶対インデックスと相対インデックスとポストベースインデックスを用いる
  • 受け手側の動的テーブルに追加されていないエントリーへの参照を行うようなリクエストは,そのようなエントリーが追加されるまでは処理されない

といった違いがある.

encoderはインデックステーブルとハフマン符号とを組み合わせたエントリーの圧縮を行い,これはencoderストリームとして送信される.一方decoderはencoderストリームにより送信されてきたエントリーを複号したり,インデックステーブルの管理を行う.

QUICにおける静的テーブルは,これまでと異なり,動的テーブルとインデックスを共有することが無くなった.すなわち両者のインデックスは独立なものとなった(ついでにzero-basedにもなった)ため,静的と動的どちらのテーブルを用いるかはencoderストリームが指定するようになっている.

また,動的テーブルについては絶対インデックスと相対インデックス,ポストインデックスの3つのインデックスを用いて管理を行うようになっている.絶対インデックスはauto-incrementなインデックスでzero-based.相対インデックスもzero-basedで,動的テーブルへの登録時には現在動的テーブルに追加されているエントリーの内,最後に追加されたもののインデックスを参照し,使用時にはそこからの相対位置を返す.ポストベースインデックスは,早退インデックスとは逆方向に相対的なエントリーの位置を示すインデックス.これらのインデックスを合わせて用いることにより,動的テーブル上のエントリーを,どのような順番で処理しても正しく参照できるようになる.

TCP HoL Blockingの解消

TCPを用いた通信では,TCP Head of Line Blocking(TCP HoL Blocking)と呼ばれる問題が生じうる.これはTCPがアプリケーションレイヤーにデータを渡す際,そのデータ順序が整っていることを保証するために生じる.

データ順序を保証して渡すために,TCPにおいてはパケットロスが生じた際に再送を行い,さらにこの間,後続のパケットを送信せずに待機させる.それ故,パケットロスにより生じる遅延を後続の全てのパケットが被ることになる.

TCP HoL Blocking
TCP HoL Blockingの発生例

実際にはTCPではスライディングウィンドウ[6]を用いて複数のパケットをまとめて送信しているわけだが,こうしていてもどこかのパケットでパケットロスが発生するとそのウィンドウから送り直しになってしまう.

これに対し,QUICはそもそもUDPベースのプロトコルなので,TCP HoL Blockingのような現象は発生しない.しかし単にUDPっぽく送るだけではデータの順序が壊れても把握できなそうである.この点についても対策が行われている.これを説明するには,QUICのパケットロスの検知と再送処理について説明して,そこで合わせて説明するのが良いと思うので,そうする.

また,QUICにおける再送処理についても従来と異なる点がある.そもそもTCPのパケットロス検知は,シーケンス番号(SEQ)やSACKを用いてパケットロスの発生を検知していた.しかしそもそもこれでは,そのパケットが再送されてきたものなのかそうでないのかの判断は付かない.

何度も述べている通り,QUICはUDPベースのプロトコルなので,そもそもSEQみたいな仕組みが必要ではある.しかし上述のような課題があるので,実際にはSACKよりも広い範囲を取ったACKを採用し,パケットの順序はPacket Numberとして保持し,データの順序はSTREAMフレームに存在するオフセット(Stream Offset)と長さ(Stream Length)として保持する.Packet Number02^{62}-1までの値を取り,送信毎にインクリメントされる.クライアントはサーバーからレスポンスを受け取ったら,そのPacket Numberに関連した情報をACKとして返す.こうするとサーバー側はタイムアウトを待たずにパケットロスしたパケットのPacket Numberを検出することができる.

サーバー側はパケットロスを検出したら,そのPacket Numberではなく,今までに発行されているPacket Numberの続きとして再送する.データの順序はStream OffsetStream Lengthによって決定されるのでこのようなPacket Numberの発行で問題無い.

Packet Numberを用いた再送
Packet Numberを用いた再送

ということで本題に戻ってくると,このような仕組みが存在するのでQUICにおいてはデータの順序が保証され,またTCP HoL Blockingのような現象も発生しない.

ハンドシェイクに要する時間を短くする

HTTP/2上でTLS1.2を用いてハンドシェイクを行うことを考えると,これには3回分のRTT[7]がかかる.

内訳をクライアント側からの視点で具体的に示しておくと,

  • 最初にTCPハンドシェイクとしてSYNを送ってSYN/ACKが返ってくるからACKを返す
  • 次にTLSハンドシェイクとしてclient helloを送ってserver helloとかが返ってくる
  • TLSハンドシェイクの続きでclient key changeみたいなことをしてchange cipher specとかが返ってくる

一方QUICにおいてはTLS1.3をベースとした暗号化方式[8]を採用している.まずこの点において,TLS1.2に存在した,client helloとかをやっているときに鍵交換をやるといったことをやっていない[9]ので,これによりRTTが1減る.さらに,QUICではTCPハンドシェイクとTLSハンドシェイクを統合しているので,結果的にRTTは1でハンドシェイクが完了することになる.

また,TLS1.3においてはセッションが再開される際にはRTTは0でセッションを再開することができる.これはコネクション再開時に,ハンドシェイク時に交換された0-RTT keysとSCIDを送信することによりなされる.

HTTP/3に関連する技術

SSH3

SSH3プロトコルは,現在IETFに提出されている仮称のプロトコル名で,HTTP/3上でSSHを実行する.これを行うと,主に次のような恩恵がある.

  • QUICの,例えばIPアドレスが途中で変更されてもコネクションを維持できるといったメリットを享受できる
  • SSH通信が秘匿される.即ち第三者から見ればただのHTTP通信が流れているように見える
  • HTTPの上に乗るのでロードバランサーによる制御が利く

SSH3プロトコルは,HTTP/3のリクエストレスポンス上にSSHを載せるということではなく,拡張CONNECTメソッドを用いてHTTP/3上のコネクションをSSH3用に利用する.

Happy Eyeballs Version 3

既に数日前に書いた記事において説明しているが,IPv4とIPv6のデュアルスタック環境においてどちらの通信を利用するべきかは自明ではないため,AAAAレコードとAレコードの名前解決を早く行えた方を採用するHappy Eyeballs Version 2という仕組みがある.

これに対し,Happy Eyeballs Version 3では考慮対象にDNSのHTTPレコードも含め,採用する通信を決定する.この際,名前解決に成功した宛て先アドレスをソートする際に,Happy Eyeballs Version 3においてはECHやQUICのサポート情報を優先してソートするようになっている.これによりQUICによる通信が優先される.

脚注
  1. 例えば通信主体がキャリアとWi-Fi間で変わったり,NATのリバインディングによってポート番号が変わったりなどのケースが考えられる ↩︎

  2. アンプ攻撃とかが通りうる ↩︎

  3. 具体的には,サーバー側が送信できるパケットの大きさは,クライアントから受け取ったパケットの3倍の大きさまでに制限されるし,Initialパケットを含むUDPのペイロードは1200バイト以上である必要がある ↩︎

  4. PATH_CHALLENGEPATH_RESPONSENEW_CONNECTION_IDPADDINGの各フレームは,probing frameと呼ばれる.逆にnon-probing frameはこれら以外. ↩︎

  5. 実際には,HTTP/2の前身にあたるSPDYでは単純にハフマン符号とLZ77を用いたdeflate圧縮が用いられていたが,CRIME攻撃という,brute-force likeな攻撃に対して脆弱であったためにこれを解消したHPACKが用いられるようになった ↩︎

  6. ハンドシェイク時に予め決定しているサイズまでのパケット数をまとめて送信する ↩︎

  7. Round Trip Time.処理要求を発射してから応答が返ってくるまで ↩︎

  8. QUIC用に拡張されたTLSで,ALPN(Application-Layer Protocol Negotiation)を用いてアプリケーションレイヤーで用いるプロトコルを選択する際に規定が追加されたりしている ↩︎

  9. じゃあどうやって鍵交換しとんねんという話だが,Initialパケット内にCRYPTOフレームを設けて,ここにConnection IDを入れることにより,SCIDを交換する序にTLSの鍵交換が行われる. ↩︎