Happy Eyeballs v2:デュアルスタックのクライアントはIPv6とIPv4をどう競わせるのか

DNSがIPv6とIPv4のアドレスを両方返したとき、クライアントがAAAA・Aの問い合わせ、アドレスの並べ替え、時間差をつけた接続試行で先に成功した接続を選ぶ、RFC 8305の手順と推奨デフォルト値の意味を整理します。

ネットワークIPv6DNSTCP

DNS の名前解決の記事は、A レコードと AAAA レコードでアドレスを受け取ったあと、どのアドレスから接続を試すかはクライアントが決める、という話で終わりました。この記事はその続きです。1つのホスト名が IPv6 と IPv4 のアドレスを複数返すとき、デュアルスタックのクライアントがどの順番で試し、いつ次のアドレスに移るのかを定めたのが Happy Eyeballs です。現在の仕様は RFC 8305(Happy Eyeballs Version 2)です。

アドレスを1つずつ待つと何が起きるか

RFC 6724 は、getaddrinfo() のような API がアドレスのリストを並べ替えて返し、アプリケーションは通常、先頭のアドレスから接続を試して、使えるアドレスが見つかるまでリストを順にたどると説明しています。このやり方では、前のほうのアドレスが応答しないと、その試行が失敗に終わるまで次のアドレスに進めません。

RFC 6555 は 2012 年当時の状況をこう書いています。IPv6 の接続性に問題がある環境では、IPv6 に対応したアプリケーションが IPv4 にフォールバックするまでに何秒もの遅延が生じ、その結果、端末で IPv6 を丸ごと無効にすることが多かったというものです。DNS に AAAA レコードがあっても、その IPv6 経路が実際に使えるとは限りません。

RFC 6724 は、最初のアドレスだけを使って失敗したらあきらめるべきではない(SHOULD NOT)としたうえで、アプリケーションによっては小さな遅延を挟みながら複数のアドレスを並行して試し、最初に成功したものを使う方法が適切な場合もあると述べています。Happy Eyeballs はこの考え方を具体的な手順にしたものです。

RFC 8305 が定めること、定めないこと

RFC 8305 は 2017 年 12 月に発行された Standards Track の文書で、最初のアルゴリズムを記述した RFC 6555(2012 年 4 月)を廃止(obsolete)しています。文書自身が示す内容は2つです。ユーザーが感じる遅延を減らすアルゴリズムが満たすべき要件と、例としてのアルゴリズムです。

そのため、この記事に出てくる 50 ms や 250 ms といった数値は、RFC が推奨する設定可能なデフォルト値であり、すべてのブラウザや OS がそのまま使う値ではありません。RFC 8305 の 8 節は、これらの遅延値が非常に多くの実運用デバイスで接続のタイミングを測定して決めたものであり、時間とともに変わると予想されること、実装者は仕様を変えずに別の値を使ってよいことを述べています。特定のクライアントが実際にどの値を使っているかは、その実装のドキュメントやコードで別途確認する必要があります。

RFC 8305 はホストが IPv4 より IPv6 を優先するポリシーを前提に書かれており、ポリシーが異なる場合は推奨事項をそれに合わせて読み替えればよいとしています。手順は4つの段階に分かれます。

  1. 非同期の DNS 問い合わせを開始する(3 節)
  2. 受け取った宛先アドレスを並べ替える(4 節)
  3. 非同期の接続試行を開始する(5 節)
  4. 1つの接続が確立したら、ほかの試行をキャンセルする(5 節)

ステップ1:AAAA と A を送り、両方を待たない

IPv4 と IPv6 の両方の接続性を持つクライアントは、AAAA と A の両方の問い合わせを送る必要があります。RFC は、2つをできるだけ続けて送り、AAAA を先に、その直後に A を送るよう推奨しています(SHOULD)。

また、両方の応答が返ってくるのを待ってから接続を始めるべきではなく(SHOULD NOT)、DNS の名前解決を非同期として扱うべきだとしています(SHOULD)。片方の応答が返ってこなかったり大きく遅れたりすると、もう片方の接続まで遅れてしまうためです。プラットフォームに非同期の DNS API がない場合は、アドレスファミリーごとに別スレッドで同期問い合わせを1つずつ実行すれば同じ動作を再現できる、とも書かれています。

どちらの応答が先に届くかで動作が分かれます。

  • 有効な AAAA レコードを1つ以上含む応答が先に届いたら、最初の IPv6 接続試行をすぐに始めます。
  • 有効な A 応答が先に届いたら、IPv6 を優先するために AAAA 応答を少しだけ待ちます(SHOULD)。この待ち時間を Resolution Delay と呼び、推奨値は 50 ms です。RFC は、AAAA 応答が A 応答の数ミリ秒後に届くことはよくあると説明しています。
  • その待ち時間内に有効な AAAA 応答が届けば、すぐに IPv6 の試行を始めます。データのない AAAA 応答(no error, no data)が届くか、待ち時間が終わっても応答がなければ、それまでに受け取った IPv4 アドレスで並べ替えと接続試行に進みます(SHOULD)。
  • 接続試行の途中で、まだどの接続も確立していないときに AAAA 応答が届いたら、新しい IPv6 アドレスを候補リストに加えて試行を続けます。

つまり 50 ms は「A が先に届いたときに AAAA をどれだけ待つか」の値であり、DNS の名前解決全体に加わる待ち時間ではありません。

ステップ2:RFC 6724 で並べ替えてからアドレスファミリーを交互にする

接続を始める前に、クライアントは試行の順番を決めます。このリストには、その時点までに受け取った両方のアドレスファミリーのアドレスをすべて含めます。そうすることで、最初の IPv6 アドレスと最初の IPv4 アドレスだけでなく、リスト全体に競争の効果が及びます。

まず、RFC 6724 の 6 節にある宛先アドレス選択(Destination Address Selection)で並べ替えなければなりません(MUST)。このアルゴリズムは IPv6 と IPv4 のアドレスを1つのリストでまとめて並べ替え、2つのアドレスを比較する 10 個のルールを順に適用します。到達できない宛先を避けるルールや、ポリシーテーブルの優先度(precedence)を比べるルールなどが含まれます。RFC 8305 はさらに、クライアントが経路ごとの RTT の履歴を持っているなら、RTT の低いアドレスを優先するルールを RFC 6724 のルール 8 と 9 の間に追加すべきだとしています(SHOULD)。この履歴は、異なるネットワークインターフェースをまたいで使ってはいけません(MUST NOT)。

次に、アドレスファミリーが交互に並ぶようにリストを組み替えます(SHOULD)。並べ替えた結果の先頭が IPv6 なら、最初の IPv4 アドレスを2番目に繰り上げる、という具合です。あるアドレスファミリーがまるごと使えないときに、そのファミリーの長いリストを全部待たずに済むようにするためです。別のファミリーに移る前に最初のファミリーのアドレスをいくつ続けて試すかを First Address Family Count と呼び、推奨値は 1、特定のファミリーをより強く優先したい場合は 2 を使えます。

ステップ3:開始は時間差で、試行は重なって進む

接続試行は同時に開始しません(SHOULD NOT)。ネットワークに過大な負荷をかけないよう、まずリストの先頭のアドレスに1つ試行を開始し、残りを1つずつ順に開始します。ただし、新しい試行を始めても前の試行は止まらないため、複数の試行が同時に進行中になることがあります。全部を一斉に送るのではなく、間隔を空けて1つずつ追加しながら、先に始めた試行も待ち続ける方式です。

次の試行までの間隔を Connection Attempt Delay と呼びます。

  • RTT のデータがない場合の推奨デフォルト値は 250 ms です。より精緻な実装では、前の試行が2回目の TCP SYN を送る時点、つまり最初の SYN 再送の時点に合わせるべきだと RFC は説明しています。
  • 下限は必ず設けなければならず(MUST)、前の試行から 10 ms 以内に次の接続を開始してはいけません(MUST NOT)。推奨される最小値(Minimum Connection Attempt Delay)は 100 ms です。
  • 上限も設けるべきで(SHOULD)、推奨される最大値(Maximum Connection Attempt Delay)は 2 秒です。

まとめると、必ず守るべきなのは下限を設けることと 10 ms より短い間隔で開始しないことで、50 ms、1個、250 ms、100 ms、2 秒は推奨値です。

ステップ4:先に成功した接続を使う

試行の1つが成功したら(通常は TCP ハンドシェイクが完了した時点)、まだ成功していないほかの試行はすべてキャンセルし(SHOULD)、まだ試していないアドレスは無視します(SHOULD)。このとき、進行中の非同期 DNS 問い合わせはキャンセルしてもかまいません(MAY)。ただし DNS リゾルバは、その後も短い時間(推奨は 1 秒)ネットワークから届く DNS 応答を処理し続けるべきです(SHOULD)。その応答が DNS キャッシュに入り、後の接続で使えるためです。

推奨デフォルト値で描いた仮想のタイムライン

以下は、RFC 8305 の推奨デフォルト値(Resolution Delay 50 ms、First Address Family Count 1、Connection Attempt Delay 250 ms)をそのまま使うと仮定した仮想の例です。時刻は測定値ではなく、アドレスはドキュメント用の例示アドレスです。RFC 6724 の並べ替えで IPv6 アドレスが先頭に来たと仮定しています。

(仮想の例、RFC 8305 の推奨デフォルト値に基づく)
  0 ms  AAAA 問い合わせを送信、直後に A 問い合わせを送信
 20 ms  A 応答が到着: 192.0.2.10
        → AAAA を最大 50 ms(70 ms の時点まで)待つ
 35 ms  AAAA 応答が到着: 2001:db8::10, 2001:db8::20
        → 並べ替えてアドレスファミリーを交互に:
          [2001:db8::10, 192.0.2.10, 2001:db8::20]
        → 試行1を開始: 2001:db8::10(応答なし)
285 ms  試行1はまだ確立していない → 試行2を開始: 192.0.2.10
        (試行1はキャンセルせず続行)
325 ms  試行2の TCP ハンドシェイクが完了 → この接続を使う
        → 試行1をキャンセル、2001:db8::20 は試さない

もし 70 ms までに AAAA 応答が届かなかった場合、クライアントはその時点で IPv4 アドレスだけで並べ替えと接続試行を始めます。その後、まだ接続が確立する前に AAAA が届けば、その IPv6 アドレスをまだ試していないアドレスのリストに並べ替えて加えます。

v1(RFC 6555)からの変更点

RFC 8305 の付録 A によると、RFC 6555 は AAAA レコードと A レコードを持つホストへの接続をどう時間差で試すかに主に焦点を当てていました。RFC 8305 はそこに、DNS 問い合わせの方法、アドレスファミリーごとに複数のアドレスがある場合の扱い、競争中に DNS の応答が変わった場合の扱い、履歴情報の活用、NAT64/DNS64 を使う IPv6 専用ネットワークへの対応を加えています。また、RFC 8305 のアルゴリズムを単純に実装しても、以前の仕様である RFC 6555 に準拠していると書かれています。

トラブルシューティングで覚えておきたいこと

  • Happy Eyeballs が扱うのは TCP/IP 層での最初の接続失敗です。それ以外の失敗や性能の問題は選ばれた接続に影響し得ますし、最初のハンドシェイクの間しか動作しないため、MTU 関連の問題が表に出ないことがあります(9 節、9.1 節)。
  • DNS が異なるアプリケーションサーバーのアドレスを返す場合、アプリケーション自体がそのすべてで正常に動いているとは限りません。RFC は TLS と HTTP をよくある例として挙げています(9.2 節)。
  • 設定ミスで IPv6 が常に失敗し IPv4 は動いている場合でも、Happy Eyeballs のせいで運用者が問題に気づきにくくなることがあります。RFC は、ネットワーク運用者がすべてのアドレスファミリーを外部の手段で監視するよう推奨しています(9.3 節)。接続できているからといって IPv6 が正常だとは限りません。
  • 同じホスト名への接続が毎回同じ IP アドレスに向かうと仮定してはいけません。RFC 8305 は、Happy Eyeballs によって後続の接続が別の IP アドレスを使う可能性が高まり得ると述べ、セキュリティ上の性質を安定したホスト名とアドレスの対応に頼らないよう求めています(10 節)。

出典

確認日:2026-10-11。この記事は RFC 8305 に基づいて説明しています。同日、IETF Datatracker で RFC 8305 が Proposed Standard であり、これを廃止した RFC が表示されていないことを確認しました。

ブログへ