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

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

このコミットは、Go言語の標準ライブラリnetパッケージ内のipraw_test.goファイルに対する変更です。具体的には、IPコネクション(IPConn)をnet.Connおよびnet.PacketConnインターフェースを介してテストするための改善と、モックICMP(Internet Control Message Protocol)関連のコードのリファクタリングが行われています。

コミット

commit c02d18ab3456a9ba95506d081bf9099693e5ea73
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Sat Feb 16 12:55:39 2013 +0900

    net: add IPConn through Conn test
    
    Also refactors mock ICMP stuff.
    
    R=dave, rsc
    CC=golang-dev
    https://golang.org/cl/7325043

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

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

元コミット内容

このコミットの元の内容は以下の通りです。

net: add IPConn through Conn test

Also refactors mock ICMP stuff.

これは、netパッケージにおいてIPConnのテストをnet.Connインターフェースを介して追加し、同時にモックICMP関連のコードをリファクタリングしたことを示しています。

変更の背景

この変更の背景には、Go言語のnetパッケージにおけるネットワーク通信の抽象化とテストの堅牢性向上の必要性があります。

Goのnetパッケージは、TCP、UDP、IPなどの様々なネットワークプロトコルを扱うための統一されたインターフェースを提供します。net.Connはストリーム指向のネットワーク接続のための汎用インターフェースであり、net.PacketConnはパケット指向のネットワーク接続のための汎用インターフェースです。IPConnはIPプロトコルレベルでの通信を扱うための具体的な型ですが、これらの汎用インターフェースを通じて操作できることが重要です。

以前のICMPテストコードは、おそらく直接的なバイト操作やsyscallパッケージへの依存度が高く、コードの可読性、保守性、および移植性に課題があったと考えられます。特に、ICMPメッセージの構築や解析が手動で行われていた場合、プロトコルの詳細な仕様変更に対応するのが困難になります。

このコミットは、以下の目的で実施されました。

  1. IPConnのインターフェースを通じたテストの強化: IPConnnet.Connおよびnet.PacketConnインターフェースの期待通りに動作するかを確認するためのテストを追加・改善することで、netパッケージ全体の信頼性を高めます。これにより、将来的な変更や異なるプラットフォームでの動作保証が容易になります。
  2. ICMPモック処理のリファクタリング: ICMPメッセージの生成と解析をより構造化された方法で行うことで、テストコードの複雑性を軽減し、理解しやすく、変更しやすいものにします。これにより、ICMPプロトコルの詳細がテストコードに散らばることを防ぎ、テストの正確性と保守性を向上させます。
  3. syscall依存の低減: syscallパッケージへの直接的な依存を減らすことで、コードのプラットフォーム非依存性を高め、Goがサポートする様々なOSでの動作をよりスムーズにします。

前提知識の解説

1. ICMP (Internet Control Message Protocol)

ICMPは、IPネットワーク上でエラーメッセージや運用情報(例: ネットワークの到達可能性)を交換するために使用されるプロトコルです。TCPやUDPとは異なり、データ転送のためではなく、ネットワーク層での制御や診断のために利用されます。

  • ICMP Echo Request/Reply: 最もよく知られているICMPの機能は、"ping"コマンドで使用されるエコー要求(Echo Request)とエコー応答(Echo Reply)です。エコー要求は特定のホストに送信され、ホストはエコー応答を返します。これにより、ホストが到達可能であるか、ネットワーク遅延はどの程度かなどを確認できます。
  • ICMPメッセージの構造: ICMPメッセージはIPヘッダの後に続きます。メッセージの先頭にはタイプ(Type)とコード(Code)フィールドがあり、これらがメッセージの種類(例: エコー要求、宛先到達不能)を示します。エコー要求/応答の場合、これに加えて識別子(ID)とシーケンス番号(Sequence Number)が含まれ、これらは要求と応答を関連付けるために使用されます。

2. Go言語のnetパッケージ

Go言語のnetパッケージは、ネットワークI/Oのプリミティブを提供します。

  • net.Connインターフェース: ストリーム指向のネットワーク接続(例: TCP)を表すインターフェースです。ReadWriteCloseLocalAddrRemoteAddrSetDeadlineなどのメソッドを持ちます。
  • net.PacketConnインターフェース: パケット指向のネットワーク接続(例: UDP、IP)を表すインターフェースです。ReadFromWriteToCloseLocalAddrSetDeadlineなどのメソッドを持ちます。
  • net.IPConn: netパッケージが提供するIPプロトコルレベルの接続を表す具体的な型です。これはnet.Connnet.PacketConnの両方のインターフェースを満たします。
  • ListenPacketDial:
    • net.ListenPacket(network, address string): 指定されたネットワークとアドレスでパケット接続をリッスンします。
    • net.Dial(network, address string): 指定されたネットワークとアドレスに接続を確立します。

3. syscallパッケージ

Go言語のsyscallパッケージは、オペレーティングシステムの低レベルなプリミティブ(システムコール)へのアクセスを提供します。ネットワークプログラミングにおいては、ソケットの作成や設定、特定のプロトコルオプションの操作などに使用されることがあります。しかし、netパッケージのような高レベルな抽象化が提供されている場合、直接syscallを使用することは稀であり、プラットフォーム依存のコードになる傾向があります。このコミットでは、syscallへの直接的な依存を減らすことで、よりポータブルなコードを目指しています。

技術的詳細

このコミットの技術的な詳細は、主にICMPメッセージの構造化された扱いと、それを利用したIPConnのテスト方法の改善にあります。

1. ICMPメッセージの構造化

以前のテストコードでは、ICMPメッセージのバイト列を直接操作していましたが、このコミットでは以下の新しい構造体が導入されました。

  • icmpMessage struct:

    type icmpMessage struct {
        Type     int             // type
        Code     int             // code
        Checksum int             // checksum
        Body     icmpMessageBody // body
    }
    

    これは一般的なICMPメッセージのヘッダ部分(タイプ、コード、チェックサム)と、メッセージのペイロード(Body)を抽象化します。BodyicmpMessageBodyインターフェースを満たす任意の型を取ることができます。

  • icmpMessageBody interface:

    type icmpMessageBody interface {
        Len() int
        Marshal() ([]byte, error)
    }
    

    これはICMPメッセージのボディ部分が満たすべきインターフェースです。Len()はボディの長さを返し、Marshal()はボディをバイト列に変換します。

  • icmpEcho struct:

    type icmpEcho struct {
        ID   int    // identifier
        Seq  int    // sequence number
        Data []byte // data
    }
    

    これはICMPエコー要求/応答メッセージのボディ部分を具体的に表します。識別子(ID)、シーケンス番号(Seq)、およびデータ(Data)を含みます。

これらの構造体とインターフェースの導入により、ICMPメッセージの構築と解析が、バイト列のオフセット計算やマジックナンバーの直接使用から解放され、より型安全で可読性の高いコードになりました。

2. ICMPメッセージのシリアライズとデシリアライズ

  • (*icmpMessage).Marshal()メソッド: icmpMessage構造体をバイト列に変換するメソッドです。このメソッドは、ICMPヘッダ(タイプ、コード、チェックサム)とボディのバイト列を結合し、ICMPチェックサムを計算してヘッダに埋め込みます。IPv6 ICMP(ICMPv6)の場合、チェックサムの計算は異なります(このコードでは単純にスキップされていますが、実際にはIPv6の擬似ヘッダを含むチェックサム計算が必要です)。

  • parseICMPMessage(b []byte) (*icmpMessage, error)関数: バイト列bを解析してicmpMessage構造体に変換する関数です。メッセージのタイプに基づいて適切なicmpMessageBodyの実装(例: icmpEcho)を解析し、icmpMessage構造体に格納します。

  • (*icmpEcho).Marshal()parseICMPEcho(b []byte) (*icmpEcho, error): icmpEcho構造体をバイト列に変換するメソッドと、バイト列をicmpEcho構造体に解析する関数です。これらはID、Seq、Dataフィールドを適切にバイト列にマッピングします。

3. IPv4ペイロードの抽出

  • ipv4Payload(b []byte) []byte関数:
    func ipv4Payload(b []byte) []byte {
        if len(b) < 20 {
            return b
        }
        hdrlen := int(b[0]&0x0f) << 2
        return b[hdrlen:]
    }
    
    このヘルパー関数は、受信したIPv4パケットのバイト列から、IPヘッダを除いたペイロード部分(つまり、ICMPメッセージが始まる部分)を抽出します。IPv4ヘッダの長さは可変であり、通常は先頭バイトの下位4ビットで示される「IHL (Internet Header Length)」フィールドから計算されます。b[0]&0x0fでIHLを取得し、それに4を掛けることでヘッダ長(バイト単位)を計算します。

4. テストの改善

  • TestConnICMPEchoTestPacketConnICMPEcho: 以前のTestICMP関数が、net.Connインターフェースを使用するTestConnICMPEchoと、net.PacketConnインターフェースを使用するTestPacketConnICMPEchoに分割されました。これにより、それぞれのインターフェースの動作がより明確にテストされます。 これらのテストでは、新しく導入されたicmpMessageicmpEcho構造体を使用して、ICMPエコー要求メッセージを構築し、ネットワーク経由で送信し、応答を受信して解析します。

  • syscallの削除: 以前のコードでは、syscall.AF_INETsyscall.AF_INET6といった定数を使用してソケットのファミリーを判別していましたが、このコミットではsyscallパッケージへの依存が削除されました。これは、netパッケージが提供する高レベルな抽象化を利用することで、プラットフォーム固有のシステムコールを直接扱う必要がなくなったことを示唆しています。

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

このコミットのコアとなる変更は、src/pkg/net/ipraw_test.goファイルにおけるICMPメッセージの構造化と、それを利用したテストロジックの刷新です。

1. ICMPメッセージ構造体の追加

--- a/src/pkg/net/ipraw_test.go
+++ b/src/pkg/net/ipraw_test.go
@@ -196,30 +196,100 @@ func icmpEchoTransponder(t *testing.T, net, raddr string, waitForReady chan bool) {
 }
 
 const (
-	ICMP4_ECHO_REQUEST = 8
-	ICMP4_ECHO_REPLY   = 0
-	ICMP6_ECHO_REQUEST = 128
-	ICMP6_ECHO_REPLY   = 129
+	icmpv4EchoRequest = 8
+	icmpv4EchoReply   = 0
+	icmpv6EchoRequest = 128
+	icmpv6EchoReply   = 129
 )
 
-func newICMPEchoRequest(net string, id, seqnum, msglen int, filler []byte) []byte {
-	afnet, _, _ := parseNetwork(net)
-	switch afnet {
-	case "ip4":
-		return newICMPv4EchoRequest(id, seqnum, msglen, filler)
-	case "ip6":
-		return newICMPv6EchoRequest(id, seqnum, msglen, filler)
-	}
-	return nil
+// icmpMessage represents an ICMP message.
+type icmpMessage struct {
+	Type     int             // type
+	Code     int             // code
+	Checksum int             // checksum
+	Body     icmpMessageBody // body
 }
 
-func newICMPv4EchoRequest(id, seqnum, msglen int, filler []byte) []byte {
-	b := newICMPInfoMessage(id, seqnum, msglen, filler)
-	b[0] = ICMP4_ECHO_REQUEST
+// icmpMessageBody represents an ICMP message body.
+type icmpMessageBody interface {
+	Len() int
+	Marshal() ([]byte, error)
+}
+
+// Marshal returns the binary enconding of the ICMP echo request or
+// reply message m.
+func (m *icmpMessage) Marshal() ([]byte, error) {
+	b := []byte{byte(m.Type), byte(m.Code), 0, 0}
+	if m.Body != nil && m.Body.Len() != 0 {
+		mb, err := m.Body.Marshal()
+		if err != nil {
+			return nil, err
+		}
+		b = append(b, mb...)
+	}
+	switch m.Type {
+	case icmpv6EchoRequest, icmpv6EchoReply:
+		return b, nil
+	}
+	csumcv := len(b) - 1 // checksum coverage
+	s := uint32(0)
+	for i := 0; i < csumcv; i += 2 {
+		s += uint32(b[i+1])<<8 | uint32(b[i])
+	}
+	if csumcv&1 == 0 {
+		s += uint32(b[csumcv])
+	}
+	s = s>>16 + s&0xffff
+	s = s + s>>16
+	// Place checksum back in header; using ^= avoids the
+	// assumption the checksum bytes are zero.
+	b[2] ^= byte(^s & 0xff)
+	b[3] ^= byte(^s >> 8)
+	return b, nil
+}
+
+// parseICMPMessage parses b as an ICMP message.
+func parseICMPMessage(b []byte) (*icmpMessage, error) {
+	msglen := len(b)
+	if msglen < 4 {
+		return nil, errors.New("message too short")
+	}
+	m := &icmpMessage{Type: int(b[0]), Code: int(b[1]), Checksum: int(b[2])<<8 | int(b[3])}
+	if msglen > 4 {
+		var err error
+		switch m.Type {
+		case icmpv4EchoRequest, icmpv4EchoReply, icmpv6EchoRequest, icmpv6EchoReply:
+			m.Body, err = parseICMPEcho(b[4:])
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	return m, nil
+}
+
+// imcpEcho represenets an ICMP echo request or reply message body.
+type icmpEcho struct {
+	ID   int    // identifier
+	Seq  int    // sequence number
+	Data []byte // data
+}
+
+func (p *icmpEcho) Len() int {
+	if p == nil {
+		return 0
+	}
+	return 4 + len(p.Data)
+}
+
+// Marshal returns the binary enconding of the ICMP echo request or
+// reply message body p.
+func (p *icmpEcho) Marshal() ([]byte, error) {
+	b := make([]byte, 4+len(p.Data))
+	b[0], b[1] = byte(p.ID>>8), byte(p.ID&0xff)
+	b[2], b[3] = byte(p.Seq>>8), byte(p.Seq&0xff)
+	copy(b[4:], p.Data)
+	return b, nil
+}
+
+// parseICMPEcho parses b as an ICMP echo request or reply message
+// body.
+func parseICMPEcho(b []byte) (*icmpEcho, error) {
+	bodylen := len(b)
+	p := &icmpEcho{ID: int(b[0])<<8 | int(b[1]), Seq: int(b[2])<<8 | int(b[3])}
+	if bodylen > 4 {
+		p.Data = make([]byte, bodylen-4)
+		copy(p.Data, b[4:])
+	}
+	return p, nil
+}

2. テストロジックの変更

TestICMP関数が削除され、TestConnICMPEchoTestPacketConnICMPEchoが追加されました。これらの新しいテスト関数は、上記の新しいICMPメッセージ構造体とマーシャリング/アンマーシャリング関数を利用しています。

TestConnICMPEchoの抜粋:

--- a/src/pkg/net/ipraw_test.go
+++ b/src/pkg/net/ipraw_test.go
@@ -49,196 +49,269 @@ func TestResolveIPAddr(t *testing.T) {
 	}
 }
 
-var icmpTests = []struct {
+var icmpEchoTests = []struct {
 	net   string
 	laddr string
 	raddr string
-	ipv6  bool // test with underlying AF_INET6 socket
 } {
-	{"ip4:icmp", "", "127.0.0.1", false},
-	{"ip6:ipv6-icmp", "", "::1", true},
+	{"ip4:icmp", "0.0.0.0", "127.0.0.1"},
+	{"ip6:ipv6-icmp", "::", "::1"},
 }
 
-func TestICMP(t *testing.T) {
+func TestConnICMPEcho(t *testing.T) {
 	if os.Getuid() != 0 {
 		t.Skip("skipping test; must be root")
 	}
 
-	seqnum := 61455
-	for _, tt := range icmpTests {
-		if tt.ipv6 && !supportsIPv6 {
+	for i, tt := range icmpEchoTests {
+		net, _, err := parseNetwork(tt.net)
+		if err != nil {
+			t.Fatalf("parseNetwork failed: %v", err)
+		}
+		if net == "ip6" && !supportsIPv6 {
 			continue
 		}
-		id := os.Getpid() & 0xffff
-		seqnum++
-		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, echo []byte) {
-	c, err := ListenPacket(net, laddr)
-	if err != nil {
-		t.Errorf("ListenPacket(%q, %q) failed: %v", net, laddr, err)
-		return
-	}
-	c.SetDeadline(time.Now().Add(100 * time.Millisecond))
-	defer c.Close()
-
-	ra, err := ResolveIPAddr(net, raddr)
-	if err != nil {
-		t.Errorf("ResolveIPAddr(%q, %q) failed: %v", net, raddr, err)
-		return
-	}
-
-	waitForReady := make(chan bool)
-	go icmpEchoTransponder(t, net, raddr, waitForReady)
-	<-waitForReady
-
-	_, err = c.WriteTo(echo, ra)
-	if err != nil {
-		t.Errorf("WriteTo failed: %v", err)
-		return
-	}
-
-	reply := make([]byte, 256)
-	for {
-		_, _, err := c.ReadFrom(reply)
-		if err != nil {
-			t.Errorf("ReadFrom failed: %v", err)
-			return
-		}
-		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)
-		if rid != xid || rseqnum != xseqnum {
-			t.Errorf("ID = %v, Seqnum = %v, want ID = %v, Seqnum = %v", rid, rseqnum, xid, xseqnum)
-			return
-		}
-		break
-	}
-}
-
-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)
-		return
-	}
-	c.SetDeadline(time.Now().Add(100 * time.Millisecond))
-	defer c.Close()
-	waitForReady <- true
-
-	echo := make([]byte, 256)
-	var nr int
-	for {
-		nr, err = c.Read(echo)
-		if err != nil {
-			t.Errorf("Read failed: %v", err)
-			return
-		}
-		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
-	}
-
-	switch c.(*IPConn).fd.family {
-	case syscall.AF_INET:
-		echo[0] = ICMP4_ECHO_REPLY
-	case syscall.AF_INET6:
-		echo[0] = ICMP6_ECHO_REPLY
-	}
-
-	_, err = c.Write(echo[:nr])
-	if err != nil {
-		t.Errorf("Write failed: %v", err)
-		return
-	}
+		c, err := Dial(tt.net, tt.raddr)
+		if err != nil {
+			t.Fatalf("Dial failed: %v", err)
+		}
+		c.SetDeadline(time.Now().Add(100 * time.Millisecond))
+		defer c.Close()
+
+		typ := icmpv4EchoRequest
+		if net == "ip6" {
+			typ = icmpv6EchoRequest
+		}
+		xid, xseq := os.Getpid()&0xffff, i+1
+		b, err := (&icmpMessage{
+			Type: typ, Code: 0,
+			Body: &icmpEcho{
+				ID: xid, Seq: xseq,
+				Data: bytes.Repeat([]byte("Go Go Gadget Ping!!!"), 3),
+			},
+		}).Marshal()
+		if err != nil {
+			t.Fatalf("icmpMessage.Marshal failed: %v", err)
+		}
+		if _, err := c.Write(b); err != nil {
+			t.Fatalf("Conn.Write failed: %v", err)
+		}
+		var m *icmpMessage
+		for {
+			if _, err := c.Read(b); err != nil {
+				t.Fatalf("Conn.Read failed: %v", err)
+			}
+			if net == "ip4" {
+				b = ipv4Payload(b)
+			}
+			if m, err = parseICMPMessage(b); err != nil {
+				t.Fatalf("parseICMPMessage failed: %v", err)
+			}
+			switch m.Type {
+			case icmpv4EchoRequest, icmpv6EchoRequest:
+				continue
+			}
+			break
+		}
+		switch p := m.Body.(type) {
+		case *icmpEcho:
+			if p.ID != xid || p.Seq != xseq {
+				t.Fatalf("got id=%v, seqnum=%v; expected id=%v, seqnum=%v", p.ID, p.Seq, xid, xseq)
+			}
+		default:
+			t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, typ, 0)
+		}
+	}
 }
 
-const (
-	ICMP4_ECHO_REQUEST = 8
-	ICMP4_ECHO_REPLY   = 0
-	ICMP6_ECHO_REQUEST = 128
-	ICMP6_ECHO_REPLY   = 129
-)
-
-func newICMPEchoRequest(net string, id, seqnum, msglen int, filler []byte) []byte {
-	afnet, _, _ := parseNetwork(net)
-	switch afnet {
-	case "ip4":
-		return newICMPv4EchoRequest(id, seqnum, msglen, filler)
-	case "ip6":
-		return newICMPv6EchoRequest(id, seqnum, msglen, filler)
-	}
-	return nil
+func TestPacketConnICMPEcho(t *testing.T) {
+	if os.Getuid() != 0 {
+		t.Skip("skipping test; must be root")
+	}
+
+	for i, tt := range icmpEchoTests {
+		net, _, err := parseNetwork(tt.net)
+		if err != nil {
+			t.Fatalf("parseNetwork failed: %v", err)
+		}
+		if net == "ip6" && !supportsIPv6 {
+			continue
+		}
+
+		c, err := ListenPacket(tt.net, tt.laddr)
+		if err != nil {
+			t.Fatalf("ListenPacket failed: %v", err)
+		}
+		c.SetDeadline(time.Now().Add(100 * time.Millisecond))
+		defer c.Close()
+
+		ra, err := ResolveIPAddr(tt.net, tt.raddr)
+		if err != nil {
+			t.Fatalf("ResolveIPAddr failed: %v", err)
+		}
+		typ := icmpv4EchoRequest
+		if net == "ip6" {
+			typ = icmpv6EchoRequest
+		}
+		xid, xseq := os.Getpid()&0xffff, i+1
+		b, err := (&icmpMessage{
+			Type: typ, Code: 0,
+			Body: &icmpEcho{
+				ID: xid, Seq: xseq,
+				Data: bytes.Repeat([]byte("Go Go Gadget Ping!!!"), 3),
+			},
+		}).Marshal()
+		if err != nil {
+			t.Fatalf("icmpMessage.Marshal failed: %v", err)
+		}
+		if _, err := c.WriteTo(b, ra); err != nil {
+			t.Fatalf("PacketConn.WriteTo failed: %v", err)
+		}
+		var m *icmpMessage
+		for {
+			if _, _, err := c.ReadFrom(b); err != nil {
+				t.Fatalf("PacketConn.ReadFrom failed: %v", err)
+			}
+			// TODO: fix issue 3944
+			//if net == "ip4" {
+			//	b = ipv4Payload(b)
+			//}
+			if m, err = parseICMPMessage(b); err != nil {
+				t.Fatalf("parseICMPMessage failed: %v", err)
+			}
+			switch m.Type {
+			case icmpv4EchoRequest, icmpv6EchoRequest:
+				continue
+			}
+			break
+		}
+		switch p := m.Body.(type) {
+		case *icmpEcho:
+			if p.ID != xid || p.Seq != xseq {
+				t.Fatalf("got id=%v, seqnum=%v; expected id=%v, seqnum=%v", p.ID, p.Seq, xid, xseq)
+			}
+		default:
+			t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, typ, 0)
+		}
+	}
 }
 
-func newICMPv4EchoRequest(id, seqnum, msglen int, filler []byte) []byte {
-	b := newICMPInfoMessage(id, seqnum, msglen, filler)
-	b[0] = ICMP4_ECHO_REQUEST
-
-	// calculate ICMP checksum
-	cklen := len(b)
-	s := uint32(0)
-	for i := 0; i < cklen-1; i += 2 {
-		s += uint32(b[i+1])<<8 | uint32(b[i])
-	}
-	if cklen&1 == 1 {
-		s += uint32(b[cklen-1])
-	}
-	s = (s >> 16) + (s & 0xffff)
-	s = s + (s >> 16)
-	// place checksum back in header; using ^= avoids the
-	// assumption the checksum bytes are zero
-	b[2] ^= uint8(^s & 0xff)
-	b[3] ^= uint8(^s >> 8)
-
-	return b
-}
-
-func newICMPv6EchoRequest(id, seqnum, msglen int, filler []byte) []byte {
-	b := newICMPInfoMessage(id, seqnum, msglen, filler)
-	b[0] = ICMP6_ECHO_REQUEST
-	return b
-}
-
-func newICMPInfoMessage(id, seqnum, msglen int, filler []byte) []byte {
-	b := make([]byte, msglen)
-	copy(b[8:], bytes.Repeat(filler, (msglen-8)/len(filler)+1))
-	b[0] = 0                    // type
-	b[1] = 0                    // code
-	b[2] = 0                    // checksum
-	b[3] = 0                    // checksum
-	b[4] = uint8(id >> 8)       // identifier
-	b[5] = uint8(id & 0xff)     // identifier
-	b[6] = uint8(seqnum >> 8)   // sequence number
-	b[7] = uint8(seqnum & 0xff) // sequence number
-	return b
-}
-
-func parseICMPEchoReply(b []byte) (id, seqnum int) {
-	id = int(b[4])<<8 | int(b[5])
-	seqnum = int(b[6])<<8 | int(b[7])
-	return
-}

3. ipv4Payload関数の追加

--- a/src/pkg/net/ipraw_test.go
+++ b/src/pkg/net/ipraw_test.go
@@ -258,14 +331,11 @@ func TestIPConnLocalName(t *testing.T) {
 	}
 }
 
+func ipv4Payload(b []byte) []byte {
+	if len(b) < 20 {
+		return b
+	}
+	hdrlen := int(b[0]&0x0f) << 2
+	return b[hdrlen:]
+}
+

コアとなるコードの解説

1. ICMPメッセージ構造体の追加と関連メソッド

  • icmpMessageicmpMessageBodyicmpEcho: これらの構造体とインターフェースは、ICMPメッセージの各部分をGoの型システムで表現するためのものです。これにより、バイト列の直接操作ではなく、構造体のフィールドにアクセスすることで、メッセージの構築や解析が可能になります。これは、コードの可読性と保守性を大幅に向上させます。
  • (*icmpMessage).Marshal(): このメソッドは、icmpMessage構造体の内容をICMPプロトコル仕様に準拠したバイト列に変換します。特に重要なのは、ICMPチェックサムの計算ロジックがここにカプセル化されている点です。これにより、テストコードの他の部分でチェックサム計算の詳細を意識する必要がなくなります。
  • parseICMPMessage(): この関数は、受信したICMPバイト列をicmpMessage構造体に解析します。メッセージのタイプに応じて適切なボディ(例: icmpEcho)をデコードするロジックが含まれています。
  • (*icmpEcho).Marshal()parseICMPEcho(): これらはicmpEcho構造体(ID、Seq、Data)とバイト列間の変換を処理します。

これらの変更により、ICMPメッセージの扱いが「バイト列のオフセットとマスクによる手動操作」から「型安全な構造体とメソッドによる操作」へと進化しました。

2. テストロジックの変更

  • TestConnICMPEchoTestPacketConnICMPEcho: これらの新しいテスト関数は、net.Connnet.PacketConnインターフェースを介してIPConnのICMPエコー機能をテストします。
    • テストはまず、net.Dialまたはnet.ListenPacketを使用してIP接続を確立します。
    • 次に、icmpMessageicmpEcho構造体を使用してICMPエコー要求メッセージを構築し、(*icmpMessage).Marshal()でバイト列に変換します。
    • c.Write(b)net.Connの場合)またはc.WriteTo(b, ra)net.PacketConnの場合)を使用して、構築したICMPエコー要求を送信します。
    • c.Read(b)またはc.ReadFrom(b)で応答を受信し、parseICMPMessage()で解析します。
    • 受信したメッセージのタイプとボディ(特にIDとSeq)が期待通りであることをアサートします。
    • os.Getuid() != 0によるroot権限チェックは、RAWソケット(ICMP通信に必要)の作成には通常root権限が必要であるためです。

このテストの変更は、netパッケージのインターフェースをより適切に利用し、ICMPメッセージの生成と解析を抽象化することで、テストコードの堅牢性と可読性を向上させています。

3. ipv4Payload関数の追加

  • ipv4Payload(b []byte) []byte: この関数は、net.Conn.Readnet.PacketConn.ReadFromで受信したバイト列が完全なIPパケットである場合に、そのIPヘッダを取り除き、ペイロード(この場合はICMPメッセージ)のみを返すために使用されます。IPv4ヘッダの長さは可変であるため、この関数はIPヘッダのIHLフィールドを読み取り、正確なペイロードの開始位置を特定します。これにより、parseICMPMessage関数は純粋なICMPメッセージのバイト列を受け取ることができ、関心の分離が実現されます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/pkg/net/ipraw_test.goの変更前後の比較)
  • ICMPプロトコル仕様(RFC 792など)
  • Go言語のnetパッケージに関する一般的なドキュメントやチュートリアル
  • Go言語のsyscallパッケージに関する情報
  • Go issue 3944: https://github.com/golang/go/issues/3944 (このコミットのコードコメントで言及されているTODO項目。このコミット自体が直接解決するものではないが、関連する課題として参照される。)

注記: TestPacketConnICMPEcho内の// TODO: fix issue 3944コメントは、このコミットが特定の課題(IPv4ペイロードの抽出に関するものと思われる)を完全に解決しているわけではなく、将来的な修正が必要であることを示唆しています。