QUICの接続はクライアントのIPが変わってもなぜ続くのか:connection ID、パス検証、ロードバランサーがすべきこと
QUICはアドレスではなくconnection IDでパケットを接続に結び付ける。RFC 9000に基づき、クライアントのアドレスが変わったときのパス検証、経路ごとに新しいconnection IDが要る理由、ロードバランサーの背後で移行を支える方法と止める方法をまとめた。
QUICサーバーのログを見ていると、同じ接続のパケットがある時点から別のIPアドレスやポートから届くことがある。クライアントが別のネットワークに移ったのかもしれないし、途中のNATが外側のポートを割り当て直したのかもしれない。QUICはどちらの場合も接続を続けられるように設計されている。ただし何もしなくても続くわけではない。受け取る側は新しいアドレスを確かめる必要があり、サーバーの前にあるロードバランサーはそのパケットを同じサーバーに届けられなければならない。
この記事はQUICバージョン1を定めたRFC 9000(2021年5月)の5.1節、5.2節、8章、9章、17.2〜17.3節、18.2節、19.15〜19.18節に基づく(2026-10-19確認)。特定のQUIC実装やブラウザ、OSが実際にいつ移行を始めるかは扱わない。RFC 9000の外にある拡張も扱わない。
接続を探す鍵はアドレスではなくconnection ID
QUICの接続は複数のconnection IDを持つことができ、そのどれでも接続を識別できる。大事なのは、各エンドポイントが相手が使うconnection IDを選ぶという点だ。クライアントからサーバーへのパケットのDestination Connection IDはサーバーが発行した値で、逆方向はクライアントが発行した値になる(5.1節)。
RFCが示すconnection IDの第一の目的は、UDPやIPといった下位層のアドレスが変わってもパケットが別のエンドポイントに届かないようにすることだ。受け取ったパケットのDestination Connection IDが長さ0でなく、既存の接続に対応していれば、そのパケットは送信元アドレスに関係なくその接続のものとして処理される(5.2節)。9章の冒頭も同じことを述べている。connection IDを使うことで、接続はエンドポイントのアドレス(IPアドレスとポート)の変化を乗り越えられる。
知っておきたい形式上の決まりがいくつかある。
- 1-RTTパケットが使うショートヘッダー(short header、17.3節)にはDestination Connection IDだけがあり、長さのフィールドがない。受け取る側が長さをあらかじめ知っている必要があるということだ。RFCは、connection IDでルーティングするロードバランサーを使う場合、長さを固定するか、固定部分に長さを埋め込む方式をロードバランサーと取り決められると書いている(5.1節)。
- QUICバージョン1ではconnection IDは最大20バイトだ(17.2節)。NEW_CONNECTION_IDフレームで渡すconnection IDは1〜20バイトでなければならない(19.15節)。
- connection IDには、発行者と協力していない外部の観測者が同じ接続のほかのconnection IDと結び付けられる情報を入れてはならない(MUST NOT)。同じ接続で同じ値を二度発行してもいけない(5.1節)。
長さ0のconnection IDも使える。この場合はアドレスで接続を区別することになるため、同じIPアドレスとポートで長さ0のconnection IDを使って複数の接続を同時に扱うと、相手の接続移行、NATの再バインディング、クライアントのポート再利用が起きたときに失敗するとRFCは書いている。それらの機能が使われないと確信できない限り、そうしてはならない(MUST NOT、5.1節)。
移行は誰が、いつできるか
RFC 9000はアドレスが変わる場合を二つに分けている(9章)。
| 種類 | 何が起きるか | エンドポイントの意図か |
|---|---|---|
| 能動的な移行(active migration) | エンドポイントが新しいローカルアドレスからパケットを送り始める | はい |
| NATの再バインディング | NATなどの中間装置がフローに新しい外側のポートやIPアドレスを割り当てる | いいえ |
どちらの場合も、受け取る側は相手のアドレスの変化を検知したら、以前に検証済みのアドレスでない限りパス検証を行わなければならない(MUST)。
移行には制限がある。
- ハンドシェイクが確認(confirmed)される前に移行を始めてはならない(MUST NOT)。「確認」の定義はRFC 9001の4.1.2節にあるとRFC 9000は書いている。
- このバージョンのQUICは、移行を新しいクライアントアドレスへのものに限っている。移行はすべてクライアントが始める。クライアントは知らないサーバーアドレスからパケットを受け取ったら破棄しなければならない(MUST)。唯一の例外が後述するサーバーの優先アドレス(preferred address)だ。
- サーバーが
disable_active_migrationトランスポートパラメーターを送っていれば、クライアントはハンドシェイク時にサーバーが使ったアドレスへ、新しいローカルアドレスからパケットを送ってはならない。探索用のパケットも同様だ(MUST NOT)。
探索パケットと非探索パケット
移行の手順を読むには、まず9.1節の区別を押さえておく必要がある。
| 分類 | 該当するフレーム |
|---|---|
| 探索フレーム(probing frame) | PATH_CHALLENGE、PATH_RESPONSE、NEW_CONNECTION_ID、PADDING |
| 非探索フレーム | それ以外のすべてのフレーム(STREAM、ACKなど) |
探索フレームだけを含むパケットが探索パケットで、ほかのフレームが一つでもあれば非探索パケットになる。エンドポイントは移行の前に、新しいローカルアドレスから探索パケットで経路を試してよい(MAY)。検証に失敗しても、その経路をこの接続で使えないというだけで、ほかに使える経路があれば接続は終わらない。
実際の移行は、新しいローカルアドレスから非探索フレームを含むパケットを送ることで行われる(9.2節)。受け取る側から見ると、新しいアドレスから非探索フレームを含むパケットが届いたら、相手がそのアドレスに移ったという合図になる(9.3節)。
クライアントのアドレスが変わったときにサーバーがすること
クライアントがアドレスAからアドレスBに移った場合を、RFC 9000の規則どおりにたどると次のようになる。図のconnection IDの名前や順序は説明のための例だ。
クライアント (A → B) サーバー
|-- Bから: STREAM など, DCID=s2 -------->| 新しいアドレスBから非探索パケットが届く
| | → 以降のパケットはBへ送る
|<-- Bへ: PATH_CHALLENGE(x), DCID=c2 ----| Bの検証を開始。検証が済むまでは
| | Bから受け取ったバイト数の3倍まで
|<-- Aへ: PATH_CHALLENGE(y), DCID=c1 ----| 以前の経路Aも検証する
|-- Bから: PATH_RESPONSE(x), DCID=s2 --->| 同じ8バイトが返る → Bの検証完了
| | 輻輳制御とRTT推定を初期値に戻す
| | (ポートだけの変化なら維持してよい)
s2: サーバーが発行。クライアントがBから最初のパケットを送るまで未使用だった値で、
クライアントはBから送るパケットにこれを使う。
c2: クライアントが発行。サーバーがBへ最初のパケットを送るまで未使用だった値で、
サーバーはBへ送るパケットにこれを使う。
c1: クライアントが発行。すでに経路Aに結び付いた値で、サーバーが移行前からAへの
送信に使っていたもの。c2はもうBへの送信に使ったので、Aへの送信に
使い回すことはできない(9.5節)。
各段階の要求レベルは次のとおり。
新しいアドレスへ送り、検証を始める。 移行を受け入れるなら、以降のパケットを新しいアドレスへ送らなければならず(MUST)、まだ進行中でなければ、相手がそのアドレスを本当に持っているかを確かめるパス検証を始めなければならない(MUST、9.3節)。パケットは順序が入れ替わって届くことがあるため、送信先アドレスを切り替えるのはパケット番号が最も大きい非探索パケットに応じたときだけだ。
検証前は多く送らない。 送信元アドレスを偽った攻撃者が、移行を利用して無関係な被害者にデータを大量に送りつけさせる可能性がある(9.3.1節)。そのため、検証されていないアドレスへは、そのアドレスから受け取った量の3倍までしか送れない(MUST、8章)。これをアンチアンプリフィケーション制限(anti-amplification limit)と呼ぶ。
PATH_CHALLENGEとPATH_RESPONSE。 検証する側は予測できない8バイトをPATH_CHALLENGEに入れて送り(MUST)、受け取った側は同じ値をPATH_RESPONSEで返さなければならない(MUST、8.2.1節、8.2.2節、19.17節、19.18節)。応答はPATH_CHALLENGEを受け取ったのと同じ経路で送る必要があり、輻輳制御による制約がない限り遅らせてはならない。1200バイトの規則は、二つのフレームで書き方が異なる。
- PATH_CHALLENGEを含むデータグラムは1200バイト以上に拡張しなければならない(MUST)。例外は、その経路のアンチアンプリフィケーション制限のためにその大きさを送れないときだけだ(8.2.1節)。この大きさで送ることで、送る側から相手への経路がQUICに使えることを確かめられる。
- PATH_RESPONSEを含むデータグラムも1200バイト以上に拡張しなければならない(MUST)。ただし、拡張した結果がアンチアンプリフィケーション制限を超えるなら拡張してはならない(MUST NOT、8.2.2節)。RFCは、これが起きるのは受け取ったPATH_CHALLENGE自体が拡張されたデータグラムで送られなかった場合だけだと見ている。
検証が成功するのは、送ったものと同じデータを含むPATH_RESPONSEを受け取ったときだけだ。PATH_CHALLENGEを1200バイト未満のデータグラムで送った場合でも、一致する応答が返れば相手のアドレスは検証されたことになり、それ以降は受け取った量の3倍を超えて送れる。ただし経路MTUは検証されていないので、経路が必要なMTUに対応しているかを確かめるには、1200バイト以上に拡張したPATH_CHALLENGEでパス検証をもう一度始めなければならない(MUST)。PATH_CHALLENGEを含むパケットへのACKは偽造できるため、検証とはみなされない(以上8.2.3節)。
以前の経路も確かめる。 経路外にいてもパケットを観測できる攻撃者は、本物のパケットのコピーを先に届けることで、NATの再バインディングに見える偽の移行を起こし、自分を経由する経路に接続を移させられる。これに対処するため、移行らしきことが起きたら直前まで使っていた経路もPATH_CHALLENGEで検証しなければならない(MUST、9.3.3節)。元の経路でパケット番号のより大きい非探索パケットが届けば、接続はその経路に戻る。
失敗したら戻る。 検証が失敗するのは、検証していた側が試みをあきらめたときだけだ。あきらめる判断はタイマーで行うのが原則で(SHOULD)、RFCは現在のPTOと新しい経路のPTOのうち大きいほうの3倍を推奨している。新しい経路のPTOはRFC 9002の初期RTT値(kInitialRtt)で計算する(8.2.4節)。新しいアドレスの検証に失敗したら、最後に検証できたアドレスに戻らなければならない(MUST)。そのアドレスの状態を持っていなければ、接続の状態をすべて捨てて黙って閉じなければならない(MUST、9.3.2節)。
輻輳制御を初期化する。 新しい経路の容量は以前の経路と同じとは限らない。相手が新しいアドレスを持っていると確認できたら、新しい経路の輻輳制御器とRTT推定器をすぐに初期値へ戻さなければならない(MUST)。例外はポート番号だけが変わった場合だ。ポートだけの変化はたいていNATの再バインディングや中間装置によるものなので、既存の状態を維持してもよい(MAY、9.4節)。
新しいクライアントアドレスを検証したサーバーは、新しいアドレス検証トークン(8章)をクライアントに送るのが原則だ(SHOULD、9.3節)。
新しい経路には新しいconnection ID
同じconnection IDを複数の経路で使うと、受動的な観測者が二つの経路のトラフィックを同じ接続のものだと結び付けられてしまう。そのためRFC 9000は次のように定めている(9.5節)。
- 複数のローカルアドレスから送るときに同じconnection IDを使い回してはならない(MUST NOT)。移行するときも、新しい経路を探索するときも同じだ。
- 複数の宛先アドレスへ送るときも同じconnection IDを使い回してはならない(MUST NOT)。
- 例外が一つある。相手が制御できないネットワークの変化(NATの再バインディングなど)のために、同じDestination Connection IDのパケットが新しい送信元アドレスから届いた場合は、同じローカルアドレスから送る限り、今のconnection IDを新しいリモートアドレスに対して使い続けてよい(MAY)。
この規則が実際に効いてくるのはconnection IDの在庫だ。上の図でサーバーがBへの送信に使ったc2は、クライアントがあらかじめ発行しておいた値でなければならない。使えるconnection IDを使い切ったエンドポイントは、新しい経路を探索することも移行を始めることもできず、相手の探索や移行に応答することもできない(9.5節)。受け取る側が相手から受け取った未使用のconnection IDを持っていなければ、相手が一つ渡してくれるまで新しい経路には何も送れない(9.3節)。
在庫を管理する仕組みは次のとおり。
| 仕組み | 役割 | 根拠 |
|---|---|---|
active_connection_id_limit トランスポートパラメーター |
相手から受け取って保持するconnection IDの最大数。2以上でなければならず(MUST)、なければ2とみなす | 18.2節 |
| NEW_CONNECTION_IDフレーム | 新しいconnection IDとシーケンス番号、ステートレスリセットトークン(16バイト)を渡す。Retire Prior Toフィールドで、それより小さい番号のIDの廃止を求める | 19.15節、5.1.2節 |
| RETIRE_CONNECTION_IDフレーム | 相手が発行したIDをもう使わないと伝える。新しいIDを送ってほしいという要求も兼ねる | 19.16節、5.1.2節 |
発行する側は、相手が使える未使用のconnection IDを十分に持てるようにすべきで(SHOULD)、相手の上限より多く渡してはならない(MUST NOT)。相手が一つ廃止したら新しいものを渡すのが原則だ(SHOULD、5.1.1節)。移行を始める側も、相手が移行時に新しいconnection IDを使えるよう、相手の在庫に気を配るべきだ(SHOULD)。相手が在庫を使い切っているかもしれないなら、新しい経路で送るすべてのパケットにNEW_CONNECTION_IDを入れる方法もRFCは挙げている(9.5節)。
ロードバランサーの背後にあるサーバーはどうすべきか
送信元と宛先のIPアドレスとポートだけでサーバーを選ぶロードバランサーだと、クライアントのアドレスが変わったパケットは別のサーバーに行ってしまうことがある。RFC 9000の5.2.3節は、こうした構成で接続を続けるための方法を二つ挙げている。
- connection IDに基づいてパケットを正しいサーバーへ転送する、帯域外(out-of-band)の仕組みを用意する。
- 最初に接続されたアドレスとは別の専用IPアドレスやポートを使えるなら、
preferred_addressトランスポートパラメーターでクライアントにそちらへ移るよう求める。クライアントはそのアドレスを使わないことも選べる。
どちらも用意できない構成のサーバーは、disable_active_migration トランスポートパラメーターで移行に対応していないことを伝えるべきだ(SHOULD)。このパラメーターについて知っておきたい点がある。
- このパラメーターが禁じるのは、クライアントがハンドシェイク時のサーバーアドレスへ新しいローカルアドレスから送ること、つまり能動的な移行だ(18.2節)。NATの再バインディングはクライアントが新しいローカルアドレスを使うわけではないので、この禁止で防げるものではない。アドレスの変化を検知したらパス検証を行うという9章の規則はそのまま適用される。この部分はRFCの文言をこの記事が解釈したものだ。
- クライアントがこの規則に違反した場合、サーバーはステートレスリセット(Stateless Reset)を送らずにその経路のパケットを捨てるか、パス検証を進めて移行を受け入れるか、どちらかをしなければならない(MUST、9章)。ステートレスリセットを送ったり接続を閉じたりすると、第三者がトラフィックを偽装して接続を切れるようになってしまうからだ。
- クライアントが
preferred_addressに従って移ったあとは、このパラメーターは移行を禁じない。 - 単純なアドレスベースの負荷分散を使うサーバー構成は、ステートレスリセットオラクル(stateless reset oracle)を作らないようにしなければならない(MUST、5.2.3節、21.11節)。
サーバーの優先アドレス(preferred address)
サーバーのアドレスを変える唯一の方法が、9.6節の優先アドレスだ。RFCは、複数のサーバーが共有するアドレスで最初に接続を受け、そのあと接続の安定のためにユニキャストアドレスへ移したい場合に特に役立つと書いている。
- サーバーはハンドシェイク中に
preferred_addressトランスポートパラメーターで、IPv4とIPv6のアドレスを一つずつ知らせられる。一方のアドレスファミリーだけを示すなら、もう一方を0.0.0.0:0や[::]:0で埋める(18.2節)。 - このパラメーターにはシーケンス番号1のconnection IDとステートレスリセットトークンが入っているので、クライアントが移るときに使える未使用のconnection IDが少なくとも一つ保証される。長さ0のconnection IDを使うサーバーは優先アドレスを送ってはならない(MUST NOT)。
- クライアントはハンドシェイクが確認されたあと、二つのアドレスのどちらかを選んでパス検証を始めるのが原則で(SHOULD)、移る前に必ず検証しなければならない(MUST、9.6.2節)。検証に失敗したら、元のサーバーアドレスへ送り続けなければならない(MUST)。
- サーバーは、クライアントが優先アドレスへ送った非探索パケットを受け取り、新しい経路の検証も済ませるまでは、元のアドレスから非探索パケットを送らなければならない(MUST)。
- 優先アドレスは、それを知らせた接続でのみ有効だ。この接続から再開(resume)した接続も含め、ほかの接続で使ってはならない(MUST NOT)。
まとめ:実装や運用で確認すること
RFC 9000から直接出てくる規則と、それをもとにしたこの記事の提案を分けて書く。
RFC 9000の規則
- アドレスが変わったパケットでもconnection IDが合えば同じ接続として扱い、新しいアドレスはパス検証で確かめる。
- 検証前は、そのアドレスから受け取った量の3倍までしか送らない。
- 移行らしきことが起きたら以前の経路もPATH_CHALLENGEで検証し、新しいアドレスの検証に失敗したら最後に検証できたアドレスに戻る。
- 新しいローカルアドレスや新しい宛先アドレスには未使用のconnection IDを使う。相手には未使用のconnection IDを十分に発行しておく。
- connection IDでサーバーを見つけられないロードバランサーの背後なら、
disable_active_migrationを送る。
この記事の提案
active_connection_id_limitが既定値の2なら、ハンドシェイクで使ったID以外の予備は一つしかないかもしれない。移行やNATの再バインディングを頻繁に経験するクライアントを受けるサーバーなら、この値と実際の発行数を確認しておく。- 接続単位のログには、アドレスだけでなくconnection IDのシーケンス番号とパス検証の結果も残す。同じ接続でアドレスが変わった記録を、移行、NATの再バインディング、検証失敗に分けて見られる。
参考にしたRFC
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport(2021年5月):5.1節、5.2節、8章、9章、17.2〜17.3節、18.2節、19.15〜19.18節。
この記事はRFC 9000の規則だけを扱う。リンクと内容は2026年10月19日に確認した。