HTTP/2の裏側を覗く:CONTINUATIONフレームが担う「ヘッダー分割」の泥臭い現実と実務対策
こんにちは。ネットワークインフラの現場を渡り歩いてきたシニアエンジニアの私です。
Web APIの設計や、大規模なリバースプロキシのチューニングをしていると、普段私たちが何気なく使っているHTTP/2がいかに洗練されているかに感嘆させられます。TCPの頭痛の種だった「ヘッド・オブ・ライン・ブロック(HoLB)」を綺麗に打ち破ったマルチプレクシング、そして1バイトでも帯域を節約しようとするHPACKによるヘッダー圧縮。どれをとっても美しいプロトコル設計です。
しかし、現場でパケットキャプチャを広げ、Wiresharkのタイムラインを睨みつけていると、こうした美しい理論の隙間にある「泥臭い現実」に直面します。
今回は、その泥臭い現実の象徴とも言える「CONTINUATIONフレーム」にスポットを当てます。「ヘッダーが大きすぎたら分割するだけの地味なフレームでしょ?」と侮るなかれ。この小さなフレームの挙動を誤ると、時にセキュリティリスクを招き、時にプロキシサーバーとの間で不可解な接続断を引き起こす原因になります。
実務でWeb APIやインフラに携わるあなたへ、RFCの仕様から通信フロー、そして現場で役立つデバッグTipsまで、たっぷりと解説していきましょう。
—
1. なぜCONTINUATIONフレームが必要なのか?(RFCの背景)
HTTP/2の基本単位は「フレーム」です。すべてのフレームには共通の9バイトのヘッダーがあり、その中には「フレームのペイロード長(長さを示す24ビットのフィールド)」が含まれています。
さて、ここで一つの疑問が浮かびます。
「HTTPリクエスト/レスポンスのヘッダー(CookieやUser-Agent、巨大な認可トークン等)が、1つのフレームの最大サイズを超えたらどうするのか?」
HTTP/2仕様(RFC 7540)では、デフォルトの最大フレームサイズ(SETTINGS_MAX_FRAME_SIZE)は $2^{14} – 1$ バイト(約16KB)に設定されています。もちろん、この上限はネゴシエーションによって最大 $2^{24} – 1$ バイト(約16MB)まで引き上げることが可能です。
しかし、どれだけ大きな上限を設定しても、世の中には「巨大なCookieを抱えたリクエスト」や「何十行ものカスタムヘッダー、JWT、さらにトレーラーヘッダー」を送ってくるクライアントが存在します。
ここで登場するのが、`HEADERS`フレームと`CONTINUATION`フレームのコンビネーションです。
ヘッダーブロック分割の基本ルール
1. 最初の断片: まず、`HEADERS`(または`PUSH_PROMISE`)フレームを用いてヘッダーブロックの送信を開始します。この時、もし全データがこの1発に入り切らない場合、フレームヘッダーのフラグにある `END_HEADERS` フラグは「OFF(0)」 のままにしておきます。
2. 後続の断片: 入り切らなかった残りのヘッダーデータは、`CONTINUATION`フレーム(Type: 0x9) に詰め替えて連続して送信します。
3. 終端: 最後の断片を収めた`CONTINUATION`フレームには、`END_HEADERS` フラグを「ON(1)」 にセットし、受信側に「これで今回のヘッダーブロックはすべて揃った」と伝えます。
言葉で説明するよりも、実際の通信フローを見た方が早いですね。次でシーケンスを確認しましょう。
—
2. 通信フロー(シーケンス)で見るCONTINUATIONの挙動
HTTP/2のストリーム上で、巨大なリクエストヘッダーがどのように流れていくのか、シーケンスを図解します。
[Client] [Server / Proxy]
| |
|— HEADERS (Stream ID: 1, END_HEADERS=0) ——————>|
| (ヘッダーの先頭部分 / 16KBの枠に収まった分) |
| |
|— CONTINUATION (Stream ID: 1, END_HEADERS=0) ————->|
| (ヘッダーの中間部分) |
| |
|— CONTINUATION (Stream ID: 1, END_HEADERS=1) ————->|
| (ヘッダーの末尾部分 / END_HEADERSがON) |
| |
| ※ ここで初めてサーバー側はHPACKデコードと処理を開始 |
| |
|<-- DATA / HEADERS (Response) -------------------------------|
| |
ここで非常に重要なポイントがあります。
受信側(サーバーやリバースプロキシ)は、`END_HEADERS`フラグがONになるまでの間、受け取った`HEADERS`や`CONTINUATION`の断片を内部バッファに溜め込み続けます。
つまり、CONTINUATIONフレームが延々と送られてくる間、受信側はそのストリームのヘッダー処理のためにメモリを占有し続けることになります。これが、のちに解説するセキュリティ上のリスク(HTTP/2 Continuation Flood)の温床となります。
—
3. 各種パラメーターとフレーム構造の解剖
ここで、CONTINUATIONフレームのバイナリ構造と、関連するHTTP/2設定パラメーターを確認しておきましょう。
フレームヘッダーの構造
HTTP/2のすべてのフレームは、以下の9バイトから始まります。
+—————————————————————+
| Length (24) |
+———————————————–+—————+
| Type (8) | Flags (8) |
+-+———————————————–+————-|
|R| Stream Identifier (31) |
+-+—————————————————————+
| Payload (Lengthに続く可変長) … |
+—————————————————————+
- Type: `CONTINUATION` の場合、値は `0x9` (10進数で9)です。
- Flags:
- `END_HEADERS` (`0x4`): このフレームがヘッダーブロックの最後であることを示します。
- Stream Identifier: どのストリームに属するヘッダーかを示す31ビットの整数。注意点として、`CONTINUATION`フレームのStream Identifierは、最初に送信された`HEADERS`フレームのStream Identifierと完全に一致していなければなりません。
関連するSETTINGSパラメーター
HTTP/2のコネクション確立時に行われる初期設定(SETTINGS)において、ヘッダーサイズやフレームサイズに関わる重要な項目があります。
- `SETTINGS_MAX_FRAME_SIZE` (ID: 0x5):
- 受信側が許容する最大フレームペイロードサイズ(デフォルト: 16,384バイト、最大: 16,777,215バイト)。
- クライアントはこの値を超えるサイズの`HEADERS`フレームを送ることはできません。そのため、これを超えるヘッダーを送る場合は必然的に`CONTINUATION`による分割が必要になります。
—
4. 実務で遭遇するトラブルと「HTTP/2 Continuation Flood」の脅威
プロトコルの仕様としては美しくまとまっているCONTINUATIONフレームですが、実運用やセキュリティの文脈では、過去に深刻な脆弱性(通称 HTTP/2 Continuation Flood、CVE-2024-27919など)の原因となりました。
何が問題だったのか?
悪意ある攻撃者が、`END_HEADERS`フラグを一切立てずに、無限に微小な`CONTINUATION`フレームを送り続ける攻撃手法です。
前述の通り、サーバーは`END_HEADERS`が来るまでヘッダーをバッファリングし続けます。サーバー側がこれに対するメモリ上限(リミット)を適切に設けていなかったり、非効率なメモリ確保を行っていたりすると、あっという間にサーバーのメモリが枯渇し、CPU使用率が100%に張り付いてサービス全体がダウン(DoS状態)してしまいます。
インフラエンジニアが取るべき対策
1. ミドルウェアの速やかなアップデート:
- Nginx, Apache, Envoy, Node.js, Go言語のnet/httpなど、主要なHTTP/2実装はすべてこの脆弱性に対するパッチ(継続的なヘッダーバッファの厳格な制限や、不正なストリームの即座のRST_STREAMなど)をリリースしています。インフラのベースOSやWebサーバーは常に最新の状態を維持してください。
2. WAF(Web Application Firewall)やリバースプロキシでの検知:
- 不自然に長いヘッダーブロックや、`END_HEADERS`が来ないままダラダラと続くCONTINUATIONフレームのシーケンスを検知・遮断できるルールを適用します。
—
5. 実装・デバッグTips(curl / Python / トラブルシューティング)
実務において、APIサーバーを開発していて「あれ、変なヘッダーのせいで通信がおかしいぞ?」となったとき、どのように確認すればよいでしょうか。
A. curlでHTTP/2の挙動を詳細に追う
curlを使って、HTTP/2でのリクエスト送信時の詳細な挙動(トレース)を確認するには、`-v`(verbose)オプションに加え、HTTP/2の通信内容をダンプするオプションを活用します。
HTTP/2を強制しつつ、詳細なヘッダーのやり取りをstderrに出力する
curl -v –http2 “https://api.example.com/v1/resource” \
-H “Authorization: Bearer <非常に長いトークン...>” \
-H “X-Custom-Debug-Header: enabled”
より低レイヤーでパケットを解析したい場合は、`tcpdump`でキャプチャしてWiresharkで開くのが一番確実です。Wiresharkのフィルタバーに `http2` と入力すれば、`HEADERS`フレームのあとに続く`CONTINUATION`フレームの連鎖(Stream IDが一致していることなど)が手に取るようにわかります。
B. Python (httpx) によるHTTP/2リクエストの挙動確認
Pythonで近代的なHTTP/2クライアントとして広く使われている `httpx` を用いて、意図的に大きなヘッダーを含むリクエストを送信するスクリプトの例です。
import httpx
巨大なカスタムヘッダーを生成(CONTINUATIONのテスト用)
※多くのサーバーやライブラリは、自動的にフレーム分割や適切なサイズ調整を行います
huge_cookie_value = “session_id=” + (“a” 20000)
headers = {
“Cookie”: huge_cookie_value,
“User-Agent”: “NetworkArchitect-DebugBot/1.0”
}
HTTP/2を有効にしてリクエストを送信
httpxは内部でHTTP/2のフレーム構築(HEADERSと必要に応じたCONTINUATIONの生成)を自動処理します
try:
with httpx.Client(http2=True) as client:
response = client.get(“https://httpbin.org/headers”, headers=headers)
print(f”ステータスコード: {response.status_code}”)
print(f”使用されたプロトコル: {response.http_version}”) # “HTTP/2” と出力されるはずです
print(“レスポンスボディ:”)
print(response.json())
except httpx.HTTPError as exc:
print(f”通信エラーが発生しました: {exc}”)
# 実務では、サーバー側のヘッダーサイズ制限(例: Nginxの large_client_header_buffers)
# に引っかかり、431 Request Header Fields Too Large が返ってくるケースも多いです。
C. Nginxのチューニング設定例
もしあなたがNginxをリバースプロキシとして運用している場合、クライアントから送られてくる巨大なヘッダー(あるいはCONTINUATIONで分割されて送られてくる一連のヘッダーブロック)を受け止めるためのバッファサイズ調整が必要になることがあります。
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL証明書等の設定は省略…
# ==========================================================
# HTTP/2 ヘッダーバッファのチューニング
# ==========================================================
# クライアントからのリクエストヘッダーが大きすぎる場合、
# 内部バッファ(large_client_header_buffers)を超えると 431 エラーになります。
# デフォルトでは不足しがちな大規模API環境では、ここを適切に拡張します。
# 1つのバッファの最大サイズと、割り当てる最大数
large_client_header_buffers 4 16k;
# HTTP/2における内部的なリクエストヘッダーの最大長制限
# (デフォルトは1024KB程度ですが、要件に応じて調整)
http2_max_header_size 64k;
location / {
proxy_pass http://backend_upstream;
# バックエンドへのプロキシ設定…
}
}
—
6. まとめ
HTTP/2の`CONTINUATION`フレームは、普段の開発では意識の片隅に追いやられがちな存在です。しかし、プロトコルの根幹である「フレーム分割」と「ストリーム管理」の仕組みを理解しているか否かで、難解なネットワークトラブル(原因不明の接続断やヘッダー起因の431エラー、さらにはセキュリティ脆弱性への対策)に直面したときの対応スピードが劇的に変わります。
- ヘッダーが1フレームに収まらない時は `CONTINUATION` が使われる
- 受信側は `END_HEADERS` が来るまでバッファリングし続ける(ここに脆弱性のリスクがあった)
- インフラやプロキシ(Nginx等)のバッファ設定は、実運用要件(JWTや巨大Cookie)に合わせて正しくサイジングする
ネットワークの現場では、パケットの1行、フラグの1ビットがすべての真実を語っています。次にWiresharkでパケットを覗く機会があれば、ぜひ`HEADERS`のあとに続く`CONTINUATION`の美しくも泥臭いバトンタッチに注目してみてください。
それでは、また次回のハードコアな技術解説でお会いしましょう。
コメント