[インデックス 12370] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるDial
およびListen
APIの挙動を、異なるプラットフォーム間で一貫させることを目的としています。特に、"tcp6"
ネットワークタイプとIPv4またはIPv4-mapped IPv6アドレスの組み合わせに関する挙動の不一致を解消し、IPV6_V6ONLY
ソケットオプションの扱いを調整することで、より予測可能で堅牢なネットワーク操作を実現します。これにより、特定の不正なアドレス組み合わせは拒否されるようになります。また、ユニキャストリスナーのテストも改善されています。
コミット
commit b5dc8724cb1f13c4419641fd3b666ebd46408f21
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Tue Mar 6 00:13:10 2012 +0900
net: make Dial and Listen behavior consistent across over platforms
This CL changes the behavior of Dial and Listen API family.
Previous Dial and Listen allow a combo of "tcp6" and IPv4 or IPv6
IPv4-mapped address as its argument, but it also makes slightly
different behaviors between Linux and other platforms. This CL fixes
such differences across over platforms by tweaking IP-level socket
option IPV6_V6ONLY. Consequently new Dial and Listen API family will
reject arguments consists of "tcp6" and IPv4 or IPv6 IPv4-mapped
address.
This CL also adds a bit clarified unicast listener tests.
Fixes #2581.
R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/5677086
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b5dc8724cb1f13c4419641fd3b666ebd46408f21
元コミット内容
Goのnet
パッケージにおけるDial
およびListen
関数は、以前は"tcp6"
ネットワークタイプとIPv4アドレスまたはIPv4-mapped IPv6アドレスの組み合わせを引数として許可していました。しかし、この挙動はLinuxと他のプラットフォーム間で微妙な違いを生じさせていました。このコミットは、IPV6_V6ONLY
ソケットオプションを調整することで、これらのプラットフォーム間の差異を修正します。結果として、新しいDial
およびListen
APIは、"tcp6"
とIPv4またはIPv4-mapped IPv6アドレスの組み合わせを引数として拒否するようになります。また、ユニキャストリスナーのテストも明確化され、追加されています。この変更はIssue #2581を修正します。
変更の背景
この変更の主な背景は、Goのnet
パッケージが提供するDial
およびListen
といったネットワークAPIのプラットフォーム間での挙動の不一致でした。具体的には、以下の問題が挙げられます。
"tcp6"
とIPv4/IPv4-mappedアドレスの組み合わせの挙動の不一致: 以前のGoのネットワークAPIでは、"tcp6"
というネットワークタイプ(IPv6ソケットを使用することを示す)を指定しながら、IPv4アドレス(例:127.0.0.1
)やIPv4-mapped IPv6アドレス(例:::ffff:127.0.0.1
)を引数として渡すことが可能でした。しかし、この組み合わせがLinuxと他のオペレーティングシステム(BSD系など)で異なる挙動を示すことが判明しました。- 一部のプラットフォームでは、IPv6ソケットがIPv4トラフィックも処理できるように設定されている場合(
IPV6_V6ONLY
オプションが0に設定されている場合)、この組み合わせでも接続が確立できました。 - しかし、他のプラットフォームでは、この設定が異なるか、あるいはIPv6ソケットがIPv6トラフィックのみを処理するように厳密に設定されている(
IPV6_V6ONLY
が1に設定されている)ため、接続が失敗したり、予期せぬ挙動を示したりしました。 - 特に、Linuxでは
IPV6_V6ONLY
のデフォルト値が0であることが多く、IPv4-mappedアドレスを介してIPv4接続を受け入れることができましたが、他のシステムではそうではない場合がありました。
- 一部のプラットフォームでは、IPv6ソケットがIPv4トラフィックも処理できるように設定されている場合(
- APIの予測可能性の向上: 開発者にとって、同じコードが異なる環境で異なる挙動を示すことは、バグの原因となり、デバッグを困難にします。APIの挙動を一貫させることで、Goアプリケーションの移植性と信頼性を向上させる必要がありました。
IPV6_V6ONLY
オプションの適切な利用:IPV6_V6ONLY
ソケットオプションは、IPv6ソケットがIPv6アドレスのみをリッスンするか、それともIPv4-mapped IPv6アドレスを介してIPv4接続もリッスンするかを制御します。このオプションの適切な設定は、デュアルスタック環境でのネットワークプログラミングにおいて非常に重要です。このコミットは、このオプションをより意図的に、かつプラットフォーム間で一貫した方法で利用するように変更します。- Issue #2581の修正: このコミットは、GoのIssueトラッカーで報告されていた#2581の問題を直接修正します。これは、前述のプラットフォーム間の挙動の不一致に関する具体的なバグ報告であったと考えられます。
- テストの明確化: 挙動の変更に伴い、既存のテストが新しい挙動を正確に反映しているか、またエッジケースを適切にカバーしているかを確認する必要がありました。そのため、ユニキャストリスナーに関するテストが追加・修正されています。
これらの背景から、Goのネットワークスタックの堅牢性と移植性を高めるために、この変更が導入されました。
前提知識の解説
このコミットを理解するためには、以下のネットワークおよびソケットプログラミングに関する前提知識が必要です。
-
IPアドレスとアドレスファミリー (Address Family):
- IPv4 (Internet Protocol version 4):
192.168.1.1
のような32ビットのアドレス形式。 - IPv6 (Internet Protocol version 6):
2001:0db8:85a3:0000:0000:8a2e:0370:7334
のような128ビットのアドレス形式。 - IPv4-mapped IPv6アドレス: IPv6アドレス空間内でIPv4アドレスを表現するための特殊な形式。
::ffff:192.168.1.1
のように表記され、IPv6ソケットがIPv4トラフィックを処理できるようにするために使用されます。 - アドレスファミリー (AF_INET, AF_INET6): ソケットを作成する際に指定するプロトコルファミリー。
AF_INET
はIPv4、AF_INET6
はIPv6を指します。
- IPv4 (Internet Protocol version 4):
-
ソケット (Socket):
- ネットワーク通信のエンドポイント。アプリケーションがネットワークとやり取りするための抽象化されたインターフェースです。
Listen
: サーバー側で、特定のIPアドレスとポートで着信接続を待ち受けるための操作。Dial
: クライアント側で、特定のIPアドレスとポートを持つリモートホストへの接続を確立するための操作。
-
ソケットオプション (Socket Options):
- ソケットの挙動を制御するための設定。
setsockopt
システムコールなどを用いて設定します。 IPV6_V6ONLY
:AF_INET6
ソケットに特有のオプション。IPV6_V6ONLY=1
(true): ソケットはIPv6アドレスからの接続のみを受け入れます。IPv4-mapped IPv6アドレスを介したIPv4接続は拒否されます。IPV6_V6ONLY=0
(false): ソケットはIPv6アドレスからの接続に加えて、IPv4-mapped IPv6アドレスを介したIPv4接続も受け入れます。これにより、単一のIPv6ソケットでIPv4とIPv6の両方のトラフィックを処理する「デュアルスタック」挙動が可能になります。- このオプションのデフォルト値はOSによって異なり、またシステム全体の設定(例: Linuxの
net.ipv6.bindv6only
sysctl設定)によっても影響を受けます。OpenBSDのように、このオプションの変更を許可しないOSもあります。
- ソケットの挙動を制御するための設定。
-
ワイルドカードアドレス (Wildcard Address):
- 特定のIPアドレスではなく、「任意のアドレス」を意味するアドレス。
- IPv4では
0.0.0.0
、IPv6では::
(0:0:0:0:0:0:0:0
の短縮形)がこれに該当します。 - サーバーが複数のネットワークインターフェースを持つ場合に、どのインターフェースからの接続でも受け入れるために使用されます。
-
Go言語の
net
パッケージ:- Goの標準ライブラリで、ネットワークI/Oプリミティブを提供します。TCP/UDPクライアントおよびサーバーの実装、IPアドレスの解決などが可能です。
net.Listen(network, address)
: 指定されたネットワークとアドレスでリッスンを開始します。net.Dial(network, address)
: 指定されたネットワークとアドレスに接続を試みます。network
引数には、"tcp"
,"tcp4"
,"tcp6"
,"udp"
,"udp4"
,"udp6"
などが指定できます。"tcp"
や"udp"
は、システムがIPv4とIPv6のどちらを優先するか、またはデュアルスタック挙動を許可するかによって、内部的にAF_INET
またはAF_INET6
ソケットを選択します。"tcp4"
や"udp4"
は明示的にIPv4ソケット (AF_INET
) を使用します。"tcp6"
や"udp6"
は明示的にIPv6ソケット (AF_INET6
) を使用します。
これらの概念を理解することで、コミットがなぜ特定の変更を行ったのか、そしてそれがネットワーク通信の挙動にどのように影響するのかを深く把握できます。
技術的詳細
このコミットの技術的詳細は、主にGoのnet
パッケージがソケットを作成し、設定する際のロジック、特にIPV6_V6ONLY
ソケットオプションの扱いと、アドレスファミリーの選択ロジックの変更に集約されます。
-
IPV6_V6ONLY
の明示的な制御:- 以前は、
AF_INET6
ソケットを作成する際に、setDefaultSockopts
関数内で無条件にIPV6_V6ONLY
を0
(デュアルスタックモード)に設定しようとしていました。これは「OSのデフォルトがどうであれ、両方のIPバージョンを許可する」という意図でしたが、一部のOS(特にOpenBSD)ではこのオプションの設定が許可されておらず、またLinuxと他のプラットフォームで挙動の不一致を引き起こしていました。 - このコミットでは、
setDefaultSockopts
関数にipv6only
という新しいブール引数が追加されました。この引数がtrue
の場合、IPV6_V6ONLY
は1
に設定され、IPv6専用モードになります。false
の場合、以前と同様に0
に設定されます。 - これにより、GoのネットワークAPIは、特定のネットワークタイプ(例:
"tcp6"
)がIPv6専用の挙動を意図している場合に、明示的にIPV6_V6ONLY=1
を設定できるようになり、プラットフォーム間の挙動の一貫性を高めます。
- 以前は、
-
favoriteAddrFamily
関数の変更:- この関数は、与えられたネットワークタイプ(例:
"tcp"
,"tcp4"
,"tcp6"
)とローカルアドレス(laddr
)、リモートアドレス(raddr
)、およびモード("listen"
または"dial"
)に基づいて、使用すべきアドレスファミリー(AF_INET
またはAF_INET6
)を決定します。 - 変更点として、この関数は単にアドレスファミリーを返すだけでなく、
ipv6only
というブール値も返すようになりました。 - 特に
"listen"
モードでワイルドカードアドレス(0.0.0.0
や::
)が指定された場合のロジックが大幅に拡張されました。"tcp"
や"udp"
のような汎用ネットワークタイプでワイルドカードアドレスが指定され、かつプラットフォームがIPv6とIPv4-mapped IPv6アドレスの両方をサポートする場合(supportsIPv4map
がtrue
)、AF_INET6
ソケットとipv6only=false
(デュアルスタック)が選択されます。これにより、単一のIPv6ソケットでIPv4とIPv6の両方の接続を受け入れます。"tcp6"
や"udp6"
のように明示的にIPv6が指定された場合、AF_INET6
ソケットとipv6only=true
(IPv6専用)が選択されます。これにより、"tcp6"
を指定したにもかかわらずIPv4アドレスやIPv4-mappedアドレスで接続しようとする不正な組み合わせが拒否されるようになります。"tcp4"
や"udp4"
のように明示的にIPv4が指定された場合は、AF_INET
ソケットとipv6only=false
が選択されます(IPv4ソケットにはIPV6_V6ONLY
は適用されないため、false
は実質的に意味を持ちません)。
- この変更により、GoのネットワークAPIは、ユーザーが指定したネットワークタイプとアドレスに基づいて、ソケットのデュアルスタック挙動をより正確に制御できるようになりました。
- この関数は、与えられたネットワークタイプ(例:
-
socket
関数の変更:- ソケットを作成する主要な内部関数である
socket
のシグネチャに、ipv6only
という新しい引数が追加されました。 - この
ipv6only
引数は、favoriteAddrFamily
から返された値がそのまま渡され、setDefaultSockopts
関数に引き渡されます。
- ソケットを作成する主要な内部関数である
-
sockaddr
インターフェースの拡張:sockaddr
インターフェースにisWildcard() bool
メソッドが追加されました。これは、アドレスがワイルドカードアドレス(0.0.0.0
や::
)であるかどうかを判定するために使用されます。IPAddr
,TCPAddr
,UDPAddr
構造体にisWildcard()
メソッドが実装され、IP
フィールドがnil
であるか、またはIsUnspecified()
(0.0.0.0
や::
を判定する)である場合にtrue
を返します。
-
テストの強化:
unicast_test.go
ファイルが大幅に拡張され、TestTCPListener
,TestUDPListener
,TestSimpleTCPListener
,TestSimpleUDPListener
,TestDualStackTCPListener
,TestDualStackUDPListener
,TestProhibitionaryDialArgs
といった新しいテスト関数が追加されました。- これらのテストは、異なるネットワークタイプ、アドレス、およびプラットフォーム設定(特に
IPV6_V6ONLY
の挙動)の下でのListen
およびDial
の挙動を検証します。 - 特に
TestProhibitionaryDialArgs
は、"tcp6"
とIPv4アドレスまたはIPv4-mappedアドレスの組み合わせが拒否されることを明示的にテストします。 usableLocalPort
ヘルパー関数が追加され、テスト中に利用可能なローカルポートを動的に取得できるようになりました。avoidOSXFirewallDialogPopup
変数が追加され、OS X(macOS)でワイルドカードリッスン時にファイアウォールダイアログがポップアップするのを避けるためのロジックが導入されました。
これらの変更により、GoのネットワークAPIは、より予測可能で、プラットフォーム間で一貫した挙動を提供するようになり、特にIPv6とIPv4の相互運用性に関する問題が解決されました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルと関数に集中しています。
-
src/pkg/net/ipsock_posix.go
:favoriteAddrFamily
関数のシグネチャが変更され、int
だけでなくipv6only bool
も返すようになりました。favoriteAddrFamily
内のロジックが大幅に拡張され、"listen"
モードでのワイルドカードアドレスの扱いと、ipv6only
の決定ロジックが追加されました。internetSocket
関数でfavoriteAddrFamily
の戻り値がfamily, ipv6only
として受け取られ、socket
関数に渡されるようになりました。sockaddr
インターフェースにisWildcard() bool
メソッドが追加されました。ipToSockaddr
関数内で、IPv4の0.0.0.0
がIPv6モードで::
(unspecified address)に書き換えられるコメントが修正されました。
-
src/pkg/net/sock.go
:socket
関数のシグネチャにipv6only bool
引数が追加されました。setDefaultSockopts
関数の呼び出しにipv6only
引数が追加されました。
-
src/pkg/net/sockopt_bsd.go
,src/pkg/net/sockopt_linux.go
,src/pkg/net/sockopt_windows.go
:setDefaultSockopts
関数のシグネチャにipv6only bool
引数が追加されました。syscall.AF_INET6
の場合のIPV6_V6ONLY
ソケットオプションの設定ロジックが変更され、ipv6only
引数の値に基づいて1
または0
が設定されるようになりました。
-
src/pkg/net/iprawsock_posix.go
,src/pkg/net/tcpsock_posix.go
,src/pkg/net/udpsock_posix.go
:IPAddr
,TCPAddr
,UDPAddr
構造体にisWildcard() bool
メソッドが追加されました。
-
src/pkg/net/unicast_test.go
:- テスト構造が大幅に再編成され、
listenerTests
やdualStackListenerTests
といった新しいテストデータ構造が導入されました。 TestTCPListener
,TestUDPListener
,TestSimpleTCPListener
,TestSimpleUDPListener
,TestDualStackTCPListener
,TestDualStackUDPListener
,TestProhibitionaryDialArgs
など、多数の新しいテスト関数が追加されました。usableLocalPort
,differentWildcardAddr
,checkFirstListener
,checkSecondListener
,checkDualStackSecondListener
,checkDualStackAddrFamily
といったヘルパー関数が追加されました。- 既存の
TestUnicastTCPAndUDP
関数が削除され、より詳細な新しいテストに置き換えられました。
- テスト構造が大幅に再編成され、
-
src/pkg/net/file_test.go
,src/pkg/net/net_test.go
,src/pkg/net/server_test.go
:- 既存のテストケースが、新しい
IPV6_V6ONLY
の挙動に合わせて調整されました。特に、supportsIPv4map
の条件付きで実行されていた一部のテストが削除または変更され、より厳密な挙動が期待されるようになりました。 net_test.go
にavoidOSXFirewallDialogPopup
変数が追加されました。
- 既存のテストケースが、新しい
これらの変更は、Goのネットワークスタックの基盤となる部分に影響を与え、ソケットの作成と設定、特にIPv6のデュアルスタック挙動の制御方法を根本的に変更しています。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、主にfavoriteAddrFamily
関数のロジック拡張と、socket
およびsetDefaultSockopts
関数へのipv6only
引数の導入にあります。
favoriteAddrFamily
関数の変更 (src/pkg/net/ipsock_posix.go
)
以前のfavoriteAddrFamily
は、ネットワークタイプとアドレスに基づいて単一のアドレスファミリー(AF_INET
またはAF_INET6
)を返していました。変更後、この関数はfamily int, ipv6only bool
の2つの値を返すようになりました。
// 変更前
// func favoriteAddrFamily(net string, laddr, raddr sockaddr, mode string) int {
// 変更後
func favoriteAddrFamily(net string, laddr, raddr sockaddr, mode string) (family int, ipv6only bool) {
この変更の最も重要な部分は、"listen"
モードでワイルドカードアドレスが指定された場合のロジックです。
if mode == "listen" && laddr.isWildcard() {
if supportsIPv4map {
return syscall.AF_INET6, false // AF_INET6ソケットでIPV6_V6ONLY=0 (デュアルスタック)
}
return laddr.family(), false // laddrのファミリーを使用し、IPV6_V6ONLY=0 (デュアルスタック)
}
laddr.isWildcard()
: 新しく追加されたメソッドで、ローカルアドレスがワイルドカード(0.0.0.0
や::
)であるかを判定します。supportsIPv4map
: システムがIPv6のIPv4-mappedアドレスをサポートしているかを示すグローバル変数。- もしシステムがIPv4-mappedアドレスをサポートしている場合、
AF_INET6
ソケットを使用し、ipv6only
をfalse
に設定します。これは、単一のIPv6ソケットでIPv4とIPv6の両方の接続を受け入れる「デュアルスタック」挙動を意図しています。 - サポートしていない場合、
laddr
のアドレスファミリー(通常はAF_INET
)を使用し、ipv6only
はfalse
です。
また、ネットワークタイプが明示的に"tcp4"
や"tcp6"
の場合の挙動も明確化されました。
switch net[len(net)-1] {
case '4':
return syscall.AF_INET, false // IPv4専用、IPV6_V6ONLYは関係なし
case '6':
return syscall.AF_INET6, true // IPv6専用、IPV6_V6ONLY=1
}
"tcp4"
や"udp4"
の場合、AF_INET
が選択され、ipv6only
はfalse
です(IPv4ソケットにはIPV6_V6ONLY
は適用されないため)。"tcp6"
や"udp6"
の場合、AF_INET6
が選択され、ipv6only
はtrue
です。これにより、"tcp6"
を指定した場合はIPv6専用のソケットが作成され、IPv4アドレスやIPv4-mappedアドレスでの接続は拒否されるようになります。
socket
関数の変更 (src/pkg/net/sock.go
)
socket
関数は、ソケットを作成し、デフォルトのソケットオプションを設定する役割を担います。この関数のシグネチャにipv6only bool
が追加されました。
// 変更前
// func socket(net string, f, t, p int, la, ra syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) {
// 変更後
func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) {
このipv6only
引数は、internetSocket
関数からfavoriteAddrFamily
の戻り値として渡され、さらにsetDefaultSockopts
関数に引き渡されます。
// ...
fd, err = socket(net, family, sotype, proto, ipv6only, la, ra, toAddr)
// ...
setDefaultSockopts
関数の変更 (src/pkg/net/sockopt_bsd.go
, src/pkg/net/sockopt_linux.go
, src/pkg/net/sockopt_windows.go
)
この関数は、ソケット作成後にプラットフォーム固有のデフォルトオプションを設定します。ここでもipv6only bool
引数が追加されました。
// 変更前
// func setDefaultSockopts(s, f, t int) error {
// 変更後 (例: sockopt_linux.go)
func setDefaultSockopts(s, f, t int, ipv6only bool) error {
switch f {
case syscall.AF_INET6:
if ipv6only {
syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, 1)
} else {
// Allow both IP versions even if the OS default
// is otherwise. Note that some operating systems
// never admit this option.
syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, 0)
}
}
// ...
}
syscall.AF_INET6
ソケットの場合、ipv6only
がtrue
であればIPV6_V6ONLY
を1
に設定し、IPv6専用モードを強制します。ipv6only
がfalse
であれば、IPV6_V6ONLY
を0
に設定し、デュアルスタックモードを許可します。これにより、"tcp"
のような汎用ネットワークタイプでワイルドカードアドレスを指定した場合に、IPv4とIPv6の両方の接続を受け入れることができます。
isWildcard()
メソッドの追加 (src/pkg/net/iprawsock_posix.go
, src/pkg/net/tcpsock_posix.go
, src/pkg/net/udpsock_posix.go
)
IPAddr
, TCPAddr
, UDPAddr
構造体にisWildcard()
メソッドが追加され、sockaddr
インターフェースに組み込まれました。
func (a *IPAddr) isWildcard() bool {
if a == nil || a.IP == nil {
return true // アドレスがnilの場合もワイルドカードとみなす
}
return a.IP.IsUnspecified() // IPが0.0.0.0や::であるかを判定
}
このメソッドは、favoriteAddrFamily
関数内で、リスニングアドレスがワイルドカードであるかを効率的に判定するために使用されます。
テストの変更 (src/pkg/net/unicast_test.go
)
unicast_test.go
は、これらの変更が正しく機能することを検証するために大幅に書き換えられました。特に注目すべきは、TestProhibitionaryDialArgs
です。
func TestProhibitionaryDialArgs(t *testing.T) {
// ...
// This test requires both IPv6 and IPv6 IPv4-mapping functionality.
if !supportsIPv4map || avoidOSXFirewallDialogPopup() {
return
}
port := usableLocalPort(t, "tcp", "[::]")
l, err := Listen("tcp", "[::]"+":"+port) // IPv6デュアルスタックリスナーを作成
if err != nil {
t.Fatalf("Listen failed: %v", err)
}
defer l.Close()
for _, tt := range prohibitionaryDialArgTests {
_, err = Dial(tt.net, tt.addr+":"+port) // "tcp6"とIPv4/IPv4-mappedアドレスでDialを試みる
if err == nil {
t.Fatal("Dial(%q, %q) should fail", tt.net, tt.addr) // 失敗することを期待
}
}
}
このテストは、"tcp6"
ネットワークタイプとIPv4アドレス(127.0.0.1
)またはIPv4-mapped IPv6アドレス(::ffff:127.0.0.1
)の組み合わせでDial
を試みた場合に、エラーが発生すること(接続が拒否されること)を検証します。これは、favoriteAddrFamily
が"tcp6"
に対してipv6only=true
を返し、その結果IPV6_V6ONLY=1
が設定されたソケットが作成されるため、IPv4トラフィックが拒否されるという新しい挙動を反映しています。
これらの変更により、GoのネットワークAPIは、ユーザーが指定したネットワークタイプとアドレスに基づいて、ソケットのデュアルスタック挙動をより正確に制御し、プラットフォーム間の挙動の不一致を解消しています。
関連リンク
- Go Issue #2581: net: make Dial and Listen behavior consistent across over platforms
- Go CL 5677086: net: make Dial and Listen behavior consistent across over platforms