[インデックス 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
型(TCPAddr
やUDPAddr
など)のString()
メソッドで得られた文字列を再度解決した際に、元のAddr
オブジェクトを正確に再現することを保証するものです。これは、アドレスの文字列表現と、その文字列からのアドレス解決の間に一貫性がないという問題(Issue #6465)を修正します。
変更の背景
Goのnet
パッケージでは、IPアドレスやポート番号を含むネットワークアドレスを表現するためにTCPAddr
やUDPAddr
といった構造体が使用されます。これらの構造体には、アドレスを文字列形式で表現するためのString()
メソッドが提供されています。また、文字列形式のアドレスからTCPAddr
やUDPAddr
オブジェクトを解決するためのResolveTCPAddr
やResolveUDPAddr
といった関数も提供されています。
このコミットが修正しようとしている問題は、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.TCPAddr
やnet.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()
表現や再解決時に元の形式が失われる可能性があった点です。
技術的詳細
このコミットの技術的詳細は、主に以下の点に集約されます。
-
copyIP
関数の変更 (src/pkg/net/cgo_unix.go
):copyIP
関数は、IP
アドレスのコピーを作成する際に、元のIP
の長さが16バイト未満の場合(つまりIPv4アドレスの場合)、x.To16()
を呼び出して16バイトのIPv6形式に変換してからコピーするように変更されました。これにより、IPv4アドレスが常に16バイトの内部表現を持つようになり、IP.String()
やJoinHostPort
などの関数がより一貫した挙動を示すようになります。 -
ipEmptyString
ヘルパー関数の導入 (src/pkg/net/ip.go
): 新しくipEmptyString(ip IP) string
関数が導入されました。この関数は、IP
が空(長さが0)の場合に空文字列を返し、それ以外の場合はip.String()
の結果を返します。これは、TCPAddr
やUDPAddr
のString()
メソッドで、IPアドレスが指定されていない場合に"<nil>"
ではなく空文字列を返すようにするために使用されます。 -
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アドレスを元の形式で保持するようになります。 -
TCPAddr.String()
/UDPAddr.String()
の修正 (src/pkg/net/tcpsock.go
,src/pkg/net/udpsock.go
): これらのString()
メソッドは、IPアドレス部分の文字列化に新しく導入されたipEmptyString
ヘルパー関数を使用するように変更されました。これにより、a.IP
がnil
または空のIPアドレスである場合に、JoinHostPort
に渡されるIPアドレス文字列が空になり、JoinHostPort
が適切に処理できるようになります。 -
テストケースの修正と追加 (
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.go
とipsock_test.go
では、IPv4(127, 0, 0, 1).To4()
の呼び出しがIPv4(127, 0, 0, 1)
に修正されました。これは、IPv4
関数がすでに適切なIP
型を返すため、冗長なTo4()
呼び出しを削除し、アドレスの内部表現の一貫性を保つためです。tcp_test.go
とudp_test.go
では、TestResolveTCPAddr
とTestResolveUDPAddr
に重要なテストロジックが追加されました。これは、ResolveTCPAddr(tt.net, str)
(またはResolveUDPAddr
)を呼び出すことで、addr.String()
から得られた文字列が元のaddr
オブジェクトを正確に再現するかどうかを検証する「ラウンドトリップ」テストです。これにより、このコミットの主要な目的であるアドレス解決の一貫性が保証されます。
これらの変更により、Goのnet
パッケージは、IPアドレスの内部表現と文字列表現、そしてその逆の解決プロセスにおいて、より予測可能で堅牢な挙動を示すようになります。特に、IPv4アドレスがIPv6-mapped形式で扱われる場合でも、その情報が失われることなく正確に再現されるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルにまたがっています。
-
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
-
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) {
-
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 }
-
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 {
-
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.go
の copyIP
関数
この変更は、IP
アドレスのコピーを作成する際の挙動を調整します。以前は単にバイトスライスをコピーしていましたが、IPv4アドレス(長さが16バイト未満)の場合にx.To16()
を呼び出すことで、そのIPv4アドレスを16バイトのIPv6形式(IPv4-mapped IPv6アドレス)に変換してからコピーするように変更されました。
これは、Goのnet
パッケージが内部的にIPv4アドレスをIPv6-mapped形式で扱うことがあるため、アドレスの内部表現の一貫性を保つための重要な変更です。これにより、IP.String()
などの関数が、元のIPv4アドレスがどのような形式で提供されたかに関わらず、常に一貫した16バイトの内部表現に基づいて文字列を生成できるようになります。結果として、アドレスの文字列化と再解決のラウンドトリップがより正確になります。
src/pkg/net/ip.go
の ipEmptyString
関数
この新しいヘルパー関数は、net.IP
が空(長さが0)の場合に空文字列を返し、それ以外の場合は通常のIP.String()
の結果を返します。
これは主にTCPAddr
やUDPAddr
のString()
メソッドで使用されます。アドレスが指定されていない場合(例: &TCPAddr{Port: 80}
)、以前はa.IP.String()
が"<nil>"
のような文字列を返す可能性がありましたが、ipEmptyString
を使用することで、IPアドレス部分が空文字列として扱われ、JoinHostPort
関数がより適切に":80"
のような形式を生成できるようになります。これにより、アドレスの文字列表現がよりクリーンで予測可能になります。
src/pkg/net/ipsock.go
の ipv4only
関数
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.go
の TCPAddr.String()
および src/pkg/net/udpsock.go
の UDPAddr.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
)
最も重要な変更の一つは、TestResolveTCPAddr
とTestResolveUDPAddr
に「ラウンドトリップ」テストが追加されたことです。
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
が元のaddr
とreflect.DeepEqual
で完全に一致するかどうかを検証します。
このテストの追加により、net
パッケージのアドレス解決機能が、アドレスを文字列に変換し、その文字列を再度解決するという「ラウンドトリップ」のプロセスにおいて、元の正確なアドレス情報を再現できることが保証されます。これは、このコミットの主要な目的であり、ネットワークアドレスの表現と解決における一貫性と堅牢性を大幅に向上させます。
関連リンク
- Go
net
パッケージのドキュメント: https://pkg.go.dev/net - Go
net.IP
型のドキュメント: https://pkg.go.dev/net#IP - Go
net.TCPAddr
型のドキュメント: https://pkg.go.dev/net#TCPAddr - Go
net.UDPAddr
型のドキュメント: https://pkg.go.dev/net#UDPAddr
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- GoのIssueトラッカー (GitHub): https://github.com/golang/go/issues
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/13740048
はGerritの変更リストへのリンクです) - IPv4-mapped IPv6アドレスに関するRFC: RFC 4291 - IP Version 6 Addressing Architecture (特に2.5.5.2. IPv4-mapped IPv6 Address)