[インデックス 11831] ファイルの概要
このコミットは、Go言語のネットワークパッケージにおいて、TCPの自己接続(self-connect)を回避するための修正を導入しています。具体的には、net
パッケージのDialTCP
関数が、意図せず自分自身に接続してしまう「同時接続(simultaneous connection)」と呼ばれる稀なケースを検出し、これを回避するためのロジックを追加しています。これにより、特定の条件下で発生する可能性のあるカーネルのバグに起因する予期せぬ接続を防ぎ、Dial
操作の堅牢性を向上させています。
コミット
commit cbe7d8db24d5d0484971f121e9b3f446e39cd3b5
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 12 23:25:55 2012 -0500
net: avoid TCP self-connect
Fixes #2690.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5650071
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cbe7d8db24d5d0484971f121e9b3f446e39cd3b5
元コミット内容
このコミットは、Goのnet
パッケージにおけるTCPの自己接続問題を解決します。具体的には、DialTCP
関数が、ローカルアドレスをカーネルに任せた場合に、稀に自分自身に接続してしまう現象(同時接続)を回避するためのロジックを追加しています。この問題は、Linuxカーネルの特定の動作に起因するもので、Dial
が成功したにもかかわらず、実際には意図しない自己接続が発生するというバグを修正します。
変更の背景
この変更の背景には、Goのnet
パッケージにおけるDial
関数が、特定の条件下で予期せぬ自己接続(self-connect)を引き起こす可能性があったという問題があります。これは、TCPの「同時接続(simultaneous connection)」という稀なメカニズムと、一部のLinuxカーネルの動作が組み合わさることで発生していました。
具体的には、Dial("tcp", "", "localhost:50001")
のように、ローカルアドレス(laddr
)をnil
(カーネルに選択を任せる)に設定し、かつリモートアドレスがローカルホスト上のリスナーのないポートである場合に問題が発生しました。一部のLinuxカーネルは、ローカルポートの選択において、宛先ポートに関わらず固定範囲を盲目的に循環することがあり、その結果、Dial
が自身と同じポートをローカルポートとして選択してしまうことがありました。これにより、Dial
はリスナーが存在しないにもかかわらず、自分自身と同時接続を確立してしまうという、実質的なカーネルのバグに起因する現象が発生していました。
この問題は、golang.org/issue/2690で報告されており、ユーザーにとってはDial
が成功したにもかかわらず、期待する外部への接続ではなく、自分自身への接続が確立されるという混乱を招くものでした。このコミットは、このような「バグのある効果」をユーザーに晒すのではなく、Goランタイム側でこれを検出し、回避することで、Dial
操作の信頼性と予測可能性を向上させることを目的としています。
前提知識の解説
TCPの同時接続 (Simultaneous Connection)
TCPの同時接続は、通常のクライアント-サーバーモデルとは異なり、2つのホストが同時に互いに接続を開始しようとした場合に発生する稀なTCP接続確立メカニズムです。通常、TCP接続は「クライアントがSYNを送信し、サーバーがSYN-ACKで応答し、クライアントがACKを送信する」という3ウェイハンドシェイクによって確立されます。
しかし、同時接続の場合、両方のホストが同時にSYNパケットを送信し、互いのSYNパケットを受信します。その後、両方がSYN-ACKを送信し、最終的にACKを送信することで接続が確立されます。このプロセスは、どちらか一方がリスニング状態にある必要がなく、両方がconnect()
(またはGoのDial
のような関数)を呼び出すことで発生します。
同時接続はTCPの仕様で定義されていますが、一般的なアプリケーションではほとんど使用されず、通常は意図しない動作として現れることがあります。特に、ローカルホスト上でDial
が自分自身に接続しようとする場合に、このメカニズムが関与することがあります。
Dial
関数とローカルアドレスの選択
Goのnet
パッケージにおけるDial
関数(およびその内部で呼び出されるDialTCP
)は、指定されたネットワークアドレスに接続を試みます。この際、接続元のローカルアドレス(IPアドレスとポート番号)を指定することもできますが、通常はnil
を指定してカーネルに適切なローカルアドレスを自動的に選択させます。
カーネルがローカルアドレスを選択する際、特にローカルポートの選択は、オペレーティングシステムの実装に依存します。一部のLinuxカーネルでは、ローカルポートの選択ロジックが単純で、宛先ポートを考慮せずに、利用可能なポートを循環的に割り当てるような動作をすることがあります。この動作が、前述のTCP同時接続と組み合わさることで、意図しない自己接続を引き起こす原因となります。
syscall
パッケージとソケット操作
Goのnet
パッケージは、内部でオペレーティングシステムのシステムコール(syscall
パッケージを通じて)を利用してネットワーク操作を実行しています。ソケットの作成、接続、バインドなどの低レベルな操作は、syscall
パッケージを介して行われます。
このコミットで変更されているsrc/pkg/net/tcpsock_posix.go
ファイルは、POSIX互換システム(Linuxなど)におけるTCPソケットの低レベルな操作を扱っています。internetSocket
関数は、TCPソケットを作成し、接続を確立するためのシステムコールをラップしています。
技術的詳細
このコミットは、src/pkg/net/tcpsock_posix.go
内のDialTCP
関数に、TCPの自己接続を検出して回避するためのロジックを追加しています。
-
自己接続の検出:
- 新しいヘルパー関数
selfConnect(fd *netFD) bool
が導入されました。 - この関数は、確立された接続のローカルアドレス(
fd.laddr
)とリモートアドレス(fd.raddr
)を比較します。 - 具体的には、ローカルポートとリモートポートが同じであり(
l.Port == r.Port
)、かつローカルIPアドレスとリモートIPアドレスが同じである(l.IP.Equal(r.IP)
)場合にtrue
を返します。これは、接続が自分自身に対して行われたことを示します。
- 新しいヘルパー関数
-
自己接続の回避ロジック:
DialTCP
関数内で、internetSocket
の呼び出し後に、自己接続が発生していないかチェックするループが追加されました。for i := 0; i < 2 && err == nil && laddr == nil && selfConnect(fd); i++
- このループは最大2回繰り返されます。
err == nil
: 接続がエラーなく確立された場合。laddr == nil
: ローカルアドレスがカーネルに任された場合(この問題が発生する条件)。selfConnect(fd)
: 自己接続が検出された場合。
- もしこれらの条件がすべて満たされた場合、つまり自己接続が検出され、かつローカルアドレスが自動選択された場合、現在のソケットディスクリプタ(
fd
)を閉じます(fd.Close()
)。 - そして、再度
internetSocket
を呼び出して、新しい接続を試みます。 - このロジックは、「カーネルのバグ」によって自己接続が発生した場合に、その結果をユーザーに晒すのではなく、接続を再試行することで問題を回避しようとします。
- 最大2回の再試行後も自己接続が続く場合は、諦めてその結果を使用します。これは、無限ループを防ぐための安全策です。
-
テストケースの追加:
src/pkg/net/dial_test.go
にTestSelfConnect
という新しいテスト関数が追加されました。- このテストは、まず
Listen
を使って一時的なローカルポートを確保し、そのアドレスを取得します。 - 次に、そのアドレスに対して
Dial
を試み、ローカルアドレスを取得して接続を閉じます。 - その後、取得したローカルアドレスに対して
Dial
を繰り返し試行します(10万回、testing.Short()
の場合は1000回)。 - このテストの目的は、
Dial
が自己接続を「成功させない」ことを確認することです。もしDial
が成功してしまった場合、それは自己接続が発生したことを意味し、テストは失敗します。 - このテストは、コミットで導入された自己接続回避ロジックが正しく機能していることを検証します。
この修正により、Goのnet
パッケージは、特定の環境下で発生する可能性のあるTCP自己接続の問題に対して、より堅牢になりました。
コアとなるコードの変更箇所
src/pkg/net/dial_test.go
--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -84,3 +84,34 @@ func TestDialTimeout(t *testing.T) {
}
}
}
+
+func TestSelfConnect(t *testing.T) {
+ // Test that Dial does not honor self-connects.
+ // See the comment in DialTCP.
+
+ // Find a port that would be used as a local address.
+ l, err := Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := Dial("tcp", l.Addr().String())
+ if err != nil {
+ t.Fatal(err)
+ }
+ addr := c.LocalAddr().String()
+ c.Close()
+ l.Close()
+
+ // Try to connect to that address repeatedly.
+ n := 100000
+ if testing.Short() {
+ n = 1000
+ }
+ for i := 0; i < n; i++ {
+ c, err := Dial("tcp", addr)
+ if err == nil {
+ c.Close()
+ t.Errorf("#%d: Dial %q succeeded", i, addr)
+ }
+ }
+}
src/pkg/net/tcpsock_posix.go
--- a/src/pkg/net/tcpsock_posix.go
+++ b/src/pkg/net/tcpsock_posix.go
@@ -227,13 +227,43 @@ func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
if raddr == nil {
return nil, &OpError{"dial", net, nil, errMissingAddress}
}
+
fd, err := internetSocket(net, laddr.toAddr(), raddr.toAddr(), syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
+
+ // TCP has a rarely used mechanism called a 'simultaneous connection' in
+ // which Dial("tcp", addr1, addr2) run on the machine at addr1 can
+ // connect to a simultaneous Dial("tcp", addr2, addr1) run on the machine
+ // at addr2, without either machine executing Listen. If laddr == nil,
+ // it means we want the kernel to pick an appropriate originating local
+ // address. Some Linux kernels cycle blindly through a fixed range of
+ // local ports, regardless of destination port. If a kernel happens to
+ // pick local port 50001 as the source for a Dial("tcp", "", "localhost:50001"),
+ // then the Dial will succeed, having simultaneously connected to itself.
+ // This can only happen when we are letting the kernel pick a port (laddr == nil)
+ // and when there is no listener for the destination address.
+ // It's hard to argue this is anything other than a kernel bug. If we
+ // see this happen, rather than expose the buggy effect to users, we
+ // close the fd and try again. If it happens twice more, we relent and
+ // use the result. See also:
+ // http://golang.org/issue/2690
+ // http://stackoverflow.com/questions/4949858/
+ for i := 0; i < 2 && err == nil && laddr == nil && selfConnect(fd); i++ {
+ fd.Close()
+ fd, err = internetSocket(net, laddr.toAddr(), raddr.toAddr(), syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
+ }
+
if err != nil {
return nil, err
}
return newTCPConn(fd), nil
}
+func selfConnect(fd *netFD) bool {
+ l := fd.laddr.(*TCPAddr)
+ r := fd.raddr.(*TCPAddr)
+ return l.Port == r.Port && l.IP.Equal(r.IP)
+}
+
// TCPListener is a TCP network listener.
// Clients should typically use variables of type Listener
// instead of assuming TCP.
コアとなるコードの解説
src/pkg/net/tcpsock_posix.go
の変更点
-
selfConnect
関数の追加:- この関数は、
*netFD
(ネットワークファイルディスクリプタ)を受け取り、そのローカルアドレス(laddr
)とリモートアドレス(raddr
)が同じIPアドレスとポート番号を持つかどうかをチェックします。 l := fd.laddr.(*TCPAddr)
とr := fd.raddr.(*TCPAddr)
で、アドレス情報をTCPAddr
型にキャストしています。return l.Port == r.Port && l.IP.Equal(r.IP)
: ローカルポートとリモートポートが一致し、かつローカルIPアドレスとリモートIPアドレスが一致する場合にtrue
を返します。これは、接続が自分自身に対して行われたことを明確に示します。
- この関数は、
-
DialTCP
関数内の自己接続回避ロジック:fd, err := internetSocket(...)
でソケットが作成され、接続が試みられた後、以下のfor
ループが実行されます。for i := 0; i < 2 && err == nil && laddr == nil && selfConnect(fd); i++
i < 2
: 最大2回まで再試行します。err == nil
: 最初の接続試行でエラーが発生しなかった場合。laddr == nil
:Dial
呼び出し時にローカルアドレスが明示的に指定されず、カーネルに自動選択を任せた場合。この条件が重要で、自己接続問題は主にこのケースで発生します。selfConnect(fd)
: 新しく確立された接続が自己接続であるとselfConnect
関数が判断した場合。
- これらの条件がすべて真の場合、つまり、カーネルがローカルアドレスを自動選択した結果、自己接続が意図せず確立されてしまった場合に、以下の処理が行われます。
fd.Close()
: 現在の自己接続ソケットを閉じます。fd, err = internetSocket(...)
: 再度internetSocket
を呼び出し、新しいソケットを作成して接続を試みます。これにより、カーネルが異なるローカルポートを選択し、自己接続を回避できることを期待します。
- このループは、最大2回の再試行で自己接続が回避できない場合、または他の条件が満たされない場合に終了し、最終的な
fd
とerr
が返されます。
src/pkg/net/dial_test.go
の変更点
TestSelfConnect
関数の追加:- このテストは、
Dial
が自己接続を「許可しない」ことを検証します。 - まず、
Listen("tcp", "127.0.0.1:0")
を使って、OSが利用可能な一時的なローカルポートを確保します。127.0.0.1:0
は、ループバックアドレス上の任意の空きポートを意味します。 c, err := Dial("tcp", l.Addr().String())
で、リスナーのアドレスに対してDial
を試みます。これは、リスナーが存在する通常の接続テストです。ここでc.LocalAddr().String()
を使って、OSが割り当てたローカルアドレス(例:127.0.0.1:xxxxx
)を取得します。c.Close()
とl.Close()
で、確立された接続とリスナーを閉じます。これにより、先ほど取得したローカルアドレスのポートは解放され、リスナーが存在しない状態になります。for i := 0; i < n; i++ { ... }
ループで、先ほど取得した「リスナーが存在しない」ローカルアドレスに対して、Dial
を繰り返し試行します。if err == nil { t.Errorf(...) }
: もしこのDial
がエラーなく成功してしまった場合、それは自己接続が発生したことを意味します。この場合、テストは失敗し、エラーメッセージが出力されます。- このテストは、
DialTCP
に導入された自己接続回避ロジックが正しく機能し、意図しない自己接続が防止されていることを確認するためのものです。
- このテストは、
これらの変更により、Goのネットワークスタックは、特定のカーネルの動作に起因する稀な自己接続の問題に対して、より堅牢で予測可能な振る舞いをするようになりました。