Claude が書いた記事
HTTP/2 のフレームは TCP にどう乗ってるか ── バイト列に「長さ」で切れ目を入れる
HTTP/2 Bomb の脆弱性を調べてて、攻め口の手前で詰まった。そもそも HTTP/2 って TCP の上をどう流れてるんや? フレームと TCP セグメントが別単位という話を整理した学習ログ。
俺の最初の疑問
きっかけは HTTP/2 Bomb (CVE-2026-49975) や。あの脆弱性を調べてて「で、これは HTTP/2 をどうやって攻めるんや?」と疑問に思った。HPACK・フレーム・フロー制御 ── 攻め口の説明にそういう言葉が出てくるけど、その手前で引っかかった。
そもそも HTTP/2 って、TCP の上をどう流れてるんや?「フレーム」ってイーサネットフレームのことか?
攻撃の中身に入る前に、HTTP/2 ってなんやねん?を整理した記録。
まず一言でいうと
HTTP/2 フレームと TCP セグメントは別の単位。1 つのフレームが複数の TCP セグメントにまたがることがあるし、逆に 1 つのセグメントに複数のフレームが入ることもある。(TCP が一回に送り出すデータの単位は「セグメント」── 「パケット」は IP の単位や。詳しくは後述。)
カギは TCP が「切れ目のないバイト列」だということ。TCP はメッセージの境界を一切持たへん。ただ順番どおりにバイトを届けるだけ。だから上に乗る HTTP/2 は、各フレームの頭に「ここから何バイトがこのフレームか」を書いた固定長のヘッダを置く。受信側はその長さを頼りに、バイト列のどこで切れててもフレームの境界を復元できる。
これが フレーミング (バイト列にメッセージの切れ目を入れる仕組み) や。
何と比べるとわかるか
HTTP/1.1 と並べると、HTTP/2 が何をしてるかが見える。どっちも下は同じ TCP のバイト列やのに、境界の付け方が違う。
| HTTP/1.1 | HTTP/2 | |
|---|---|---|
| 境界の付け方 | テキストの区切り (\r\n・Content-Length) | バイナリの長さ前置き (9 バイトのフレームヘッダ) |
| 1 単位 | メッセージ (リクエスト / レスポンス) | メッセージをフレームに分割 (HEADERS / DATA / …) |
| 区切りの読み方 | 区切り文字が来るまで読む | 頭の Length を見て、その分だけ読む |
HTTP/1.1 は「\r\n が来たらヘッダの行が終わり」みたいに区切り文字を探しながら読む。HTTP/2 は逆に最初に長さを宣言してしまう。長さが分かってるから、バイト列のどこでセグメントが切れても「あと何バイト足りない」が計算できる。
「メッセージ」(リクエスト / レスポンス 1 個) はどっちにもある考え方やけど、「フレーム」は HTTP/2 で新しく入った下位の単位。HTTP/1.1 にこれに当たる名前は無く、メッセージをまるごと 1 塊で送るだけや。
何がややこしいのか ── TCP には境界が無い
TCP を「パケットの集まり」と思うと混乱する。アプリから見た TCP は、1 本のバイト列にしか見えない。パケットの切れ目はアプリに見えへん。送る側の TCP は、都合 (MSS・輻輳・タイミング) で勝手にバイト列を区切って運ぶ。受け取る側の TCP は、それを順番どおりに繋いでアプリに渡す。どこで区切られたかはアプリに伝わらない。
ここで「TCP コネクション 1 本」がイメージしにくい人へ ── 「パケット = だいたい 1500 バイト」みたいなサイズ感がコネクションには無いからや。サイズがどこに付くのかを、いちばんやさしい言葉で。
「TCP コネクション 1 本」は、荷物が積める「入れ物」やなくて、自分の家と相手の家をつなぐ 1 本の道路 (1 車線)。道路そのものに「何キロ積める」っていう大きさは無いやろ? つながってるか・つながってないか、だけや。
トラックと、その荷物
その道路を走るのがトラック。送りたいデータは、トラックに荷物として積んで運ぶ。
- トラック = パケット … 宛先ラベル (IP アドレス) を貼った車体。1 台 ~1500 バイトまで。「1500 バイト」と思い浮かぶのはこれ。
- 荷物 = セグメント … トラックに載ってる中身 (TCP が送りたいデータ)。
つまりセグメント (荷物) が、パケット (トラック) に載って運ばれる。同じ物の呼び名違いやなくて、入れ子や。
だから
- 「コネクション 1 本」(道路) にサイズは無い。サイズはトラック (パケット) の方にある。
- 大きいデータは、何台ものトラックに分けて運ぶ。同じ道路 1 本で 1 台でも 100 台でも走れる。
HTTP/2 フレームの頭 9 バイトの内訳はこうなってる。
- Length (3 バイト) … この後に続く payload が何バイトか
- Type (1 バイト) … HEADERS / DATA / SETTINGS など
- Flags (1 バイト)
- Stream Identifier (4 バイト) … どのストリームのフレームか
この Type が「何のフレームか」を決める。1 つの HTTP メッセージは、ヘッダーとボディが種類ごとに別のフレームに分かれて運ばれる。
- HEADERS フレーム (Type=HEADERS) … HTTP ヘッダー (Host・Content-Type・
:methodなど) を運ぶ。中身は HTTP/2 Bomb で出てきた HPACK で圧縮されたバイナリ。 - DATA フレーム (Type=DATA) … ボディを運ぶ。
HTTP/1.1 が「ヘッダーもボディも 1 つのテキストの塊」で送ってたのを、HTTP/2 は種類で分けてフレームに刻む。
その「番号で参照」がどれくらいデータを削るか、具体で見るとこう。
受信側がやることは単純。「9 バイトのヘッダを読む → Length を見る → その分だけ payload として読む → 読み切ったら、次の 9 バイトはまた新しいヘッダ」をループするだけ。長さを前置きしてあるから、バイト列のどこで切れてても境界が復元できる。
ヘッダの Stream Identifier も効いてくる。これは「このフレームがどのやり取り (リクエスト) のものか」を示す名札で、これがあるから複数のリクエストのフレームを 1 本の TCP に混ぜて流せる (= 多重化)。受信側は名札で仕分けて元のやり取りに戻す。HTTP/1.1 が 1 本で 1 つずつしか流せなかったのと、ここが大きく違う ── ただ、この多重化は本記事の主役 (フレーム = 長さ前置き) の応用なので、深掘りは「次におすすめ」に譲る。
図で見る
まず、1 フレームが 3 つの TCP セグメントに分割されたとき、何がどこに入るか。
次に、その境界をどう見つけるか。TCP は切れ目のないバイト列で、HTTP/2 は「長さ」で切れ目を作る。
混乱しやすいポイント
① 「フレーム」「セグメント」「パケット」── 層ごとに単位の名前が違う
network をやってると「フレーム」でまず浮かぶのはイーサネットフレームやし、「TCP パケット」と言われると違和感が出る ── その勘は正しい。各層で運ぶ単位の名前は別々やから。
| 層 | 単位の名前 |
|---|---|
| アプリ (HTTP/2) | フレーム |
| トランスポート (TCP) | セグメント |
| ネットワーク (IP) | パケット |
| データリンク (Ethernet) | フレーム |
だから「TCP パケット」は厳密には変で、TCP の単位はセグメント。パケットは IP の単位や (「IP パケット」がすっと入るのはこれ)。「フレーム」も L2 と HTTP/2 で名前を使い回してるだけで中身は別物。そして上の単位は下の単位に包まれて運ばれる (入れ子)。HTTP/2 フレームは TCP セグメントの中身になり、そのセグメントは IP パケットに包まれ、さらにイーサネットフレームに包まれて線を流れる。この記事で「フレーム」と言ったら全部 HTTP/2 の方や。
(キャプチャツールが行を一律「Packet」と表示するから「TCP パケット」と錯覚しやすいけど、割れてる中身は TCP セグメントや。)
② 「組み立て直す」は表示の話やない
キャプチャしても、行は 3 分割なら 3 行のまま (ツールは各行を「Packet」と表示する)。3 つが 1 行に合体表示されるわけやない。フレームを「組み立て直す (reassembly)」のは、上位プロトコル (HTTP/2) を解釈するための内部のバイト連結であって、画面表示でも箱の数でもない。
ファイル読み込みと同じや。中身が HELLO のファイルを、OS は HE と LLO に分けて読むかもしれん。でもアプリは HELLO として扱える。OS が HE + LLO を内部で繋いだからといって、ディスク上のブロックが 1 個になったわけやない。TCP も同じで、Packet100 / 101 / 102 はそのまま。ただ HTTP/2 がそれを順に読んで「Length=4000 のフレームが揃った」と判断できるようになるだけ。
③ フレームヘッダーは先頭に 1 回だけ
分割されたとき、フレームヘッダー (9 バイト) が入ってるのは先頭のセグメントだけ。後続のセグメントは payload の続きしか入ってへん。「3 つに割れてるからヘッダーも 3 つ」やない。
④ 逆に 1 セグメントに複数フレームも入る
分割の逆もある。小さいフレームが続くなら、1 つの TCP セグメントに複数の HTTP/2 フレームが詰まる。フレームの境界とセグメントの境界は揃わへん ── これがフレーミングの本質や。
たとえ話
長さラベル付きの宅配。TCP は「箱を順番に届けるだけの運送屋」で、中身がどこで切れてるかは知らん。送り主 (HTTP/2) は中身を運送屋に渡す前に、各荷物の頭に「この荷物は 4000 グラム」と長さラベルを貼る。受け取り手は箱がどう分かれて届こうが、ラベルの数字どおりに測り取れば、荷物の切れ目を正しく復元できる。箱 (セグメント) の分かれ方と、荷物 (フレーム) の分かれ方は、最初から別物。
出てきた言葉の変換表
| 言葉 | つまり何の話? |
|---|---|
| フレーム | HTTP/2 のやり取りの単位 (HEADERS / DATA など)。TCP セグメントとは別単位 |
| セグメント | TCP が一回に送り出すデータの単位。IP の「パケット」・L2 の「フレーム」と層が違う |
| フレーミング | 切れ目のないバイト列に、メッセージの境界を入れる仕組み |
| Length (フレームヘッダの先頭 3 バイト) | この後に続く payload のバイト数。境界を復元する頼り |
| Stream Identifier | そのフレームがどのストリームのものか (多重化で効く) |
| reassembly (再構成) | 分割された payload を上位プロトコル解釈のため繋ぐ内部処理。表示の統合やない |
| バイトストリーム | TCP がアプリに見せる、切れ目のない 1 本のバイト列 |
| 長さ前置き (length-prefix) | 中身の前に「何バイトか」を書く境界の付け方。HTTP/2 のやり方 |
この記事の続きとして、未来の自分が次に掘るといいトピック。
- HPACK の動的テーブル ── HEADERS フレームの中で何が起きてるか。HTTP/2 Bomb の増幅の本丸で、フレームの中身に一段降りる話。
- HTTP/2 の多重化とストリーム ── Stream Identifier で複数のやり取りを 1 本の TCP に混ぜる仕組み。フレーミングが効いてくる出口。
- TLS レコード ── 同じ「長さ前置きで TCP に境界を作る」を暗号層でやってる。物差しを延ばす最初の一歩。
- MSS / MTU と TCP セグメント化 ── そもそも TCP がどんな都合でバイト列を区切るか。「なぜまたがるのか」の下地。