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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージ内の生IP(Raw IP)テストの堅牢性を向上させることを目的としています。具体的には、ICMP(Internet Control Message Protocol)関連のテストにおいて、パケットの検証が、テストコード内で渡されるブーリアンフラグ(ipv6)に依存するのではなく、基盤となるソケットのアドレスファミリー(IPv4またはIPv6)に直接基づくように変更されています。これにより、テストの信頼性と移植性が向上しています。

コミット

commit ee71afbb55cc16c87ac258ebb1c6a12c8729412b
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu Feb 23 06:27:05 2012 +0900

    net: make raw IP tests robust
    
    Make it rely on underlying socket's address family.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5673091

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

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

元コミット内容

このコミットは、src/pkg/net/ipraw_test.goファイルに対して行われました。主な変更点は以下の通りです。

  • syscallパッケージのインポートが追加されました。
  • exchangeICMPEchoおよびicmpEchoTransponder関数のシグネチャからipv6 bool引数が削除されました。
  • これらの関数内で、ICMPパケットのタイプ(リクエストまたはリプライ)を検証するロジックが、従来のipv6ブーリアンフラグに基づく条件分岐から、ソケットの基盤となるアドレスファミリー(syscall.AF_INETまたはsyscall.AF_INET6)を直接確認するswitchステートメントに変更されました。
  • newICMPEchoRequest関数のシグネチャがipv6 boolからnet stringに変更され、ネットワークタイプ文字列(例: "ip4:icmp", "ip6:icmp")に基づいて適切なICMPエコーリクエストを生成するようになりました。
  • エラーメッセージのフォーマットが%#qから%qに変更されました(これは機能的な変更ではありません)。

変更されたファイル:

  • src/pkg/net/ipraw_test.go: 38行追加、24行削除

変更の背景

Go言語のnetパッケージは、ネットワークI/Oのための抽象化されたインターフェースを提供します。その中でも、生IPソケット(Raw IP sockets)は、IP層のパケットを直接送受信する低レベルな機能を提供し、ICMP(pingなど)のようなプロトコルの実装に利用されます。

従来のnetパッケージの生IPテスト、特にICMP関連のテストでは、テスト対象がIPv4かIPv6かを判断するために、関数に渡されるipv6というブーリアンフラグに依存していました。しかし、このアプローチには潜在的な問題がありました。例えば、テスト環境のネットワーク設定やOSの挙動によっては、このフラグが実際のソケットの挙動と一致しない場合があり、テストが不安定になったり、誤ったプロトコルバージョンでパケットを検証してしまう可能性がありました。

このコミットは、このようなテストの不安定性や堅牢性の欠如を解消するために導入されました。テストがより信頼性の高い方法で、実際のソケットのアドレスファミリーに基づいてパケットを処理・検証できるようにすることが目的です。

前提知識の解説

このコミットの変更内容を理解するためには、以下の技術的な概念を把握しておく必要があります。

  • IP (Internet Protocol): インターネットにおけるデータ通信の基本となるプロトコル。IPv4とIPv6の2つの主要なバージョンがあります。
  • ICMP (Internet Control Message Protocol): IPネットワーク上でエラーメッセージや運用情報を交換するために使用されるプロトコル。pingコマンドはICMPエコーリクエスト/リプライを利用します。
  • 生IPソケット (Raw IP Sockets): アプリケーションがIPヘッダを含む生データを直接送受信できるソケット。TCPやUDPのような上位プロトコルを介さずに、IP層で直接通信を行う際に使用されます。
  • ソケットアドレスファミリー (Socket Address Family): ソケットを作成する際に指定する、通信に使用するアドレスの種類を示す定数。
    • AF_INET (syscall.AF_INET): IPv4アドレスを使用することを示します。
    • AF_INET6 (syscall.AF_INET6): IPv6アドレスを使用することを示します。
  • Go言語のnetパッケージ: Go言語でネットワークプログラミングを行うための主要なパッケージ。TCP/UDPソケット、IPアドレスの解決、ネットワークインターフェースの操作など、幅広い機能を提供します。
  • Go言語のsyscallパッケージ: オペレーティングシステム(OS)のシステムコールに直接アクセスするための低レベルなインターフェースを提供するパッケージ。ソケットのアドレスファミリー定数など、OS固有の定数や関数が含まれます。
  • Go言語のユニットテスト: testingパッケージを使用してテストコードを記述し、t.Errorfなどでテストの失敗を報告します。

技術的詳細

このコミットの核心は、ICMPテストにおけるパケットの検証ロジックを、より信頼性の高い方法に変更した点にあります。

変更前のアプローチの問題点: 以前のテストでは、exchangeICMPEchoicmpEchoTransponderといった関数にipv6 boolというブーリアン引数が渡されていました。このフラグは、テストがIPv6環境で実行されているかどうかを示し、受信したICMPパケットがIPv4のICMPエコーリプライ(ICMP4_ECHO_REPLY)なのか、IPv6のICMPエコーリプライ(ICMP6_ECHO_REPLY)なのかを判断するために使用されていました。

// 変更前のロジックの例 (簡略化)
if !ipv6 && reply[0] != ICMP4_ECHO_REPLY {
    // IPv4と期待されるが、IPv4リプライではない
    continue
}
if ipv6 && reply[0] != ICMP6_ECHO_REPLY {
    // IPv6と期待されるが、IPv6リプライではない
    continue
}

この方法では、ipv6フラグがテストの意図と実際のソケットの挙動を常に正確に反映しているとは限りませんでした。例えば、IPv6アドレスでソケットを作成したにもかかわらず、何らかの理由でipv6フラグがfalseになっていた場合、テストは誤ったICMPタイプを期待し、失敗する可能性がありました。

変更後の堅牢なアプローチ: このコミットでは、syscallパッケージを導入し、ソケットの基盤となるアドレスファミリーを直接確認するようになりました。net.IPConn型のソケットオブジェクトは、内部的にOSのファイルディスクリプタ(fd)を保持しており、このfdにはソケットがどのプロトコルファミリー(IPv4かIPv6か)で作成されたかの情報が含まれています。具体的には、c.(*IPConn).fd.familyを通じて、ソケットのアドレスファミリー(syscall.AF_INETまたはsyscall.AF_INET6)を取得できます。

// 変更後のロジックの例 (簡略化)
switch c.(*IPConn).fd.family {
case syscall.AF_INET: // ソケットがIPv4ファミリーで作成された場合
    if reply[0] != ICMP4_ECHO_REPLY {
        // IPv4リプライを期待
        continue
    }
case syscall.AF_INET6: // ソケットがIPv6ファミリーで作成された場合
    if reply[0] != ICMP6_ECHO_REPLY {
        // IPv6リプライを期待
        continue
    }
}

この変更により、テストは、渡されたブーリアンフラグではなく、実際にソケットがどのIPバージョンで動作しているかに基づいてICMPパケットのタイプを検証するようになりました。これにより、テストの信頼性が大幅に向上し、様々なネットワーク環境やOSの挙動に対する耐性が高まりました。

また、newICMPEchoRequest関数も同様に、ipv6 bool引数からnet string引数に変更されました。これにより、"ip4:icmp""ip6:icmp"といったネットワークタイプ文字列から、parseDialNetwork関数を通じて適切なアドレスファミリーを導出し、それに基づいてIPv4またはIPv6のICMPエコーリクエストパケットを生成するようになりました。これは、パケット生成ロジックとパケット検証ロジックの一貫性を保つための重要な変更です。

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

src/pkg/net/ipraw_test.goファイルにおける主要な変更箇所は以下の通りです。

  1. syscallパッケージのインポート追加:

    --- a/src/pkg/net/ipraw_test.go
    +++ b/src/pkg/net/ipraw_test.go
    @@ -7,6 +7,7 @@ package net
     import (
      "bytes"
      "os"
    + "syscall"
      "testing"
      "time"
     )
    
  2. exchangeICMPEcho関数のシグネチャとロジックの変更: ipv6 bool引数が削除され、ICMPリプライの検証にc.(*IPConn).fd.familyが使用されるようになりました。

    --- a/src/pkg/net/ipraw_test.go
    +++ b/src/pkg/net/ipraw_test.go
    @@ -34,15 +35,15 @@ func TestICMP(t *testing.T) {
      id := os.Getpid() & 0xffff
      seqnum++
    - echo := newICMPEchoRequest(tt.ipv6, id, seqnum, 128, []byte("Go Go Gadget Ping!!!"))
    - exchangeICMPEcho(t, tt.net, tt.laddr, tt.raddr, tt.ipv6, echo)
    + echo := newICMPEchoRequest(tt.net, id, seqnum, 128, []byte("Go Go Gadget Ping!!!"))
    + exchangeICMPEcho(t, tt.net, tt.laddr, tt.raddr, echo)
      }
     }
     
    -func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, ipv6 bool, echo []byte) {
    +func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, echo []byte) {
      c, err := ListenPacket(net, laddr)
      if err != nil {
    - t.Errorf("ListenPacket(%#q, %#q) failed: %v", net, laddr, err)
    + t.Errorf("ListenPacket(%q, %q) failed: %v", net, laddr, err)
      return
      }
      c.SetDeadline(time.Now().Add(100 * time.Millisecond))
    @@ -50,12 +51,12 @@ func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, ipv6 bool, echo []
     
      ra, err := ResolveIPAddr(net, raddr)
      if err != nil {
    - t.Errorf("ResolveIPAddr(%#q, %#q) failed: %v", net, raddr, err)
    + t.Errorf("ResolveIPAddr(%q, %q) failed: %v", net, raddr, err)
      return
      }
     
      waitForReady := make(chan bool)
    - go icmpEchoTransponder(t, net, raddr, ipv6, waitForReady)
    + go icmpEchoTransponder(t, net, raddr, waitForReady)
      <-waitForReady
     
      _, err = c.WriteTo(echo, ra)
    @@ -71,11 +72,15 @@ func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, ipv6 bool, echo []
      t.Errorf("ReadFrom failed: %v", err)
      return
      }
    - if !ipv6 && reply[0] != ICMP4_ECHO_REPLY {
    - continue
    - }
    - if ipv6 && reply[0] != ICMP6_ECHO_REPLY {
    - continue
    + switch c.(*IPConn).fd.family {
    + case syscall.AF_INET:
    + if reply[0] != ICMP4_ECHO_REPLY {
    + continue
    + }
    + case syscall.AF_INET6:
    + if reply[0] != ICMP6_ECHO_REPLY {
    + continue
    + }
      }
      xid, xseqnum := parseICMPEchoReply(echo)
      rid, rseqnum := parseICMPEchoReply(reply)
    
  3. icmpEchoTransponder関数のシグネチャとロジックの変更: ipv6 bool引数が削除され、ICMPリクエストの検証にc.(*IPConn).fd.familyが使用されるようになりました。

    --- a/src/pkg/net/ipraw_test.go
    +++ b/src/pkg/net/ipraw_test.go
    @@ -87,11 +92,11 @@ func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, ipv6 bool, echo []
      }
     }
     
    -func icmpEchoTransponder(t *testing.T, net, raddr string, ipv6 bool, waitForReady chan bool) {
    +func icmpEchoTransponder(t *testing.T, net, raddr string, waitForReady chan bool) {
      c, err := Dial(net, raddr)
      if err != nil {
      waitForReady <- true
    - t.Errorf("Dial(%#q, %#q) failed: %v", net, raddr, err)
    + t.Errorf("Dial(%q, %q) failed: %v", net, raddr, err)
      return
      }
      c.SetDeadline(time.Now().Add(100 * time.Millisecond))\
    @@ -106,18 +111,23 @@ func icmpEchoTransponder(t *testing.T, net, raddr string, ipv6 bool, waitForRead
      t.Errorf("Read failed: %v", err)
      return
      }
    - if !ipv6 && echo[0] != ICMP4_ECHO_REQUEST {
    - continue
    - }
    - if ipv6 && echo[0] != ICMP6_ECHO_REQUEST {
    - continue
    + switch c.(*IPConn).fd.family {
    + case syscall.AF_INET:
    + if echo[0] != ICMP4_ECHO_REQUEST {
    + continue
    + }
    + case syscall.AF_INET6:
    + if echo[0] != ICMP6_ECHO_REQUEST {
    + continue
    + }
      }
      break
      }
     
    - if !ipv6 {
    + switch c.(*IPConn).fd.family {
    + case syscall.AF_INET:
      echo[0] = ICMP4_ECHO_REPLY
    - } else {
    + case syscall.AF_INET6:
      echo[0] = ICMP6_ECHO_REPLY
      }
     
    
  4. newICMPEchoRequest関数のシグネチャとロジックの変更: ipv6 bool引数がnet stringに変更され、ネットワークタイプに基づいて適切なICMPリクエスト生成関数が呼び出されるようになりました。

    --- a/src/pkg/net/ipraw_test.go
    +++ b/src/pkg/net/ipraw_test.go
    @@ -135,11 +145,15 @@ const (
      ICMP6_ECHO_REPLY   = 129
     )
     
    -func newICMPEchoRequest(ipv6 bool, id, seqnum, msglen int, filler []byte) []byte {
    - if !ipv6 {
    +func newICMPEchoRequest(net string, id, seqnum, msglen int, filler []byte) []byte {
    + afnet, _, _ := parseDialNetwork(net)
    + switch afnet {
    + case "ip4":
      return newICMPv4EchoRequest(id, seqnum, msglen, filler)
    + case "ip6":
    + return newICMPv6EchoRequest(id, seqnum, msglen, filler)
      }
    - return newICMPv6EchoRequest(id, seqnum, msglen, filler)
    + return nil
     }
     
     func newICMPv4EchoRequest(id, seqnum, msglen int, filler []byte) []byte {
    

コアとなるコードの解説

このコミットの最も重要な変更は、net.IPConnオブジェクトの内部状態を利用して、ソケットがIPv4とIPv6のどちらであるかを動的に判断するようになった点です。

c.(*IPConn).fd.familyという表現は、以下の要素から構成されています。

  • c: net.PacketConnインターフェース型の変数。
  • c.(*IPConn): 型アサーション。cが基盤となる*net.IPConn型であることを確認し、その具体的な型に変換します。IPConnは生IPソケットを表すGoの構造体です。
  • .fd: IPConn構造体の内部フィールドで、ソケットのファイルディスクリプタに関する情報(OS固有のソケットハンドルなど)を保持しています。このフィールドはGoのnetパッケージ内部でのみアクセス可能です。
  • .family: fd構造体内のフィールドで、ソケットが作成されたアドレスファミリー(例: syscall.AF_INETsyscall.AF_INET6)を整数値で保持しています。

このfd.familyの値に基づいてswitchステートメントを使用することで、テストは受信したICMPパケットの最初のバイト(ICMPタイプを示す)が、現在のソケットのアドレスファミリーに合致しているかを正確に検証できます。例えば、ソケットがsyscall.AF_INET(IPv4)であればICMP4_ECHO_REPLYを期待し、syscall.AF_INET6(IPv6)であればICMP6_ECHO_REPLYを期待します。

この変更により、テストはより「自己認識的」になり、外部から与えられるブーリアンフラグに盲目的に依存するのではなく、自身のソケットの実際の特性に基づいて振る舞いを調整するようになりました。これにより、テストの信頼性が向上し、異なるOSやネットワーク設定下での誤検出や不安定性が減少します。

newICMPEchoRequest関数の変更も同様に重要です。以前はipv6 boolでパケットタイプを決定していましたが、新しい実装ではnet string(例: "ip4:icmp")を受け取り、parseDialNetwork関数を使ってその文字列からアドレスファミリーを抽出し、適切なICMPエコーリクエスト(IPv4またはIPv6)を生成します。これにより、パケットの生成と検証の両方が、ネットワークタイプ文字列という単一の信頼できる情報源に基づいて行われるようになり、全体的なテストフレームワークの一貫性と堅牢性が高まりました。

関連リンク

参考にした情報源リンク

  • Go Gerrit Change 5673091: https://golang.org/cl/5673091 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
  • Goのソースコード (src/pkg/net/ipraw_test.go): このコミットが適用された時点のGoのソースコード。