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

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

このコミットは、Go言語のnetパッケージにおけるテストコードの整理を目的としています。具体的には、src/pkg/net/ipraw_test.goファイル内に存在していたICMP(Internet Control Message Protocol)のモック実装に関するコードを、新しく作成されたsrc/pkg/net/mockicmp_test.goファイルに移動しています。これにより、テストコードの関心事を分離し、コードの可読性と保守性を向上させています。

コミット

commit fe62a1f1feadabf6de15e524090c8010449890ff
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu Sep 12 11:59:18 2013 +0900

    net: move mock ICMP into separate file
    
    This is in prepartion for fixing issue 6320.
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/13611043

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

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

元コミット内容

net: move mock ICMP into separate file

This is in prepartion for fixing issue 6320.

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/13611043

変更の背景

この変更の主な背景は、netパッケージ内のテストコードの整理と、将来的なバグ修正(issue 6320)への準備です。

src/pkg/net/ipraw_test.goファイルは、IP rawソケットのテストに関連するコードを含んでいました。しかし、このファイル内には、ICMPメッセージのエンコード・デコードを行うためのモック実装も含まれていました。このようなモック実装は、特定のテストケースでのみ使用される補助的なコードであり、メインのテストロジックとは異なる関心事を持っています。

コードベースが成長するにつれて、単一のファイルに多くの異なる関心事のコードが混在すると、以下の問題が発生します。

  1. 可読性の低下: ファイルが肥大化し、特定の機能やテストケースに関連するコードを見つけにくくなります。
  2. 保守性の低下: ある機能の変更が、別の機能のコードに意図しない影響を与える可能性があります。また、関連性の低いコードが同じファイルにあると、変更の範囲を特定しにくくなります。
  3. 再利用性の低下: モック実装が特定のテストファイルに埋め込まれていると、他のテストファイルで同じモックが必要になった場合に、コードの重複が発生しやすくなります。

このコミットは、これらの問題を解決するために、ICMPモック実装を専用のファイルsrc/pkg/net/mockicmp_test.goに分離しました。これにより、ipraw_test.goはIP rawソケットのテストに特化し、mockicmp_test.goはICMPモックの定義に特化することで、それぞれのファイルの役割が明確になり、コードベース全体の健全性が向上します。

また、コミットメッセージには「This is in prepartion for fixing issue 6320.」と明記されており、このコード整理が特定のバグ修正のための前提作業であることが示唆されています。残念ながら、issue 6320に関する具体的な情報は今回の検索では見つかりませんでしたが、テストコードの整理が複雑なバグの特定や修正を容易にするための一般的なプラクティスであることは間違いありません。

前提知識の解説

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

1. ICMP (Internet Control Message Protocol)

ICMPは、TCP/IPプロトコルスイートの一部であり、主にIPネットワーク上でのエラー報告や診断情報(例: ネットワークデバイスが到達不能である、パケットの寿命が尽きたなど)の交換に使用されます。最もよく知られているICMPの利用例は、pingコマンドです。pingはICMPエコー要求メッセージを送信し、対象ホストからのICMPエコー応答メッセージを待ち受けることで、ネットワーク接続性を確認します。

ICMPメッセージはIPデータグラムのペイロードとしてカプセル化されます。ICMPメッセージの構造は、タイプ(Type)、コード(Code)、チェックサム(Checksum)、そしてデータ部分から構成されます。

  • タイプ (Type): ICMPメッセージの種類を示します(例: エコー要求、エコー応答、宛先到達不能など)。
  • コード (Code): タイプをさらに細分化する情報を提供します。
  • チェックサム (Checksum): メッセージの整合性を確認するための値です。
  • データ部分: メッセージの種類に応じて、追加の情報が含まれます。例えば、エコー要求/応答メッセージには識別子(ID)とシーケンス番号(Seq)、そして任意のデータが含まれます。

ICMPにはIPv4用のICMPv4とIPv6用のICMPv6があり、それぞれメッセージタイプやコードが異なります。このコミットで扱われているのは、icmpv4EchoRequest (8), icmpv4EchoReply (0), icmpv6EchoRequest (128), icmpv6EchoReply (129) といったエコーメッセージ関連の定数です。

2. Raw IP Sockets (IP Rawソケット)

通常のTCPやUDPソケットは、トランスポート層のプロトコル(TCPやUDP)を抽象化し、アプリケーションがデータグラムの送受信に集中できるようにします。しかし、Raw IPソケットは、IP層のデータグラムを直接送受信するためのインターフェースを提供します。これにより、アプリケーションはIPヘッダやそのペイロード(この場合、ICMPメッセージ)を直接構築・解析することができます。

Raw IPソケットは、以下のような用途で利用されます。

  • ネットワーク診断ツール: pingtracerouteのように、ICMPメッセージを直接送受信するツール。
  • 新しいプロトコルの実装: 標準のトランスポート層プロトコルでは対応できない、カスタムプロトコルの実装。
  • セキュリティツール: パケットのキャプチャやインジェクション。

Go言語のnetパッケージは、Raw IPソケットを含む様々なネットワークプログラミング機能を提供しています。ipraw_test.goは、これらのRaw IPソケットの機能が正しく動作するかを検証するためのテストコードを含んでいます。

3. Go言語のテストとパッケージ構造

Go言語では、テストファイルは通常、テスト対象のソースファイルと同じディレクトリに配置され、ファイル名の末尾に_test.goが付きます。例えば、foo.goのテストはfoo_test.goに記述されます。

また、Goのパッケージはディレクトリ構造に対応しており、src/pkg/netnetパッケージのソースコードを格納しています。テストファイルも同じパッケージの一部として扱われますが、テスト専用のコード(モックやヘルパー関数など)は、テスト対象のコードとは別に管理されることが推奨されます。

このコミットは、このようなGo言語のテストとパッケージ構造のベストプラクティスに従い、テスト専用のモックコードを独立したファイルに分離することで、コードベースの品質を向上させています。

技術的詳細

このコミットの技術的な詳細は、ICMPメッセージの構造をGoの構造体とインターフェースで表現し、それらをバイナリデータにマーシャリング(エンコード)したり、バイナリデータからアンマーシャリング(デコード)したりするロジックを、特定のテストファイルから独立したテストファイルに移動した点にあります。

移動されたコードは、主に以下のGoの型と関数で構成されています。

  1. 定数:

    • icmpv4EchoRequest (8)
    • icmpv4EchoReply (0)
    • icmpv6EchoRequest (128)
    • icmpv6EchoReply (129) これらは、ICMPエコーメッセージのタイプを示す定数です。
  2. icmpMessage 構造体: ICMPメッセージのヘッダ部分を表現します。

    type icmpMessage struct {
        Type     int             // type
        Code     int             // code
        Checksum int             // checksum
        Body     icmpMessageBody // body
    }
    
    • Type: ICMPメッセージのタイプ。
    • Code: ICMPメッセージのコード。
    • Checksum: ICMPヘッダとデータのチェックサム。
    • Body: ICMPメッセージのペイロード部分を表すインターフェース。
  3. icmpMessageBody インターフェース: ICMPメッセージのボディ(ペイロード)部分が満たすべきインターフェースです。

    type icmpMessageBody interface {
        Len() int
        Marshal() ([]byte, error)
    }
    
    • Len(): ボディの長さをバイト単位で返します。
    • Marshal(): ボディをバイナリ形式にエンコードします。
  4. icmpMessage.Marshal() メソッド: icmpMessage構造体をバイナリ形式のバイトスライスにエンコードするメソッドです。ICMPヘッダ(タイプ、コード、チェックサム)とボディを結合し、チェックサムを計算してヘッダに埋め込むロジックが含まれています。特に、チェックサムの計算は、ネットワークプロトコルにおけるデータの整合性保証の重要な側面です。

  5. parseICMPMessage() 関数: バイナリ形式のバイトスライスからicmpMessage構造体を解析(デコード)する関数です。メッセージの長さチェック、タイプ、コード、チェックサムの抽出、そしてボディの解析(エコーメッセージの場合)を行います。

  6. icmpEcho 構造体: ICMPエコー要求/応答メッセージのボディ部分を表現します。

    type icmpEcho struct {
        ID   int    // identifier
        Seq  int    // sequence number
        Data []byte // data
    }
    
    • ID: エコー要求/応答の識別子。
    • Seq: エコー要求/応答のシーケンス番号。
    • Data: 任意のデータペイロード。
  7. icmpEcho.Len() メソッド: icmpEchoボディの長さを返します。

  8. icmpEcho.Marshal() メソッド: icmpEcho構造体をバイナリ形式のバイトスライスにエンコードするメソッドです。識別子、シーケンス番号、データを結合します。

  9. parseICMPEcho() 関数: バイナリ形式のバイトスライスからicmpEcho構造体を解析(デコード)する関数です。

これらのコードは、ICMPメッセージの構造と処理ロジックをGo言語で忠実に再現したものであり、ネットワークプロトコルのテストにおいて、実際のICMPパケットを生成・解析する代わりに、プログラム内でICMPメッセージをシミュレートするために使用されます。

このコミットでは、これらのICMPモック関連のコードがsrc/pkg/net/ipraw_test.goから完全に削除され、src/pkg/net/mockicmp_test.goという新しいファイルにそのままコピーされました。これにより、ipraw_test.goはIP rawソケットのテストに集中でき、ICMPモックの定義は独立したファイルで管理されるようになりました。これは、テストコードのモジュール化と関心事の分離という点で、非常に良いプラクティスです。

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

このコミットによるファイル変更は以下の2点です。

  1. src/pkg/net/ipraw_test.go:

    • icmpv4EchoRequest, icmpv4EchoReply, icmpv6EchoRequest, icmpv6EchoReply の定数定義が削除されました。
    • icmpMessage 構造体、icmpMessageBody インターフェース、icmpEcho 構造体の定義が削除されました。
    • icmpMessage.Marshal()parseICMPMessage()icmpEcho.Len()icmpEcho.Marshal()parseICMPEcho() の各メソッド/関数の実装が削除されました。
    • 合計で110行が削除されています。
  2. src/pkg/net/mockicmp_test.go:

    • 新しいファイルとして作成されました。
    • src/pkg/net/ipraw_test.goから削除された上記の定数、構造体、インターフェース、メソッド、関数の定義と実装が、そのままこのファイルにコピーされました。
    • 合計で116行が追加されています。

実質的には、ipraw_test.goからICMPモック関連のコードが切り出され、mockicmp_test.goに移動された形になります。コードの内容自体に変更はありません。

コアとなるコードの解説

移動されたコアとなるコードは、ICMPメッセージの構造をGoの型で表現し、それらをバイナリデータとの間で変換(マーシャリング/アンマーシャリング)するためのロジックです。

icmpMessageicmpMessageBody インターフェース

// icmpMessage represents an ICMP message.
type icmpMessage struct {
	Type     int             // type
	Code     int             // code
	Checksum int             // checksum
	Body     icmpMessageBody // body
}

// icmpMessageBody represents an ICMP message body.
type icmpMessageBody interface {
	Len() int
	Marshal() ([]byte, error)
}

icmpMessageはICMPヘッダの基本フィールド(タイプ、コード、チェックサム)と、メッセージのペイロード部分を表すicmpMessageBodyインターフェースを保持します。このインターフェースは、ボディの長さと、それをバイナリに変換するMarshalメソッドを定義しています。これにより、異なる種類のICMPメッセージボディ(例: エコー、タイムスタンプなど)を統一的に扱うことができます。

icmpMessage.Marshal()

// 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} // Type, Code, Checksum (initially 0)
	if m.Body != nil && m.Body.Len() != 0 {
		mb, err := m.Body.Marshal()
		if err != nil {
			return nil, err
		}
		b = append(b, mb...) // Append marshaled body
	}
	switch m.Type {
	case icmpv6EchoRequest, icmpv6EchoReply:
		return b, nil // IPv6 ICMP checksum is handled by IPv6 header
	}
	// Calculate checksum for IPv4 ICMP
	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]) // Sum 16-bit words
	}
	if csumcv&1 == 0 {
		s += uint32(b[csumcv]) // Add last byte if odd length
	}
	s = s>>16 + s&0xffff // Add carries
	s = s + s>>16        // Add remaining carries
	// Place checksum back in header; using ^= avoids the
	// assumption the checksum bytes are zero.
	b[2] ^= byte(^s)
	b[3] ^= byte(^s >> 8)
	return b, nil
}

このメソッドは、icmpMessage構造体の内容をICMPパケットのバイナリ形式に変換します。

  1. まず、タイプとコードのバイトを初期化し、チェックサムのプレースホルダー(0, 0)を置きます。
  2. Bodyが存在し、データがある場合は、Body.Marshal()を呼び出してそのバイナリ表現を取得し、メインのバイトスライスbに追加します。
  3. ICMPv6メッセージの場合、チェックサムの計算はIPv6ヘッダによって行われるため、ここでは計算をスキップします。
  4. ICMPv4メッセージの場合、標準的なICMPチェックサムの計算ロジックが適用されます。これは、メッセージ全体を16ビットワードの合計として扱い、オーバーフローを繰り越しに加算し、最終的に1の補数(ビット反転)を取るというものです。計算されたチェックサムは、バイトスライスbの適切な位置(インデックス2と3)に書き込まれます。

parseICMPMessage()

// 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")
	}
	// Extract Type, Code, Checksum from the first 4 bytes
	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:
			// If it's an echo message, parse the body as icmpEcho
			m.Body, err = parseICMPEcho(b[4:])
			if err != nil {
				return nil, err
			}
		}
	}
	return m, nil
}

この関数は、受信したICMPバイナリデータからicmpMessage構造体を再構築します。

  1. まず、メッセージの長さが最低限のヘッダ長(4バイト)を満たしているかを確認します。
  2. 最初の4バイトからタイプ、コード、チェックサムを抽出してicmpMessage構造体を初期化します。
  3. メッセージにボディ部分がある場合(長さが4バイトより大きい場合)、メッセージタイプに応じて適切なボディ解析関数(この場合はparseICMPEcho)を呼び出し、結果をm.Bodyに設定します。

icmpEcho とそのメソッド

// 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) // ID (2 bytes) + Seq (2 bytes) + Data length
}

// 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) // ID (high byte, low byte)
	b[2], b[3] = byte(p.Seq>>8), byte(p.Seq) // Seq (high byte, low byte)
	copy(b[4:], p.Data) // Copy data payload
	return b, nil
}

// parseICMPEcho parses b as an ICMP echo request or reply message
// body.
func parseICMPEcho(b []byte) (*icmpEcho, error) {
	bodylen := len(b)
	// Extract ID and Seq from the first 4 bytes
	p := &icmpEcho{ID: int(b[0])<<8 | int(b[1]), Seq: int(b[2])<<8 | int(b[3])}
	if bodylen > 4 {
		// If there's additional data, copy it
		p.Data = make([]byte, bodylen-4)
		copy(p.Data, b[4:])
	}
	return p, nil
}

icmpEcho構造体は、ICMPエコーメッセージのボディ部分(識別子、シーケンス番号、データ)を表現します。

  • Len()メソッドは、このボディのバイナリ表現の長さを返します。
  • Marshal()メソッドは、icmpEcho構造体をバイナリ形式に変換します。識別子とシーケンス番号はそれぞれ2バイトで表現され、その後にデータペイロードが続きます。
  • parseICMPEcho()関数は、バイナリデータからicmpEcho構造体を解析します。最初の4バイトから識別子とシーケンス番号を抽出し、残りのバイトをデータペイロードとして扱います。

これらのコードは、Goのnetパッケージのテストにおいて、実際のネットワークスタックを介さずにICMPメッセージの送受信をシミュレートするために不可欠なモック機能を提供します。これにより、テストの実行速度が向上し、外部ネットワークへの依存がなくなります。

関連リンク

  • Go CL (Code Review) リンク: https://golang.org/cl/13611043

参考にした情報源リンク

  • ICMP (Internet Control Message Protocol) の基本概念
  • Raw IP Sockets の概念
  • Go言語のテストに関する公式ドキュメントやベストプラクティス

(注: issue 6320に関する具体的なGoのイシュートラッカーの情報は見つかりませんでした。これは、イシュー番号が古い、または内部的な参照である可能性が考えられます。)