デュアルスタックソケット接続時の `REMOTE_ADDR` にIPv4アドレスを正しく表示する

puma/puma

デュアルスタックIPv6ソケット経由でIPv4クライアントが接続した際に REMOTE_ADDRCommonLogger のログに表示されていた ::ffff:127.0.0.1 形式のアドレスを、元のIPv4形式(127.0.0.1)に変換して返すよう修正されました。

背景

この問題は #3847 によるデフォルトバインドアドレスの変更に起因しています。#3847 では、Pumaのデフォルトバインドアドレスを 0.0.0.0(IPv4)から ::(IPv6)へ変更しました。:: はデュアルスタックソケットを開き、IPv4とIPv6の両方のクライアントを受け付けます。

デュアルスタックソケットにIPv4クライアントが接続した場合、OSはその接続を IPv4マップドIPv6アドレス(IPv4-mapped IPv6 address)として表現します。これは ::ffff: というプレフィックスにIPv4アドレスを付加した形式(例: ::ffff:127.0.0.1)です。Pumaは peeraddr からこのアドレスをそのまま取得していたため、REMOTE_ADDRCommonLogger のログに ::ffff:127.0.0.1 が表示されていました。

IPアドレスが変わったことで、IPベースのアクセス制御やログ分析ツールが想定外の形式のアドレスを受け取る可能性があり、既存の設定との互換性が損なわれる問題でした。

技術的な変更

lib/puma/client.rbpeerip メソッドで @io.peeraddr.last を直接参照していた箇所を、新たに追加した socket_peerip メソッドの呼び出しに置き換えました。

変更前:

def peerip
  return @peerip if @peerip

  if @remote_addr_header
    hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
    @peerip = hdr
    return hdr
  end

  @peerip ||= @io.peeraddr.last
end

変更後:

def peerip
  return @peerip if @peerip

  if @remote_addr_header
    hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
    @peerip = hdr
    return hdr
  end

  @peerip ||= socket_peerip
end

socket_peeripunmap_ipv6 の2つのプライベートメソッドが追加されています。

IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
private_constant :IPV4_MAPPED_IPV6_PREFIX

def socket_peerip
  unmap_ipv6(@io.peeraddr.last)
end

# Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
# their IPv4 form. These addresses appear when IPv4 clients connect to
# a dual-stack IPv6 socket.
def unmap_ipv6(addr)
  addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
end

変換ロジックは String#delete_prefix を使って ::ffff: プレフィックスを取り除くだけのシンプルな実装です。プレフィックスが存在しない場合(純粋なIPv4アドレスやネイティブIPv6アドレス)はそのまま返されるため、既存の動作には影響がありません。

テストは test/test_request_single.rb に4つのケースが追加されています。

  • test_peerip_unmaps_ipv4_mapped_ipv6: ::ffff:127.0.0.1127.0.0.1 に変換されること
  • test_remote_addr_header_fallback_unmaps_ipv4_mapped_ipv6: @remote_addr_header フォールバック時にも変換が適用されること
  • test_peerip_preserves_plain_ipv4: 通常のIPv4アドレスが変換されないこと
  • test_peerip_preserves_native_ipv6: ネイティブIPv6アドレス(::1 など)が変換されないこと

設計判断

変換ロジックを peerip メソッド内に直接書くのではなく、socket_peeripunmap_ipv6 の2段階のプライベートメソッドに分離しました。これにより、peerip メソッドの呼び出し元が @remote_addr_header を使うフォールバックパスと通常パスの両方で同じ変換が適用されます。重複なく一貫した挙動を保証する設計です。

また、プレフィックス定数 IPV4_MAPPED_IPV6_PREFIXprivate_constant を付与して外部からのアクセスを禁止しています。実装の詳細を公開インターフェースから隠蔽し、将来の変更の自由度を保つ判断です。

変換の判定に正規表現やアドレスパースを使わず、delete_prefix による文字列マッチングのみとしているのも注目点です。::ffff: は標準化されたプレフィックスであり、追加の検証なしに前方一致で除去するだけで正確に動作します。

まとめ

デフォルトバインドアドレスの :: への変更(#3847)で生じた副作用を、ソケット層でのIPv4マップドIPv6アドレスの変換によって修正した変更です。変換ロジックを peerip の参照箇所に集約することで、ヘッダーフォールバックを含むすべてのパスで一貫した動作を保証しつつ、既存のIPv4・IPv6クライアントへの影響を排除しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
473388a4

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→セクション群(各論)→まとめ(結論)の3部構成が明確に適用されています。背景、技術詳細、設計判断、まとめといった必須・任意要素がすべて含まれており、模範的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:filepath)とGitHubのPRへのリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「IPv4マップドIPv6アドレス」や「デュアルスタックソケット」といった専門用語を前提として説明が進んでおり、対象読者であるエンジニアに適した技術レベルと表現になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。これにより、非常に高い可読性が確保されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロック(変更前・変更後・追加分)は、提供されたDiff情報と完全に一致しています。テストケースに関する記述も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「IPv4-mapped IPv6 address」、「peeraddr」、「private_constant」など、関連する技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

IPv4マップドIPv6アドレスが生成される仕組みや、`delete_prefix`による変換ロジックの説明など、技術的な解説はすべて正確で論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコードで裏付けられています。特に「設計判断」セクションは、コード構造から妥当な意図を読み解いており、創作(ハルシネーション)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#3916, #3847)、ファイル名、IPアドレスなどの数値や固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRのタイトル「Fix IPv4-mapped IPv6 addresses in `REMOTE_ADDR` and request logs」の内容を的確に要約しており、主題が一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事には、バージョンサポート状況やリリース日程など、PR情報に基づかない外部知識は一切含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内に時間表現に関する記述はなく、事実関係を歪曲するような記述は見られません。