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

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

このコミットは、Go言語の標準ライブラリnetパッケージにおけるIPアドレス解決の挙動を修正し、特にResolveTCPAddrおよびResolveUDPAddr関数が、アドレスを文字列に変換した後に再度解決した際に元の正確なアドレスを再現するように改善するものです。これにより、IPアドレスの表現と解決の一貫性が向上し、ネットワークプログラミングにおける潜在的なバグが修正されます。

コミット

commit e8bbbe0886ffbd87de8ea827be5c43d8566b98d1
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 23 22:40:24 2013 -0400

    net: ensure that ResolveTCPAddr(addr.String()) reproduces addr
    
    And same for UDP.
    
    Fixes #6465.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/13740048

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

https://github.com/golang/go/commit/e8bbbe0886ffbd87de8ea827be5c43d8566b98d1

元コミット内容

net: ensure that ResolveTCPAddr(addr.String()) reproduces addr And same for UDP. Fixes #6465.

このコミットは、netパッケージにおいて、ResolveTCPAddrおよびResolveUDPAddr関数が、Addr型(TCPAddrUDPAddrなど)のString()メソッドで得られた文字列を再度解決した際に、元のAddrオブジェクトを正確に再現することを保証するものです。これは、アドレスの文字列表現と、その文字列からのアドレス解決の間に一貫性がないという問題(Issue #6465)を修正します。

変更の背景

Goのnetパッケージでは、IPアドレスやポート番号を含むネットワークアドレスを表現するためにTCPAddrUDPAddrといった構造体が使用されます。これらの構造体には、アドレスを文字列形式で表現するためのString()メソッドが提供されています。また、文字列形式のアドレスからTCPAddrUDPAddrオブジェクトを解決するためのResolveTCPAddrResolveUDPAddrといった関数も提供されています。

このコミットが修正しようとしている問題は、addr := &TCPAddr{IP: IPv4(127, 0, 0, 1), Port: 80}のようなAddrオブジェクトを作成し、そのString()メソッドを呼び出して文字列を得た後、その文字列をResolveTCPAddrに渡して再度解決した場合に、元のaddrと完全に同じオブジェクトが再現されない可能性があるというものでした。特に、IPv4アドレスがIPv6形式で表現されたり、IP.To4()が不適切に使用されたりすることで、アドレスの正規化や表現に一貫性が欠けていました。

この不整合は、ネットワーク設定の保存と復元、ログ出力、または外部システムとの連携など、アドレスの文字列表現を介して情報をやり取りする際に予期せぬ挙動やバグを引き起こす可能性がありました。このコミットは、このような「ラウンドトリップ」の整合性を保証することで、netパッケージの堅牢性と予測可能性を高めることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のnetパッケージに関する知識が必要です。

  • net.IP: IPアドレスを表すバイトスライスです。IPv4アドレスは4バイト、IPv6アドレスは16バイトで表現されます。
  • net.TCPAddr / net.UDPAddr: TCP/UDPネットワークアドレスを表す構造体で、IPフィールドとPortフィールドを持ちます。ZoneフィールドはIPv6のスコープID(ゾーンインデックス)に使用されます。
  • IP.String()メソッド: net.IP型のIPアドレスを標準的な文字列形式(例: "192.168.1.1", "::1")に変換します。
  • Addr.String()メソッド: net.TCPAddrnet.UDPAddrなどのAddrインターフェースを実装する型のアドレスをhost:port形式の文字列に変換します。IPv6アドレスの場合は[host]:port形式になります。
  • net.ResolveTCPAddr / net.ResolveUDPAddr: ネットワークタイプ("tcp", "udp"など)と文字列形式のアドレスを受け取り、対応する*TCPAddr*UDPAddrオブジェクトを解決して返します。
  • IP.To4()メソッド: net.IP型がIPv4アドレスである場合、そのIPv4アドレスを4バイトのIPv4形式に変換して返します。IPv4-mapped IPv6アドレス(例: ::ffff:192.0.2.1)もIPv4アドレスに変換します。IPv4アドレスでない場合はnilを返します。このメソッドの挙動が、今回の修正の鍵となります。
  • IPv4-mapped IPv6アドレス: IPv6アドレス空間内でIPv4アドレスを表現するための特殊な形式です。::ffff:c000:0201のように、::ffff:のプレフィックスの後にIPv4アドレスが続く形式です。Goのnetパッケージでは、内部的にIPv4アドレスを16バイトのIPv6アドレスとして扱うことがあります。

問題は、IP.To4()がIPv4アドレスを常に4バイト形式に変換するため、元のIPが16バイトのIPv4-mapped IPv6アドレスであった場合でも、To4()を適用すると4バイトのIPv4アドレスになってしまい、String()表現や再解決時に元の形式が失われる可能性があった点です。

技術的詳細

このコミットの技術的詳細は、主に以下の点に集約されます。

  1. copyIP関数の変更 (src/pkg/net/cgo_unix.go): copyIP関数は、IPアドレスのコピーを作成する際に、元のIPの長さが16バイト未満の場合(つまりIPv4アドレスの場合)、x.To16()を呼び出して16バイトのIPv6形式に変換してからコピーするように変更されました。これにより、IPv4アドレスが常に16バイトの内部表現を持つようになり、IP.String()JoinHostPortなどの関数がより一貫した挙動を示すようになります。

  2. ipEmptyStringヘルパー関数の導入 (src/pkg/net/ip.go): 新しくipEmptyString(ip IP) string関数が導入されました。この関数は、IPが空(長さが0)の場合に空文字列を返し、それ以外の場合はip.String()の結果を返します。これは、TCPAddrUDPAddrString()メソッドで、IPアドレスが指定されていない場合に"<nil>"ではなく空文字列を返すようにするために使用されます。

  3. ipv4only関数の修正 (src/pkg/net/ipsock.go): ipv4only関数のロジックが変更されました。以前はip.To4()を無条件に呼び出していましたが、新しい実装ではsupportsIPv4が真であり、かつip.To4()nilでない場合(つまり、ipが有効なIPv4アドレスまたはIPv4-mapped IPv6アドレスである場合)にのみ元のipを返します。それ以外の場合はnilを返します。これにより、ipv4onlyがIPv4アドレスを適切にフィルタリングし、IPv4-mapped IPv6アドレスを元の形式で保持するようになります。

  4. TCPAddr.String() / UDPAddr.String()の修正 (src/pkg/net/tcpsock.go, src/pkg/net/udpsock.go): これらのString()メソッドは、IPアドレス部分の文字列化に新しく導入されたipEmptyStringヘルパー関数を使用するように変更されました。これにより、a.IPnilまたは空のIPアドレスである場合に、JoinHostPortに渡されるIPアドレス文字列が空になり、JoinHostPortが適切に処理できるようになります。

  5. テストケースの修正と追加 (src/pkg/net/ipraw_test.go, src/pkg/net/ipsock_test.go, src/pkg/net/tcp_test.go, src/pkg/net/udp_test.go):

    • ipraw_test.goipsock_test.goでは、IPv4(127, 0, 0, 1).To4()の呼び出しがIPv4(127, 0, 0, 1)に修正されました。これは、IPv4関数がすでに適切なIP型を返すため、冗長なTo4()呼び出しを削除し、アドレスの内部表現の一貫性を保つためです。
    • tcp_test.goudp_test.goでは、TestResolveTCPAddrTestResolveUDPAddrに重要なテストロジックが追加されました。これは、ResolveTCPAddr(tt.net, str)(またはResolveUDPAddr)を呼び出すことで、addr.String()から得られた文字列が元のaddrオブジェクトを正確に再現するかどうかを検証する「ラウンドトリップ」テストです。これにより、このコミットの主要な目的であるアドレス解決の一貫性が保証されます。

これらの変更により、Goのnetパッケージは、IPアドレスの内部表現と文字列表現、そしてその逆の解決プロセスにおいて、より予測可能で堅牢な挙動を示すようになります。特に、IPv4アドレスがIPv6-mapped形式で扱われる場合でも、その情報が失われることなく正確に再現されるようになります。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルにまたがっています。

  1. src/pkg/net/cgo_unix.go: copyIP関数に、IPv4アドレスを16バイトのIPv6形式に変換するロジックが追加されました。

    --- a/src/pkg/net/cgo_unix.go
    +++ b/src/pkg/net/cgo_unix.go
    @@ -155,6 +155,9 @@ func cgoLookupCNAME(name string) (cname string, err error, completed bool) {
     }
     
     func copyIP(x IP) IP {
    ++	if len(x) < 16 {
    ++		return x.To16()
    ++	}
     	y := make(IP, len(x))
     	copy(y, x)
     	return y
    
  2. src/pkg/net/ip.go: ipEmptyStringヘルパー関数が新しく追加されました。

    --- a/src/pkg/net/ip.go
    +++ b/src/pkg/net/ip.go
    @@ -312,6 +312,15 @@ func (ip IP) String() string {
     	return s
     }
     
    +// ipEmptyString is like ip.String except that it returns
    +// an empty string when ip is unset.
    +func ipEmptyString(ip IP) string {
    ++	if len(ip) == 0 {
    ++		return ""
    ++	}
    ++	return ip.String()
    +}
    +
     // MarshalText implements the encoding.TextMarshaler interface.
     // The encoding is the same as returned by String.
     func (ip IP) MarshalText() ([]byte, error) {
    
  3. src/pkg/net/ipsock.go: ipv4only関数のロジックが修正されました。

    --- a/src/pkg/net/ipsock.go
    +++ b/src/pkg/net/ipsock.go
    @@ -116,11 +116,11 @@ func firstSupportedAddr(filter func(IP) IP, ips []IP, inetaddr func(IP) netaddr)\n     }\n     
     // ipv4only returns IPv4 addresses that we can use with the kernel\'s
    -// IPv4 addressing modes.  It returns IPv4-mapped IPv6 addresses as
    -// IPv4 addresses and returns other IPv6 address types as nils.\n+// IPv4 addressing modes. If ip is an IPv4 address, ipv4only returns ip.\n+// Otherwise it returns nil.\n     func ipv4only(ip IP) IP {
    --	if supportsIPv4 {
    --		return ip.To4()
    -+	if supportsIPv4 && ip.To4() != nil {
    ++		return ip
     	}
     	return nil
     }
    
  4. src/pkg/net/tcpsock.go: TCPAddr.String()メソッドがipEmptyStringを使用するように変更されました。

    --- a/src/pkg/net/tcpsock.go
    +++ b/src/pkg/net/tcpsock.go
    @@ -18,10 +18,11 @@ func (a *TCPAddr) String() string {
     	if a == nil {
     		return "<nil>"\
     	}\
    ++	ip := ipEmptyString(a.IP)
     	if a.Zone != "" {
    --		return JoinHostPort(a.IP.String()+"%"+a.Zone, itoa(a.Port))\
    -+		return JoinHostPort(ip+"%"+a.Zone, itoa(a.Port))\
     	}\
    --	return JoinHostPort(a.IP.String(), itoa(a.Port))\
    -+	return JoinHostPort(ip, itoa(a.Port))\
     }\
     
     func (a *TCPAddr) toAddr() Addr {
    
  5. src/pkg/net/udp_test.go: TestResolveUDPAddrにラウンドトリップテストロジックが追加されました。

    --- a/src/pkg/net/udp_test.go
    +++ b/src/pkg/net/udp_test.go
    @@ -5,60 +5,31 @@
     package net
     
     import (
    -	"fmt"
     	"reflect"
     	"runtime"
    +	"strings"
     	"testing"
     )
     
    -type resolveUDPAddrTest struct {
    -	net           string
    -	litAddrOrName string
    -	addr          *UDPAddr
    -	err           error
    -}
    -
    -var resolveUDPAddrTests = []resolveUDPAddrTest{
    -	{"udp", "127.0.0.1:0", &UDPAddr{IP: IPv4(127, 0, 0, 1), Port: 0}, nil},
    -	{"udp4", "127.0.0.1:65535", &UDPAddr{IP: IPv4(127, 0, 0, 1), Port: 65535}, nil},
    -
    -	{"udp", "[::1]:1", &UDPAddr{IP: ParseIP("::1"), Port: 1}, nil},
    -	{"udp6", "[::1]:65534", &UDPAddr{IP: ParseIP("::1"), Port: 65534}, nil},
    -
    -	{"udp", "[::1%en0]:1", &UDPAddr{IP: ParseIP("::1"), Port: 1, Zone: "en0"}, nil},
    -	{"udp6", "[::1%911]:2", &UDPAddr{IP: ParseIP("::1"), Port: 2, Zone: "911"}, nil},
    -
    -	{"", "127.0.0.1:0", &UDPAddr{IP: IPv4(127, 0, 0, 1), Port: 0}, nil}, // Go 1.0 behavior
    -	{"", "[::1]:0", &UDPAddr{IP: ParseIP("::1"), Port: 0}, nil},         // Go 1.0 behavior
    -
    -	{"sip", "127.0.0.1:0", nil, UnknownNetworkError("sip")},
    -}
    -
    -func init() {
    -	if ifi := loopbackInterface(); ifi != nil {
    -		index := fmt.Sprintf("%v", ifi.Index)
    -		resolveUDPAddrTests = append(resolveUDPAddrTests, []resolveUDPAddrTest{
    -			{"udp6", "[fe80::1%" + ifi.Name + "]:3", &UDPAddr{IP: ParseIP("fe80::1"), Port: 3, Zone: zoneToString(ifi.Index)}, nil},
    -			{"udp6", "[fe80::1%" + index + "]:4", &UDPAddr{IP: ParseIP("fe80::1"), Port: 4, Zone: index}, nil},
    -		}...)
    -	}
    -	if ips, err := LookupIP("localhost"); err == nil && len(ips) > 1 && supportsIPv4 && supportsIPv6 {
    -		resolveUDPAddrTests = append(resolveUDPAddrTests, []resolveUDPAddrTest{
    -			{"udp", "localhost:5", &UDPAddr{IP: IPv4(127, 0, 0, 1).To4(), Port: 5}, nil},
    -			{"udp4", "localhost:6", &UDPAddr{IP: IPv4(127, 0, 0, 1).To4(), Port: 6}, nil},
    -			{"udp6", "localhost:7", &UDPAddr{IP: IPv6loopback, Port: 7}, nil},
    -		}...)
    -	}
    -}
    -
     func TestResolveUDPAddr(t *testing.T) {
    -	for _, tt := range resolveUDPAddrTests {
    -	\taddr, err := ResolveUDPAddr(tt.net, tt.litAddrOrName)
    -+	for _, tt := range resolveTCPAddrTests {
    -+		net := strings.Replace(tt.net, "tcp", "udp", -1)
    -+		addr, err := ResolveUDPAddr(net, tt.litAddrOrName)
     		if err != tt.err {
    --		\tt.Fatalf("ResolveUDPAddr(%q, %q) failed: %v", tt.net, tt.litAddrOrName, err)
    -+		\tt.Fatalf("ResolveUDPAddr(%q, %q) failed: %v", net, tt.litAddrOrName, err)
    -+		}
    -+		if !reflect.DeepEqual(addr, (*UDPAddr)(tt.addr)) {
    -+		\tt.Fatalf("ResolveUDPAddr(%q, %q) = %#v, want %#v", net, tt.litAddrOrName, addr, tt.addr)
     		}
    --		if !reflect.DeepEqual(addr, tt.addr) {
    --		\tt.Fatalf("got %#v; expected %#v", addr, tt.addr)
    -+		if err == nil {
    -+			str := addr.String()
    -+			addr1, err := ResolveUDPAddr(net, str)
    -+			if err != nil {
    -+			\tt.Fatalf("ResolveUDPAddr(%q, %q) [from %q]: %v", net, str, tt.litAddrOrName, err)
    -+			}
    -+			if !reflect.DeepEqual(addr1, addr) {
    -+			\tt.Fatalf("ResolveUDPAddr(%q, %q) [from %q] = %#v, want %#v", net, str, tt.litAddrOrName, addr1, addr)
    -+			}
     		}
     	}
     }
    

コアとなるコードの解説

src/pkg/net/cgo_unix.gocopyIP 関数

この変更は、IPアドレスのコピーを作成する際の挙動を調整します。以前は単にバイトスライスをコピーしていましたが、IPv4アドレス(長さが16バイト未満)の場合にx.To16()を呼び出すことで、そのIPv4アドレスを16バイトのIPv6形式(IPv4-mapped IPv6アドレス)に変換してからコピーするように変更されました。

これは、Goのnetパッケージが内部的にIPv4アドレスをIPv6-mapped形式で扱うことがあるため、アドレスの内部表現の一貫性を保つための重要な変更です。これにより、IP.String()などの関数が、元のIPv4アドレスがどのような形式で提供されたかに関わらず、常に一貫した16バイトの内部表現に基づいて文字列を生成できるようになります。結果として、アドレスの文字列化と再解決のラウンドトリップがより正確になります。

src/pkg/net/ip.goipEmptyString 関数

この新しいヘルパー関数は、net.IPが空(長さが0)の場合に空文字列を返し、それ以外の場合は通常のIP.String()の結果を返します。

これは主にTCPAddrUDPAddrString()メソッドで使用されます。アドレスが指定されていない場合(例: &TCPAddr{Port: 80})、以前はa.IP.String()"<nil>"のような文字列を返す可能性がありましたが、ipEmptyStringを使用することで、IPアドレス部分が空文字列として扱われ、JoinHostPort関数がより適切に":80"のような形式を生成できるようになります。これにより、アドレスの文字列表現がよりクリーンで予測可能になります。

src/pkg/net/ipsock.goipv4only 関数

ipv4only関数は、与えられたIPアドレスがIPv4アドレスとして使用可能かどうかを判断します。以前のバージョンでは、無条件にip.To4()を呼び出していましたが、これはIPv4-mapped IPv6アドレスを純粋なIPv4アドレスに変換してしまうため、元の16バイトの情報が失われる可能性がありました。

新しい実装では、supportsIPv4が真であり、かつip.To4()nilでない場合(つまり、ipが有効なIPv4アドレスまたはIPv4-mapped IPv6アドレスである場合)にのみ、元のipをそのまま返します。これにより、IPv4-mapped IPv6アドレスがIPv4アドレスとして有効であると判断された場合でも、その16バイトの内部表現が保持されるようになります。この変更は、アドレスの正規化と表現の正確性を向上させ、特にIPv4とIPv6の混在環境での挙動を改善します。

src/pkg/net/tcpsock.goTCPAddr.String() および src/pkg/net/udpsock.goUDPAddr.String()

これらのメソッドは、TCPAddrおよびUDPAddrオブジェクトを文字列形式に変換する役割を担っています。変更点としては、IPアドレス部分の文字列化に新しく導入されたipEmptyStringヘルパー関数を使用するようになったことです。

以前はa.IP.String()を直接呼び出していましたが、ipEmptyStringを使用することで、a.IPが空のIPアドレスである場合に、JoinHostPortに渡されるIPアドレス文字列が空になり、結果として":port"のような形式が正しく生成されるようになります。また、a.Zoneが存在する場合の処理も同様にipEmptyStringを使用するように変更され、全体的なアドレスの文字列表現の一貫性が向上しました。

テストケースの変更 (src/pkg/net/tcp_test.go, src/pkg/net/udp_test.go)

最も重要な変更の一つは、TestResolveTCPAddrTestResolveUDPAddrに「ラウンドトリップ」テストが追加されたことです。

if err == nil {
    str := addr.String()
    addr1, err := ResolveTCPAddr(tt.net, str) // または ResolveUDPAddr
    if err != nil {
        t.Fatalf("ResolveTCPAddr(%q, %q) [from %q]: %v", tt.net, str, tt.litAddrOrName, err)
    }
    if !reflect.DeepEqual(addr1, addr) {
        t.Fatalf("ResolveTCPAddr(%q, %q) [from %q] = %#v, want %#v", tt.net, str, tt.litAddrOrName, addr1, addr)
    }
}

このテストブロックは、ResolveTCPAddr(またはResolveUDPAddr)が成功した場合に実行されます。まず、解決されたaddrオブジェクトのString()メソッドを呼び出して文字列strを取得します。次に、そのstrを再度ResolveTCPAddrに渡してaddr1を解決します。最後に、addr1が元のaddrreflect.DeepEqualで完全に一致するかどうかを検証します。

このテストの追加により、netパッケージのアドレス解決機能が、アドレスを文字列に変換し、その文字列を再度解決するという「ラウンドトリップ」のプロセスにおいて、元の正確なアドレス情報を再現できることが保証されます。これは、このコミットの主要な目的であり、ネットワークアドレスの表現と解決における一貫性と堅牢性を大幅に向上させます。

関連リンク

参考にした情報源リンク