Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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のプラットフォーム間での挙動の不一致でした。具体的には、以下の問題が挙げられます。

  1. "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接続を受け入れることができましたが、他のシステムではそうではない場合がありました。
  2. APIの予測可能性の向上: 開発者にとって、同じコードが異なる環境で異なる挙動を示すことは、バグの原因となり、デバッグを困難にします。APIの挙動を一貫させることで、Goアプリケーションの移植性と信頼性を向上させる必要がありました。
  3. IPV6_V6ONLYオプションの適切な利用: IPV6_V6ONLYソケットオプションは、IPv6ソケットがIPv6アドレスのみをリッスンするか、それともIPv4-mapped IPv6アドレスを介してIPv4接続もリッスンするかを制御します。このオプションの適切な設定は、デュアルスタック環境でのネットワークプログラミングにおいて非常に重要です。このコミットは、このオプションをより意図的に、かつプラットフォーム間で一貫した方法で利用するように変更します。
  4. Issue #2581の修正: このコミットは、GoのIssueトラッカーで報告されていた#2581の問題を直接修正します。これは、前述のプラットフォーム間の挙動の不一致に関する具体的なバグ報告であったと考えられます。
  5. テストの明確化: 挙動の変更に伴い、既存のテストが新しい挙動を正確に反映しているか、またエッジケースを適切にカバーしているかを確認する必要がありました。そのため、ユニキャストリスナーに関するテストが追加・修正されています。

これらの背景から、Goのネットワークスタックの堅牢性と移植性を高めるために、この変更が導入されました。

前提知識の解説

このコミットを理解するためには、以下のネットワークおよびソケットプログラミングに関する前提知識が必要です。

  1. 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を指します。
  2. ソケット (Socket):

    • ネットワーク通信のエンドポイント。アプリケーションがネットワークとやり取りするための抽象化されたインターフェースです。
    • Listen: サーバー側で、特定のIPアドレスとポートで着信接続を待ち受けるための操作。
    • Dial: クライアント側で、特定のIPアドレスとポートを持つリモートホストへの接続を確立するための操作。
  3. ソケットオプション (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もあります。
  4. ワイルドカードアドレス (Wildcard Address):

    • 特定のIPアドレスではなく、「任意のアドレス」を意味するアドレス。
    • IPv4では0.0.0.0、IPv6では::0:0:0:0:0:0:0:0の短縮形)がこれに該当します。
    • サーバーが複数のネットワークインターフェースを持つ場合に、どのインターフェースからの接続でも受け入れるために使用されます。
  5. 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ソケットオプションの扱いと、アドレスファミリーの選択ロジックの変更に集約されます。

  1. IPV6_V6ONLYの明示的な制御:

    • 以前は、AF_INET6ソケットを作成する際に、setDefaultSockopts関数内で無条件にIPV6_V6ONLY0(デュアルスタックモード)に設定しようとしていました。これは「OSのデフォルトがどうであれ、両方のIPバージョンを許可する」という意図でしたが、一部のOS(特にOpenBSD)ではこのオプションの設定が許可されておらず、またLinuxと他のプラットフォームで挙動の不一致を引き起こしていました。
    • このコミットでは、setDefaultSockopts関数にipv6onlyという新しいブール引数が追加されました。この引数がtrueの場合、IPV6_V6ONLY1に設定され、IPv6専用モードになります。falseの場合、以前と同様に0に設定されます。
    • これにより、GoのネットワークAPIは、特定のネットワークタイプ(例: "tcp6")がIPv6専用の挙動を意図している場合に、明示的にIPV6_V6ONLY=1を設定できるようになり、プラットフォーム間の挙動の一貫性を高めます。
  2. favoriteAddrFamily関数の変更:

    • この関数は、与えられたネットワークタイプ(例: "tcp", "tcp4", "tcp6")とローカルアドレス(laddr)、リモートアドレス(raddr)、およびモード("listen"または"dial")に基づいて、使用すべきアドレスファミリー(AF_INETまたはAF_INET6)を決定します。
    • 変更点として、この関数は単にアドレスファミリーを返すだけでなく、ipv6onlyというブール値も返すようになりました。
    • 特に"listen"モードでワイルドカードアドレス(0.0.0.0::)が指定された場合のロジックが大幅に拡張されました。
      • "tcp""udp"のような汎用ネットワークタイプでワイルドカードアドレスが指定され、かつプラットフォームがIPv6とIPv4-mapped IPv6アドレスの両方をサポートする場合(supportsIPv4maptrue)、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は、ユーザーが指定したネットワークタイプとアドレスに基づいて、ソケットのデュアルスタック挙動をより正確に制御できるようになりました。
  3. socket関数の変更:

    • ソケットを作成する主要な内部関数であるsocketのシグネチャに、ipv6onlyという新しい引数が追加されました。
    • このipv6only引数は、favoriteAddrFamilyから返された値がそのまま渡され、setDefaultSockopts関数に引き渡されます。
  4. sockaddrインターフェースの拡張:

    • sockaddrインターフェースにisWildcard() boolメソッドが追加されました。これは、アドレスがワイルドカードアドレス(0.0.0.0::)であるかどうかを判定するために使用されます。
    • IPAddr, TCPAddr, UDPAddr構造体にisWildcard()メソッドが実装され、IPフィールドがnilであるか、またはIsUnspecified()0.0.0.0::を判定する)である場合にtrueを返します。
  5. テストの強化:

    • 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の相互運用性に関する問題が解決されました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、以下のファイルと関数に集中しています。

  1. 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)に書き換えられるコメントが修正されました。
  2. src/pkg/net/sock.go:

    • socket関数のシグネチャにipv6only bool引数が追加されました。
    • setDefaultSockopts関数の呼び出しにipv6only引数が追加されました。
  3. 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が設定されるようになりました。
  4. src/pkg/net/iprawsock_posix.go, src/pkg/net/tcpsock_posix.go, src/pkg/net/udpsock_posix.go:

    • IPAddr, TCPAddr, UDPAddr構造体にisWildcard() boolメソッドが追加されました。
  5. src/pkg/net/unicast_test.go:

    • テスト構造が大幅に再編成され、listenerTestsdualStackListenerTestsといった新しいテストデータ構造が導入されました。
    • TestTCPListener, TestUDPListener, TestSimpleTCPListener, TestSimpleUDPListener, TestDualStackTCPListener, TestDualStackUDPListener, TestProhibitionaryDialArgsなど、多数の新しいテスト関数が追加されました。
    • usableLocalPort, differentWildcardAddr, checkFirstListener, checkSecondListener, checkDualStackSecondListener, checkDualStackAddrFamilyといったヘルパー関数が追加されました。
    • 既存のTestUnicastTCPAndUDP関数が削除され、より詳細な新しいテストに置き換えられました。
  6. src/pkg/net/file_test.go, src/pkg/net/net_test.go, src/pkg/net/server_test.go:

    • 既存のテストケースが、新しいIPV6_V6ONLYの挙動に合わせて調整されました。特に、supportsIPv4mapの条件付きで実行されていた一部のテストが削除または変更され、より厳密な挙動が期待されるようになりました。
    • net_test.goavoidOSXFirewallDialogPopup変数が追加されました。

これらの変更は、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ソケットを使用し、ipv6onlyfalseに設定します。これは、単一のIPv6ソケットでIPv4とIPv6の両方の接続を受け入れる「デュアルスタック」挙動を意図しています。
  • サポートしていない場合、laddrのアドレスファミリー(通常はAF_INET)を使用し、ipv6onlyfalseです。

また、ネットワークタイプが明示的に"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が選択され、ipv6onlyfalseです(IPv4ソケットにはIPV6_V6ONLYは適用されないため)。
  • "tcp6""udp6"の場合、AF_INET6が選択され、ipv6onlytrueです。これにより、"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ソケットの場合、ipv6onlytrueであればIPV6_V6ONLY1に設定し、IPv6専用モードを強制します。
  • ipv6onlyfalseであれば、IPV6_V6ONLY0に設定し、デュアルスタックモードを許可します。これにより、"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は、ユーザーが指定したネットワークタイプとアドレスに基づいて、ソケットのデュアルスタック挙動をより正確に制御し、プラットフォーム間の挙動の不一致を解消しています。

関連リンク

参考にした情報源リンク