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

[インデックス 11898] ファイルの概要

このコミットは、Go言語の標準ライブラリである net パッケージにおけるネットワークリスニングの挙動を修正するものです。具体的には、アドレスが指定されずにポートのみが指定された場合(例: ":80")、IPv6リスニングが優先される現在の挙動を改め、IPv4リスニングを優先するように変更します。これにより、net.Listen("tcp", ":80")http.ListenAndServe(":80") のような呼び出しが、意図せずIPv6ソケットを優先的に作成してしまう問題を回避します。

コミット

commit 6fbe80572e96745fc360c74f2f1c6e878afe436b
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Wed Feb 15 01:59:18 2012 +0900

    net: prefer an IPv4 listen if no address given
    
    This CL avoids net.Listen("tcp", ":80"), http.ListenAdnServe(":80")
    prefer an IPv6 listen.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5669043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6fbe80572e96745fc360c74f2f1c6e878afe436b

元コミット内容

net: prefer an IPv4 listen if no address given

This CL avoids net.Listen("tcp", ":80"), http.ListenAdnServe(":80")
prefer an IPv6 listen.

R=rsc
CC=golang-dev
https://golang.org/cl/5669043

変更の背景

Go言語の net パッケージでは、net.Listen("tcp", ":80") のようにリスニングアドレスとしてポート番号のみを指定した場合、システムがIPv6をサポートしている環境では、デフォルトでIPv6ソケットが作成され、IPv6アドレス ([::]) にバインドされる挙動がありました。

しかし、多くのシステム、特に古いシステムや特定のネットワーク構成では、IPv6が完全にサポートされていない、またはIPv4とのデュアルスタック運用が適切に設定されていない場合があります。このような状況でIPv6ソケットが優先されると、以下のような問題が発生する可能性がありました。

  1. IPv4クライアントからの接続不可: IPv6ソケットが IPV6_V6ONLY オプション(後述)を有効にして作成された場合、そのソケットはIPv6接続のみを受け付け、IPv4クライアントからの接続を受け付けなくなります。これにより、アプリケーションがIPv4ネットワーク上のクライアントからアクセスできなくなる問題が生じます。
  2. 予期せぬ挙動: 開発者や運用者がIPv4でのリスニングを期待しているにもかかわらず、システムが自動的にIPv6を優先することで、予期せぬネットワーク挙動や接続の問題が発生する可能性があります。
  3. 互換性の問題: 既存のIPv4ベースのインフラストラクチャやツールとの互換性が損なわれる可能性があります。

このコミットは、このような問題を解決するため、アドレスが指定されない場合にIPv4リスニングを優先するよう net パッケージの挙動を変更することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のネットワークおよびGo言語の概念を理解しておく必要があります。

1. IPv4とIPv6

  • IPv4 (Internet Protocol version 4): 現在インターネットで最も広く使用されているIPアドレスのバージョンです。32ビットのアドレス空間を持ち、192.168.1.1 のような形式で表現されます。アドレス枯渇の問題が指摘されています。
  • IPv6 (Internet Protocol version 6): IPv4の後継として開発されたIPアドレスのバージョンです。128ビットのアドレス空間を持ち、2001:0db8:85a3:0000:0000:8a2e:0370:7334 のような形式で表現されます。IPv4のアドレス枯渇問題を解決し、より多くのデバイスをインターネットに接続できるように設計されています。

2. ワイルドカードアドレス

ネットワークプログラミングにおいて、特定のIPアドレスではなく、利用可能なすべてのネットワークインターフェースからの接続を受け付けるために使用される特殊なアドレスです。

  • IPv4ワイルドカードアドレス: 0.0.0.0。これは、システム上のすべてのIPv4インターフェースからの接続を受け入れることを意味します。
  • IPv6ワイルドカードアドレス: [::]。これは、システム上のすべてのIPv6インターフェースからの接続を受け入れることを意味します。

3. net.Listenhttp.ListenAndServe

  • net.Listen(network, address string): Go言語の net パッケージで提供される関数で、指定されたネットワークアドレスでリッスンを開始するために使用されます。network"tcp", "tcp4", "tcp6", "udp", "unix" など、address":8080", "127.0.0.1:80", "[::1]:80" などです。
  • http.ListenAndServe(addr string, handler Handler): Go言語の net/http パッケージで提供される関数で、HTTPサーバーを起動し、指定されたアドレスでリッスンを開始します。内部的には net.Listen を使用しています。

4. ソケットアドレスファミリー (AF_INET, AF_INET6)

ソケットプログラミングにおいて、ソケットが使用するアドレスの種類(プロトコルファミリー)を指定します。

  • syscall.AF_INET: IPv4アドレスファミリーを示します。
  • syscall.AF_INET6: IPv6アドレスファミリーを示します。

5. IPV6_V6ONLY ソケットオプション

これはIPv6ソケットに設定できるオプションで、その挙動を制御します。

  • IPV6_V6ONLY が有効な場合: IPv6ソケットはIPv6接続のみを受け付けます。IPv4-mapped IPv6アドレス(IPv4アドレスをIPv6形式で表現したもの)を介したIPv4接続は受け付けません。
  • IPV6_V6ONLY が無効な場合: IPv6ソケットはIPv6接続と、IPv4-mapped IPv6アドレスを介したIPv4接続の両方を受け付けます。これにより、単一のIPv6ソケットでIPv4とIPv6の両方のトラフィックを処理できる「デュアルスタック」挙動が可能になります。

多くのオペレーティングシステムでは、デフォルトで IPV6_V6ONLY が有効になっています。これは、IPv4とIPv6の挙動を明確に分離し、意図しないデュアルスタック挙動を防ぐためです。しかし、これにより、アドレスを指定せずにIPv6ソケットが作成された場合に、IPv4クライアントからの接続ができないという問題が発生することがあります。

6. supportsIPv4map

このコミットで関連する概念として、システムがIPv4-mapped IPv6アドレスをサポートしているかどうかのフラグがあります。これは、IPv6ソケットがIPv4接続も処理できるデュアルスタックモードをサポートしているかを示します。

技術的詳細

このコミットは、主に src/pkg/net/ipsock_posix.go ファイル内の favoriteAddrFamily 関数と、src/pkg/net/dial.goListen 関数、そしてテストファイル src/pkg/net/server_test.go に変更を加えています。

src/pkg/net/dial.go の変更

Listen 関数内で、ListenTCP を呼び出す際の net 引数が afnet から net に変更されています。これは、Listen 関数に渡された元のネットワークタイプ(例: "tcp", "tcp4", "tcp6") を ListenTCP に正確に渡すための修正です。これにより、ListenTCP が適切なアドレスファミリーを選択する際のヒントとして利用されます。

--- a/src/pkg/net/dial.go
+++ b/src/pkg/net/dial.go
@@ -185,7 +185,7 @@ func Listen(net, laddr string) (Listener, error) {
 		if a != nil {
 			la = a.(*TCPAddr)
 		}
-		return ListenTCP(afnet, la)
+		return ListenTCP(net, la)
 	case "unix", "unixpacket":
 		var la *UnixAddr
 		if a != nil {

src/pkg/net/ipsock_posix.go の変更

このファイルは、POSIX互換システム(Linux, macOSなど)におけるIPソケットの挙動を定義しています。主要な変更は favoriteAddrFamily 関数にあります。

favoriteAddrFamily 関数は、与えられたネットワークタイプ、ローカルアドレス、リモートアドレス、およびモード("listen" またはそれ以外)に基づいて、最適なアドレスファミリー(IPv4またはIPv6)を決定します。

変更前は、リスニングモード (mode == "listen") で、かつローカルアドレスのIPがnil(つまり、ワイルドカードアドレスが指定されている場合)で、システムがIPv6をサポートしている場合、無条件に syscall.AF_INET6 (IPv6) を返していました。

// 変更前 (抜粋)
if mode == "listen" {
    switch a := laddr.(type) {
    case *TCPAddr:
        if a.IP == nil && supportsIPv6 {
            return syscall.AF_INET6
        }
    // ... 他のAddrタイプも同様
}

この挙動が、アドレスが指定されない場合にIPv6リスニングを優先する原因となっていました。

変更後、この条件に && supportsIPv4map が追加されました。

--- a/src/pkg/net/ipsock_posix.go
+++ b/src/pkg/net/ipsock_posix.go
@@ -53,13 +53,13 @@ func probeIPv6Stack() (supportsIPv6, supportsIPv4map bool) {\n }\n \n // favoriteAddrFamily returns the appropriate address family to\n-// the given net, raddr, laddr and mode.  At first it figures\n+// the given net, laddr, raddr and mode.  At first it figures\n // address family out from the net.  If mode indicates \"listen\"\n // and laddr.(type).IP is nil, it assumes that the user wants to\n // make a passive connection with wildcard address family, both\n // INET and INET6, and wildcard address.  Otherwise guess: if the\n // addresses are IPv4 then returns INET, or else returns INET6.\n-func favoriteAddrFamily(net string, raddr, laddr sockaddr, mode string) int {\n+func favoriteAddrFamily(net string, laddr, raddr sockaddr, mode string) int {\n \tswitch net[len(net)-1] {\n \tcase \'4\':\n \t\treturn syscall.AF_INET\n@@ -68,17 +68,20 @@ func favoriteAddrFamily(net string, raddr, laddr sockaddr, mode string) int {\n \t}\n \n \tif mode == \"listen\" {\n+\t\t// Note that OpenBSD allows neither \"net.inet6.ip6.v6only\"\n+\t\t// change nor IPPROTO_IPV6 level IPV6_V6ONLY socket option\n+\t\t// setting.\n \t\tswitch a := laddr.(type) {\n \t\tcase *TCPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\tcase *UDPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\tcase *IPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\t}\n@@ -104,7 +107,7 @@ type sockaddr interface {\n func internetSocket(net string, laddr, raddr sockaddr, sotype, proto int, mode string, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) {\n \tvar oserr error\n \tvar la, ra syscall.Sockaddr\n-\tfamily := favoriteAddrFamily(net, raddr, laddr, mode)\n+\tfamily := favoriteAddrFamily(net, laddr, raddr, mode)\n \tif laddr != nil {\n \t\tif la, oserr = laddr.sockaddr(family); oserr != nil {\n \t\t\tgoto Error\n```

この `supportsIPv4map` の追加により、システムがIPv6をサポートしているだけでなく、IPv4-mapped IPv6アドレスを介したIPv4接続も処理できる場合にのみ、IPv6が優先されるようになります。もし `supportsIPv4map` が `false` の場合(例えば、`IPV6_V6ONLY` がデフォルトで有効になっているシステムで、かつデュアルスタック挙動が期待されない場合)、IPv6は優先されず、結果としてIPv4が選択される可能性が高まります。

また、`favoriteAddrFamily` 関数の引数の順序が `(net string, raddr, laddr sockaddr, mode string)` から `(net string, laddr, raddr sockaddr, mode string)` に変更され、それに伴い呼び出し元 (`internetSocket` 関数) の引数も修正されています。これは、引数の意味をより明確にするためのリファクタリングと考えられます。

### `src/pkg/net/server_test.go` の変更

テストファイルでは、`TestTCPServer` 関数からOpenBSD固有の条件分岐が削除されています。

```diff
--- a/src/pkg/net/server_test.go
+++ b/src/pkg/net/server_test.go
@@ -115,16 +115,13 @@ func doTest(t *testing.T, network, listenaddr, dialaddr string) {\n }\n \n func TestTCPServer(t *testing.T) {\n-\tif runtime.GOOS != \"openbsd\" {\n-\t\tdoTest(t, \"tcp\", \"\", \"127.0.0.1\")\n-\t}\n+\tdoTest(t, \"tcp\", \"\", \"127.0.0.1\")\n \tdoTest(t, \"tcp\", \"0.0.0.0\", \"127.0.0.1\")\n \tdoTest(t, \"tcp\", \"127.0.0.1\", \"127.0.0.1\")\n \tdoTest(t, \"tcp4\", \"\", \"127.0.0.1\")\n \tdoTest(t, \"tcp4\", \"0.0.0.0\", \"127.0.0.1\")\n \tdoTest(t, \"tcp4\", \"127.0.0.1\", \"127.0.0.1\")\n \tif supportsIPv6 {\n-\t\tdoTest(t, \"tcp\", \"\", \"[::1]\")\n \t\tdoTest(t, \"tcp\", \"[::]\", \"[::1]\")\n \t\tdoTest(t, \"tcp\", \"[::1]\", \"[::1]\")\n \t\tdoTest(t, \"tcp6\", \"\", \"[::1]\")

また、supportsIPv6 の条件分岐内から doTest(t, "tcp", "", "[::1]") のテストケースが削除されています。これは、アドレスが指定されない場合のIPv6リスニングの優先順位が変更されたため、このテストケースがもはや適切ではないか、または新しい挙動を反映していないためと考えられます。

OpenBSDに関するコメントが ipsock_posix.go に追加されており、OpenBSDでは net.inet6.ip6.v6only の変更や IPV6_V6ONLY ソケットオプションの設定ができないことが示唆されています。これは、OSごとのネットワークスタックの挙動の違いを考慮していることを示しています。

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

src/pkg/net/ipsock_posix.gofavoriteAddrFamily 関数内の以下の行がコアとなる変更です。

--- a/src/pkg/net/ipsock_posix.go
+++ b/src/pkg/net/ipsock_posix.go
@@ -68,17 +68,20 @@ func favoriteAddrFamily(net string, raddr, laddr sockaddr, mode string) int {\n \t}\n \n \tif mode == "listen" {\n+\t\t// Note that OpenBSD allows neither "net.inet6.ip6.v6only"\n+\t\t// change nor IPPROTO_IPV6 level IPV6_V6ONLY socket option\n+\t\t// setting.\n \t\tswitch a := laddr.(type) {\n \t\tcase *TCPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\tcase *UDPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\tcase *IPAddr:\n-\t\t\tif a.IP == nil && supportsIPv6 {\n+\t\t\tif a.IP == nil && supportsIPv6 && supportsIPv4map {\n \t\t\t\treturn syscall.AF_INET6\n \t\t\t}\n \t\t}\

具体的には、if a.IP == nil && supportsIPv6 の条件に && supportsIPv4map が追加されました。

コアとなるコードの解説

favoriteAddrFamily 関数は、ネットワークリスニングを行う際に、どのIPアドレスファミリー(IPv4またはIPv6)を使用すべきかを決定する重要なロジックを含んでいます。

変更前のコードでは、以下の条件がすべて真の場合にIPv6アドレスファミリー (syscall.AF_INET6) が選択されていました。

  1. mode == "listen": リスニングモードであること。
  2. laddr.IP == nil: ローカルアドレスのIPが指定されていない(つまり、":80" のようにポートのみが指定されている)こと。これは、ワイルドカードアドレスでのリスニングを意味します。
  3. supportsIPv6: システムがIPv6をサポートしていること。

この条件により、IPv6が利用可能なシステムでは、アドレスが指定されない場合にIPv6ソケットが優先的に作成されていました。

変更後のコードでは、上記の条件に加えて、supportsIPv4map が真であることも求められるようになりました。

  • supportsIPv4map が真の場合: システムがIPv4-mapped IPv6アドレスをサポートしており、IPv6ソケットがIPv4接続も処理できるデュアルスタックモードで動作可能であることを意味します。この場合、IPv6ソケットを作成してもIPv4クライアントからの接続も受け付けられるため、IPv6を優先しても問題がないと判断されます。
  • supportsIPv4map が偽の場合: システムがIPv4-mapped IPv6アドレスをサポートしていないか、または IPV6_V6ONLY オプションが有効になっているため、IPv6ソケットがIPv6接続のみを受け付けることを意味します。この場合、IPv6を優先するとIPv4クライアントからの接続ができなくなる可能性があるため、IPv6は優先されず、結果としてIPv4アドレスファミリーが選択されることになります。

この変更により、Goの net パッケージは、アドレスが指定されないリスニングにおいて、より賢明なアドレスファミリーの選択を行うようになります。特に、IPV6_V6ONLY がデフォルトで有効になっているシステムにおいて、開発者が明示的にIPv6アドレスを指定しない限り、IPv4リスニングが優先されることで、互換性の問題や予期せぬ接続不可の問題を回避できるようになります。

関連リンク

参考にした情報源リンク