プロダクション環境のデフォルトバインドアドレスをIPv6(`::`)に変更
Pumaのプロダクション環境におけるデフォルトバインドアドレスが 0.0.0.0 から :: に変更されました。IPv6ソケットは多くのOSでIPv4接続も受け付けるため、既存のワークフローへの影響を最小限に抑えながら、IPv6優先のインフラへの移行を支援します。
背景
AWSがパブリックIPv4アドレスに課金を開始したことを背景に、IPv4アドレスのコストが顕在化し、より豊富で安価なIPv6アドレスへの移行が現実的な選択肢となっています。Issue #3812 では、0.0.0.0(IPv4の全インターフェース)に対応するIPv6版として :: を採用することが提案されました。
:: と 0.0.0.0 の機能的等価性は重要な前提です。下表はホストごとの接続可否をまとめたものです:
| ホスト | localhost:3000 | 127.0.0.1:3000 | [::1]:3000 | 0.0.0.0:3000 | [::]:3000 | ネットワーク公開 |
|---|---|---|---|---|---|---|
0.0.0.0 |
✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
:: |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
:: は 0.0.0.0 が応答するすべてのURLに加え、[::1]:3000 にも応答します。一方で ::1(IPv6ループバック)は 127.0.0.1:3000 や 0.0.0.0:3000 に応答しないため、開発環境向けのデフォルト変更には採用されていません。同様の変更がRailsにも提案されており(rails/rails#56470)、エコシステム全体での移行が進んでいます。
技術的な変更
IPv6を利用できない環境への対応として、このPRは単純な定数置き換えではなく、実行時にネットワークインターフェースを検査してデフォルトを動的に決定する設計を採用しました。
lib/puma/configuration.rb に require 'socket' と require 'uri' が追加され、Configuration.default_tcp_host クラスメソッドが実装されています。このメソッドは Socket.getifaddrs を通じてシステムのネットワークインターフェースを調べ、非ループバックのIPv6インターフェース(ipv6? && !ipv6_loopback?)が存在する場合は Const::UNSPECIFIED_IPV6(::)を、存在しない場合は Const::UNSPECIFIED_IPV4(0.0.0.0)を返します。
def self.default_tcp_host
ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
end
IPv6が利用可能な環境では、静的な定数 DEFAULTS[:binds] と DEFAULTS[:tcp_host] も :: ベースの値で上書きされます。puma_default_options メソッドにて、DEFAULTS をdupした後に動的メソッドの結果で上書きする処理が加わっています:
def puma_default_options(env = ENV)
defaults = DEFAULTS.dup
defaults[:tcp_host] = self.class.default_tcp_host
defaults[:binds] = [self.class.default_tcp_bind]
puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
defaults
end
さらに、clamp メソッドの処理フロー末尾に rewrite_unavailable_ipv6_binds! が追加されました。これは、ユーザーが明示的に tcp://[::]:9292 を設定していたとしても、IPv6が利用できない環境では警告を出力しつつIPv4アドレスに書き換えるフォールバック機能です。lib/puma/cli.rb と lib/rack/handler/puma.rb では、ハードコードされていた Configuration::DEFAULTS[:tcp_host] の参照が Configuration.default_tcp_host の呼び出しに置き換えられています。
設計判断
静的な定数変更ではなく、実行時フォールバックを持つ動的判定が採用されています。
IPv6を単純にデフォルト化した場合、IPv6インターフェースを持たない古いシステムや特定のコンテナ環境でバインドに失敗するリスクがあります。Socket.getifaddrs による事前チェックと rewrite_unavailable_ipv6_binds! によるサイレント書き換えを組み合わせることで、IPv6環境では透過的に恩恵を受け、非IPv6環境では従来通りに動作するという二重の安全網が構築されています。
テストでは Socket.stub(:getifaddrs, []) を用いてIPv6なし環境と ipv6? && !ipv6_loopback? なアドレスを持つ環境の両方をシミュレートしており、各シナリオで期待するホストとバインドURIが設定されることを検証しています。また、test_pumactl.rb のテストでは "127.0.0.1" が "localhost" に変更されており、IPv6環境での接続確立の安定性を高める配慮も見られます。
まとめ
本PRは、単純な定数置き換えにとどまらず、実行時のインターフェース検査とフォールバック機構を組み合わせることで、環境の多様性に対応したIPv6デフォルト化を実現しています。:: が 0.0.0.0 のスーパーセットとして機能するという特性を活かし、IPv6対応環境では接続互換性を拡大しつつ、非対応環境では既存の動作を維持する設計は、エコシステム全体のIPv6移行を推進する実践的なアプローチです。