【テクニカル・上級編】CONTINUATIONフレームによるヘッダーブロックの分割伝送 – HTTPプロトコル・通信規格実践ガイド

CONTINUATIONフレームの深層:HTTP/2ヘッダー肥大化がもたらすパケットの暗部と防衛戦略

ネットワークの現場において、HTTP/2は長年「HTTP/1.xの呪縛(Head-of-Line Blocking)を断ち切った救世主」として君臨してきた。単一のTCPコネクション上で多重化(Multiplexing)を実現し、バイナリフレームによる効率的なパース、そしてHPACKによるヘッダー圧縮。これらはWebの高速化に多大な貢献を果たした。

しかし、プロトコルの仕様書(RFC 7540)の奥底に目を向けると、美しく最適化された世界に影を落とす「例外的なメカニズム」が潜んでいる。それが今回取り上げる `CONTINUATION`フレーム(Type: 0x9) だ。

マルチプレクシングの恩恵を裏で支えるストリーム管理、HPACKのコンテキスト維持、そして一歩間違えればサーモスタットの暴走をも引き起こす脆弱性のリスク。パケットアナライザの波形とLinuxカーネルの挙動を見つめながら、この「ヘッダー分割伝送」の正体に迫る。

—

1. なぜ `CONTINUATION` が必要なのか? パケットレベルの必然性

HTTP/2の基本単位は「フレーム」である。すべてのフレームは一律9オクテットのヘッダーから始まる。

+———————————————–+
| Length (24) |
+—————+—————+—————+
| Type (8) | Flags (8) |
+-+————-+—————+——————————-+
|R| Stream Identifier (31) |
+=+=============================================================+
| Payload Data (Variable) … |
+—————————————————————+

このフレーム構造において、リクエストやレスポンスのメタデータ(URL、パス、Cookie、User-Agentなど)を運ぶのが `HEADERS` フレーム(Type: 0x1)だ。

ここで最初の壁にぶつかる。「1つの `HEADERS` フレームに収まりきらない巨大なヘッダーブロックはどう扱うべきか?」 という問題だ。

Cookieの肥大化、巨大なSAML/OAuthトークン、あるいは複雑なカスタムヘッダーの乱立により、実世界のHTTPリクエストヘッダーは簡単に数KB〜数十KBに達する。一方、HTTP/2のエンドポイント間で合意された最大フレームサイズ(SETTINGS_MAX_FRAME_SIZE)のデフォルトは、RFC上最低限保証されているサイズでわずか `16,384バイト(16KB)` である。

分割伝送のシーケンス

この制限を突破するために設計されたのが `CONTINUATION` フレームである。

1. `HEADERS` フレームの送信:
先頭のヘッダー群を `HEADERS` フレームに載せて送出する。この時点ではまだヘッダーブロックが完結していないため、フラグに `END_HEADERS (0x4)` はセットされない。
2. `CONTINUATION` フレームの連鎖:
収まりきらなかった残りのヘッダーブロックを、1つ以上の `CONTINUATION` フレームに分割して順次送信する。
3. 終端の合図:
最後の `CONTINUATION` フレームに `END_HEADERS` フラグを立てることで、受信側は「ヘッダーブロックの受信が完了した」と判断し、HPACKデコーダへ処理を引き渡す。

[Client] [Server/Proxy]
|— HEADERS (Stream 1, No END_HEADERS) ————–>|
|— CONTINUATION (Stream 1, No END_HEADERS) ———->|
|— CONTINUATION (Stream 1, END_HEADERS) ————->|
| |
|— (フレームの順序入替・インターリーブは厳禁) ——–|

ここで極めて重要な制約がある。同一ストリームにおける `HEADERS` とそれに続く `CONTINUATION` フレームの間には、他のストリームのフレームを割り込ませてはならない(Interleavingの禁止)。もし別のストリームのフレームが間に割り込んだ場合、受信側はコネクションエラー(`PROTOCOL_ERROR`)として即座にセッションを切断しなければならない。

—

2. HPACKと `CONTINUATION` の密接な関係

ヘッダー圧縮アルゴリズムであるHPACKは、「動的テーブル(Dynamic Table)」を送信側と受信側で完全に同期させながら動作する。

HPACKのデコードは、送られてきたバイナリ列を先頭から順次ストリーム処理していく。もし `HEADERS` と `CONTINUATION` の間に別のフレームが混入したり、分割されたパケットの順序が狂ったりすると、動的テーブルのインデキシングが完全に破壊される。

+—————————————————————–+
| HPACK 圧縮ストリーム |
+————————-+—————————————+
| HEADERS (前半部分) | CONTINUATION (後半部分) |
| [Dynamic Table 更新含む] | [テーブル参照・継続デコード] |
+————————-+—————————————+

インフラエンジニアとして注意すべきは、「ヘッダーが分割されることで、TLSレコードのパディングやTCPセグメントの境界が複雑化する」点である。TLS層(TLS 1.3など)において、小さな `CONTINUATION` フレームが複数生成されると、暗号化オーバーヘッド(レコードヘッダーや認証タグ)が増大し、帯域効率が微小ながら悪化する。

アプリケーション層での無駄なヘッダー肥大化は、単にメモリを食うだけでなく、こうしたプロトコル層のパケット効率の低下(Small Packet Problem)を引き起こす温床となる。

—

3. 悪夢の脆弱性:HTTP/2 CONTINUATION Flood(CVE-2024-27919など)

この `CONTINUATION` フレームの仕様は、セキュリティの文脈において過去に幾度も重大なインシデントを引き起こしている。最も記憶に新しいのが、2024年春に発覚した “HTTP/2 CONTINUATION Flood”(CVE-2024-27919等) である。

脆弱性のメカニズム

攻撃者は次のような手順でターゲットのWebサーバーやリバースプロキシを麻痺させる。

1. 攻撃者は悪意あるHTTP/2コネクションを確立する。
2. 巨大な `HEADERS` フレームを送信するが、`END_HEADERS` フラグは絶対に立てない。
3. その後、果てしなく続く無限の `CONTINUATION` フレームを、`END_HEADERS` を付けずに送り続ける。
4. 【致命的な実装ミス】 多くの脆弱なHTTP/2実装(プロキシや言語の組込サーバーなど)は、`END_HEADERS` が届くまでの間、受信したヘッダーフラグメントをメモリ上(ヒープ領域)に蓄積し続けようとした。
5. 結果として、数個の不正なコネクションだけでサーバーのメモリが完全に枯渇し、OOM Killer(Out of Memory)によってサービスが完全停止(Denial of Service)に至る。

堅牢なインフラストラクチャにおける防衛策

生粋のインフラアーキテクトとして、この種の攻撃に対してどのようにシステムを防衛すべきか。単に「パッチを当てる」だけでは不十分であり、プロトコルスタックレベルでの防御的チューニングが必要不可欠である。

A. リバースプロキシ・Nginx/Envoyの適切な設定

NginxやEnvoyなどのエッジプロキシを使用する場合、HTTP/2のヘッダーバッファサイズと、不正なリクエストに対するタイムアウト・制限を厳格に設ける。

NginxにおけるHTTP/2関連のバッファ・制限パラメータ例
http {
# 1つのリクエストヘッダー全体の最大許容サイズ(デフォルトは通常1mなどだが、厳しめに絞る)
large_client_header_buffers 4 8k;

# HTTP/2接続におけるバッファ管理
http2_max_field_size 4k; 個別のヘッダーフィールドの最大長
http2_max_header_size 16k; ヘッダー全体の最大長
}

B. Linuxカーネルおよびネットワークスタックの監視

TCPウィンドウサイズやソケットバッファの枯渇を防ぐため、`sysctl.conf` においてリソースのライフサイクルを適切に管理する。

/etc/sysctl.d/99-http2-hardening.conf
TIME_WAITソケットの迅速な回収や、SYNフラッド対策と合わせたソケットバッファの最適化
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 15

メモリプレッシャー時の挙動最適化(OOM耐性)
vm.swappiness = 10

—

4. パフォーマンスの極限へ:RTT削減とTCP/TLSチューニングの実践

`CONTINUATION` フレームが多発する環境(=ヘッダーが大きい環境)は、パフォーマンス的にもアンチパターンである。これを最適化するためのアーキテクチャ設計指針を整理する。

1. ヘッダーダイエット(Header Dieting)の断行

そもそも `CONTINUATION` が発生する原因は「ヘッダーがデカすぎる」ことにある。

  • Cookieの断捨離: セッションIDやJWTを無駄に肥大化させない。必要最小限のクレームのみを保持し、セッション本体はRedisなどのKVSに逃がす。
  • 不要なカスタムヘッダーの削除: レガシーなミドルウェアが自動付与する冗長なヘッダーをリバースプロキシ層(Nginxの `proxy_hide_header` など)で全削除する。

2. TCPバッファとウィンドウチューニング

HTTP/2の多重化は、1つのTCPコネクション上で多数のストリームをさばくため、TCPの輻輳制御アルゴリズム(BBRなど)とウィンドウサイズのチューニングがパフォーマンスを左右する。

BBR輻輳制御アルゴリズムの有効化(Linux Kernel 4.9以降)
echo “net.core.default_qdisc=fq” >> /etc/sysctl.conf
echo “net.ipv4.tcp_congestion_control=bbr” >> /etc/sysctl.conf
sysctl -p

TCPウィンドウの自動チューニング範囲の拡大(高帯域・高遅延ネットワーク向け)
echo “net.ipv4.tcp_rmem = 4096 87380 16777216” >> /etc/sysctl.conf
echo “net.ipv4.tcp_wmem = 4096 65536 16777216” >> /etc/sysctl.conf
sysctl -p

これにより、巨大なヘッダーブロックが `HEADERS` と `CONTINUATION` に分割されて送信された場合でも、TCPセグメントロスに対する再送遅延を最小限に抑え、パイプラインをスムーズに流すことが可能になる。

—

結びにかえて

`CONTINUATION` フレームは、HTTP/2という巨大なエコシステムの中で、可変長かつ肥大化しがちなメタデータを安全に運ぶために生み出された「必要悪」とも言える仕様である。

その存在を意識することは普段のWebアプリケーション開発ではないかもしれない。しかし、インフラストラクチャの深層、すなわちパケットの断片化、HPACKのコンテキスト、そしてセキュリティの境界線を守るアーキテクトにとって、この小さなフレームの挙動を完全に理解しているか否かは、プロフェッショナルとしての命運を分ける。

「動けばいい」の向こう側へ。プロトコルの隅々にまで目を配り、真に堅牢で爆速なネットワークインフラを構築し続けよう。

コメント

タイトルとURLをコピーしました