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

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

このコミットは、Go言語の標準ライブラリである net パッケージにおけるネットワーク接続のリモートアドレス取得に関するバグ修正と、それに関連するテストケースの追加を行っています。具体的には、syscall.Getpeername システムコールがリモートアドレスの取得に失敗した場合に、接続確立時に指定された元々のリモートアドレスを使用するようにフォールバックするロジックが導入されました。これにより、特定の条件下でリモートアドレスが正しく設定されない問題を解決しています。

コミット

このコミットは、net パッケージにおいて、getpeername システムコールが失敗した場合に、接続時に指定されたオリジナルのリモートアドレスを使用するように修正を施しています。これにより、Goのネットワーク接続におけるリモートアドレスの信頼性が向上しました。

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

https://github.com/golang/go/commit/06cbe78bd77789afb46b597466bcecf73c25d6d2

元コミット内容

commit 06cbe78bd77789afb46b597466bcecf73c25d6d2
Author: Tyler Bunnell <tylerbunnell@gmail.com>
Date:   Wed Mar 13 14:49:24 2013 -0400

    net: use original raddr if getpeername fails
    
    Fixes #3838.
    
    R=dave, mikioh.mikioh, rsc
    CC=golang-dev
    https://golang.org/cl/7511043

変更の背景

この変更は、Goの net パッケージにおいて、特定の条件下で確立されたネットワーク接続のリモートアドレス(RemoteAddr() メソッドで取得されるアドレス)が正しく設定されないというバグ(Issue #3838)を修正するために行われました。

従来のGoのネットワーク接続処理では、ソケットが確立された後、syscall.Getpeername システムコールを使用して接続相手(ピア)のリモートアドレスを取得していました。しかし、一部のオペレーティングシステムや特定のネットワーク構成において、この getpeername システムコールが期待通りにリモートアドレスを返さない、あるいは一時的に失敗するケースが存在しました。このような場合、Goの内部で管理される接続のリモートアドレスが nil となってしまい、アプリケーションが RemoteAddr() を呼び出した際に誤った情報が返されるか、パニックを引き起こす可能性がありました。

この問題は、特に DialIP のような低レベルのIP接続を扱う際に顕著でした。DialIP は、特定のIPアドレスとプロトコルを指定して接続を確立しますが、getpeername が失敗すると、この接続先情報が失われてしまうことがありました。

このコミットは、このような getpeername の信頼性の問題を緩和し、接続確立時に指定されたオリジナルのリモートアドレスをフォールバックとして使用することで、より堅牢なネットワーク接続管理を実現することを目的としています。

前提知識の解説

ネットワークソケットとアドレス

  • ソケット (Socket): ネットワーク通信のエンドポイントです。アプリケーションがネットワーク経由でデータを送受信するために使用する抽象的な概念です。ファイルディスクリプタ(Unix系OSの場合)として表現されることが多いです。
  • ローカルアドレス (Local Address): ソケットがバインドされている、通信を行う側のマシンのIPアドレスとポート番号です。
  • リモートアドレス (Remote Address): ソケットが接続している、通信相手のマシンのIPアドレスとポート番号です。

getpeername システムコール

getpeername は、Unix系オペレーティングシステムで提供されるシステムコールの一つです。接続済みのソケット(ファイルディスクリプタで識別される)に対して呼び出され、そのソケットが接続しているピア(リモートホスト)のソケットアドレス(IPアドレスとポート番号)を取得するために使用されます。

  • 機能: 接続済みソケットのリモートアドレスを取得します。
  • 戻り値: 通常、リモートアドレスを含むソケットアドレス構造体(例: sockaddr_insockaddr_in6)を返します。エラーが発生した場合はエラーコードを返します。
  • 失敗の可能性: getpeername は、ソケットがまだ完全に接続されていない状態であったり、特定のネットワーク環境やOSのバグ、あるいは一時的なリソース不足など、様々な理由で失敗する可能性があります。失敗した場合、リモートアドレス情報が取得できません。

Go言語の net パッケージ

Go言語の net パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなどのネットワークプロトコルを扱うためのインターフェースと実装が含まれています。

  • net.Conn インターフェース: ネットワーク接続を表す基本的なインターフェースで、ReadWriteCloseLocalAddrRemoteAddr などのメソッドを定義しています。
  • net.IPConn: IPプロトコル(例: IP-in-IP、ICMP、IGMPなど)を直接扱うための接続タイプです。
  • DialIP: 指定されたIPアドレスとプロトコルを使用してIP接続を確立するための関数です。
  • syscall パッケージ: GoプログラムからOSのシステムコールを直接呼び出すための機能を提供します。syscall.Getpeername はこのパッケージを通じて呼び出されます。

fd.raddrursa

  • fd.raddr: Goの net パッケージ内部で、ネットワーク接続を表すファイルディスクリプタ(fd)構造体の一部として保持されるリモートアドレスです。これは net.Conn インターフェースの RemoteAddr() メソッドを通じて外部に公開されます。
  • ursa (Unix Remote Socket Address): socket 関数(Goの内部関数で、ソケットの初期化と設定を行う)に渡される引数の一つで、接続を確立する際に指定された「元々の」リモートソケットアドレスを表します。これは、DialIP などの関数が内部でソケットを作成する際に、接続先として指定するアドレスです。

技術的詳細

このコミットの技術的詳細な修正は、src/pkg/net/sock_posix.go ファイル内の socket 関数に集中しています。この関数は、Goの net パッケージが新しいネットワークソケットを作成し、初期化する際に呼び出される内部関数です。

修正前のコードでは、ソケットが作成された後、syscall.Getpeername(s) を呼び出してリモートアドレス rsa を取得し、それを fd.setAddr(laddr, raddr) を通じて fd.raddr に設定していました。ここで問題となるのは、getpeername が何らかの理由で失敗した場合、rsa が空(または無効なアドレス)となり、結果として fd.raddrnil に設定されてしまうことでした。

このコミットでは、以下の3行が追加されました。

	if fd.raddr == nil {
		fd.raddr = toAddr(ursa)
	}

このコードブロックの動作は以下の通りです。

  1. if fd.raddr == nil: syscall.Getpeername の結果として fd.raddrnil になっているかどうかをチェックします。これは、getpeername が失敗したか、有効なリモートアドレスを返さなかった場合に発生します。
  2. fd.raddr = toAddr(ursa): もし fd.raddrnil であった場合、ursasocket 関数に渡された、接続確立時に指定されたオリジナルのリモートソケットアドレス)を toAddr 関数でGoの net.Addr 型に変換し、それを fd.raddr に設定します。

この修正により、getpeername が失敗した場合でも、Goは接続確立時に意図されたリモートアドレスをフォールバックとして使用するようになります。これにより、RemoteAddr() メソッドが常に有効なアドレスを返すことが保証され、Issue #3838 で報告されたような、リモートアドレスが nil になることによる問題が解決されます。

また、src/pkg/net/ipraw_test.go に追加された TestIPConnRemoteName は、この修正が正しく機能することを確認するためのテストケースです。このテストは、DialIP で特定のIPアドレスに接続し、その後 c.RemoteAddr()DialIP に渡したオリジナルのリモートアドレスと一致するかどうかを検証します。これにより、getpeername の結果に依存せず、オリジナルのリモートアドレスが正しく保持されていることを確認しています。このテストはルート権限を必要とすることから、低レベルのネットワーク操作が関わっていることが示唆されます。

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

src/pkg/net/ipraw_test.go

--- a/src/pkg/net/ipraw_test.go
+++ b/src/pkg/net/ipraw_test.go
@@ -337,3 +337,19 @@ func TestIPConnLocalName(t *testing.T) {
 		}
 	}
 }
+
+func TestIPConnRemoteName(t *testing.T) {
+	if os.Getuid() != 0 {
+		t.Skip("skipping test; must be root")
+	}
+
+	raddr := &IPAddr{IP: IPv4(127, 0, 0, 10).To4()}
+	c, err := DialIP("ip:tcp", &IPAddr{IP: IPv4(127, 0, 0, 1)}, raddr)
+	if err != nil {
+		t.Fatalf("DialIP failed: %v", err)
+	}
+	defer c.Close()
+	if !reflect.DeepEqual(raddr, c.RemoteAddr()) {
+		t.Fatalf("got %#v, expected %#v", c.RemoteAddr(), raddr)
+	}
+}

src/pkg/net/sock_posix.go

--- a/src/pkg/net/sock_posix.go
+++ b/src/pkg/net/sock_posix.go
@@ -67,5 +67,8 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr,
 	rsa, _ := syscall.Getpeername(s)
 	raddr := toAddr(rsa)
 	fd.setAddr(laddr, raddr)
+	if fd.raddr == nil {
+		fd.raddr = toAddr(ursa)
+	}
 	return fd, nil
 }

コアとなるコードの解説

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

TestIPConnRemoteName という新しいテスト関数が追加されました。

  1. ルート権限のチェック: os.Getuid() != 0 で現在のユーザーがルート(UID 0)であるかを確認します。このテストは低レベルのIPソケット操作を含むため、ルート権限が必要です。ルート権限がない場合はテストをスキップします。
  2. リモートアドレスの定義: raddr := &IPAddr{IP: IPv4(127, 0, 0, 10).To4()} で、テスト用のリモートIPアドレス 127.0.0.10 を定義します。
  3. IP接続の確立: c, err := DialIP("ip:tcp", &IPAddr{IP: IPv4(127, 0, 0, 1)}, raddr) を使用して、ローカルアドレス 127.0.0.1 からリモートアドレス 127.0.0.10 へIP接続を試みます。プロトコルは ip:tcp (IPプロトコルでTCPヘッダを含む) です。
  4. エラーハンドリングとクリーンアップ: 接続に失敗した場合はテストを失敗させ、テスト終了時には defer c.Close() で接続を閉じます。
  5. リモートアドレスの検証: if !reflect.DeepEqual(raddr, c.RemoteAddr()) で、DialIP に渡したオリジナルの raddr と、確立された接続 c から取得したリモートアドレス c.RemoteAddr() が完全に一致するかどうかを reflect.DeepEqual を使って比較します。これにより、getpeername の結果に依存せず、接続時に指定されたリモートアドレスが正しく保持されていることを確認します。

このテストは、sock_posix.go の修正が、DialIP のような関数で指定されたリモートアドレスが、getpeername の結果に関わらず、最終的に RemoteAddr() で正しく取得できることを保証するためのものです。

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

socket 関数内の変更は、Goがソケットのファイルディスクリプタ fd のリモートアドレス fd.raddr を設定するロジックを改善しています。

	rsa, _ := syscall.Getpeername(s) // ソケット s のピア名(リモートアドレス)を取得
	raddr := toAddr(rsa)             // 取得したピア名を Go の net.Addr 型に変換
	fd.setAddr(laddr, raddr)         // fd にローカルアドレス laddr とリモートアドレス raddr を設定

	// ここから追加されたコード
	if fd.raddr == nil {             // もし fd.raddr が nil であれば(getpeername が失敗した場合など)
		fd.raddr = toAddr(ursa)      // 接続確立時に指定されたオリジナルのリモートアドレス ursa を使用する
	}
	return fd, nil
  • rsa, _ := syscall.Getpeername(s): 接続されたソケット s のリモートアドレスをシステムコール getpeername を使って取得しようとします。
  • raddr := toAddr(rsa): 取得したシステムコールのアドレス構造体 rsa をGoの net.Addr インターフェースに変換します。
  • fd.setAddr(laddr, raddr): ファイルディスクリプタ fd に、ローカルアドレス laddr と、getpeername から取得した raddr を設定します。
  • if fd.raddr == nil { fd.raddr = toAddr(ursa) }: これがこのコミットの核心です。
    • fd.raddrnil であるということは、syscall.Getpeername がリモートアドレスを正しく取得できなかったことを意味します。
    • このような場合、ursasocket 関数に渡された、接続確立時に指定されたオリジナルのリモートソケットアドレス)を toAddr で変換し、それを fd.raddr に代入します。
    • これにより、getpeername が失敗しても、接続の意図されたリモートアドレスが失われることなく、fd.raddr に正しく設定されるようになります。

この修正は、Goのネットワークスタックの堅牢性を高め、特定の条件下でのリモートアドレスの不整合を防ぐ上で非常に重要です。

関連リンク

参考にした情報源リンク