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

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

このコミットは、Go言語の標準ライブラリ net パッケージにおける、Raw IPソケットの取り扱いを改善するものです。具体的には、Dial および ListenPacket 関数が、"ip:protocol" という形式のネットワーク指定をサポートするよう拡張されました。これにより、ユーザーは特定のIPプロトコル(例: ICMP, OSPFなど)を指定してRaw IPソケットを確立できるようになります。

変更された主なファイルは以下の通りです。

  • src/pkg/net/dial.go: Dial, Listen, ListenPacket 関数におけるネットワークアドレス解決ロジックが変更され、新しい "ip:protocol" 形式のパースが導入されました。
  • src/pkg/net/ipraw_test.go: Raw IPソケットの新しい機能、特にICMPプロトコルを使用したテストが追加・修正されました。
  • src/pkg/net/iprawsock_plan9.go: Plan 9環境におけるRaw IPソケットの実装ファイルで、メソッドシグネチャの調整などが行われました。
  • src/pkg/net/iprawsock_posix.go: POSIX互換システム(Linux, macOSなど)におけるRaw IPソケットの実装ファイルで、同様にメソッドシグネチャの調整やエラーハンドリングの改善が行われました。
  • src/pkg/net/lookup_plan9.go: Plan 9環境におけるネットワークルックアップ関連ファイルで、lookupProtocol のスタブが追加されました。

コミット

このコミットは、Go言語の net パッケージにおいて、Dial および ListenPacket 関数がRaw IPソケットのために "ip:protocol" 形式のネットワーク指定をサポートするように拡張しました。これにより、特定のIPプロトコル(例: ICMP)を直接指定してソケットを作成できるようになり、より低レベルなネットワーク通信の制御が可能になります。この変更は、Issue #2654 を解決するために行われました。

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

https://github.com/golang/go/commit/68daa41d1bd3dc133828ddbb8a29cc64cc8802b1

元コミット内容

commit 68daa41d1bd3dc133828ddbb8a29cc64cc8802b1
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Sat Jan 21 21:51:53 2012 +0900

    net: Dial, ListenPacket with "ip:protocol" network for raw IP sockets
    
    Fixes #2654.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5545058

変更の背景

この変更の背景には、Go言語の net パッケージが提供するネットワーク通信機能の柔軟性を向上させるという目的があります。従来の net パッケージでは、TCPやUDPといったトランスポート層のプロトコルを扱うための抽象化が主に行われていました。しかし、ネットワークプログラミングにおいては、ICMP(Internet Control Message Protocol)やOSPF(Open Shortest Path First)のような、IP層で直接動作するプロトコルを扱う「Raw IPソケット」が必要となる場合があります。

以前のGoの net パッケージでは、Raw IPソケットを扱う際に、どのIPプロトコルを対象とするかを指定する方法が限定的でした。例えば、"ip4""ip6" といったネットワークタイプは存在しましたが、これだけでは特定のプロトコル番号(例: ICMPの1、TCPの6、UDPの17など)やプロトコル名(例: "icmp", "tcp", "udp")を指定してフィルタリングすることが困難でした。

Issue #2654 は、このRaw IPソケットの利用における不便さを指摘しており、特定のIPプロトコルを指定して DialListenPacket を行えるようにする要望が挙げられていました。このコミットは、その要望に応える形で、"ip:protocol" という新しいネットワーク指定形式を導入し、Raw IPソケットの利用をより直感的かつ強力にすることを目指しています。これにより、ユーザーはより低レベルなネットワークパケットの送受信を、Goの標準ライブラリを通じて容易に行えるようになります。

前提知識の解説

このコミットの理解を深めるために、以下の前提知識を解説します。

1. Raw IPソケット

通常のTCPやUDPソケットは、アプリケーション層が直接データを送受信できるように、トランスポート層(TCP/UDP)のヘッダ処理やポート番号による多重化をOSが自動的に行います。しかし、Raw IPソケットは、これらのトランスポート層の処理をスキップし、IP層のパケットを直接送受信するためのソケットです。

  • 用途: ICMP(pingやtraceroute)、OSPF、IGMPなどのIP層プロトコルを実装する場合や、カスタムのトランスポート層プロトコルを開発する場合に利用されます。
  • 特徴:
    • IPヘッダの一部(または全体)をアプリケーションが構築・解析できる。
    • 特定のIPプロトコル番号を指定して、そのプロトコルに属するパケットのみを受信できる。
    • 通常、特権ユーザー(rootなど)で実行する必要がある。

2. IPプロトコル番号とプロトコル名

IPパケットのヘッダには、「プロトコル番号」というフィールドがあり、IPペイロードがどのトランスポート層プロトコル(またはそれ以外のIP層プロトコル)であるかを示します。

  • 一般的なプロトコル番号:
    • 1: ICMP (Internet Control Message Protocol)
    • 6: TCP (Transmission Control Protocol)
    • 17: UDP (User Datagram Protocol)
    • 89: OSPF (Open Shortest Path First)
  • プロトコル名: これらのプロトコル番号には、対応するプロトコル名が存在します。例えば、1は"icmp"、6は"tcp"です。OSは通常、/etc/protocols のようなファイルでこれらのマッピングを管理しています。

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

Go言語の net パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/UDPクライアント・サーバーの構築、DNSルックアップ、IPアドレスの操作など、幅広いネットワーク機能が含まれています。

  • Dial 関数: 指定されたネットワークアドレスに接続し、Conn インターフェースを実装するオブジェクトを返します。クライアント側で接続を確立する際に使用されます。
    • 例: net.Dial("tcp", "google.com:80")
  • ListenPacket 関数: 指定されたローカルネットワークアドレスでパケットを受信するための PacketConn インターフェースを実装するオブジェクトを返します。UDPやRaw IPソケットのようなパケット指向の通信で利用されます。
    • 例: net.ListenPacket("udp", ":12345")
  • ネットワーク文字列: DialListenPacket の第一引数 net は、使用するネットワークプロトコルを指定する文字列です。これまでは "tcp", "udp", "ip4", "ip6" などが一般的でした。

4. resolveNetAddr 関数

net パッケージ内部で、与えられたネットワークタイプとアドレス文字列を解析し、適切な Addr インターフェース(TCPAddr, UDPAddr, IPAddr など)に解決する役割を持つ関数です。このコミットでは、この関数のシグネチャと内部ロジックが変更され、新しい "ip:protocol" 形式に対応するようになりました。

5. OpError

Goの net パッケージで発生するネットワーク操作のエラーを表す構造体です。操作の種類(Op)、ネットワークタイプ(Net)、アドレス(Addr)、そして元となるエラー(Err)を含みます。

これらの知識を前提として、コミットの変更内容を読み解いていきます。

技術的詳細

このコミットの技術的な核心は、net パッケージがRaw IPソケットを扱う際のネットワーク指定の柔軟性を高めるために、新しいパースロジックとそれに対応するAPIの変更を導入した点にあります。

1. parseDialNetwork 関数の導入

最も重要な変更は、src/pkg/net/dial.goparseDialNetwork という新しい内部関数が導入されたことです。この関数は、DialListenPacket に渡されるネットワーク文字列(例: "ip4:icmp", "tcp", "udp")を解析し、以下の情報を返します。

  • afnet (address family network): アドレスファミリーを示すネットワークタイプ(例: "ip", "ip4", "ip6", "tcp", "udp", "unix")。
  • proto (protocol number): IPプロトコル番号。"ip:protocol" 形式の場合に解析されます。それ以外の場合は0。
  • err: エラー情報。

この関数は、ネットワーク文字列にコロン : が含まれるかどうかで処理を分岐します。

  • コロンがない場合: 従来のネットワークタイプ("tcp", "udp", "unix" など)として扱われます。proto は0になります。
  • コロンがある場合: net[:i] の部分がアドレスファミリー("ip", "ip4", "ip6")であるかをチェックします。もしそうであれば、net[i+1:] の部分をプロトコル文字列として解析します。
    • プロトコル文字列が数値であれば、それを直接プロトコル番号として使用します。
    • プロトコル文字列が数値でなければ、lookupProtocol 関数(OSのプロトコル名解決機能を利用)を使ってプロトコル名から番号を解決します。

この parseDialNetwork 関数が導入されたことで、DialListenPacket の呼び出し元は、"ip4:1"(IPv4 ICMP)や "ip6:ospf"(IPv6 OSPF)のように、IPバージョンと特定のプロトコルを組み合わせて指定できるようになりました。

2. resolveNetAddr 関数の変更

resolveNetAddr 関数は、ネットワークタイプとアドレス文字列から具体的なアドレス構造体(*TCPAddr, *UDPAddr, *IPAddr など)を解決する役割を担っています。このコミットでは、resolveNetAddr のシグネチャが変更され、parseDialNetwork の結果である afnet を返すようになりました。

// 変更前
func resolveNetAddr(op, net, addr string) (a Addr, err error)

// 変更後
func resolveNetAddr(op, net, addr string) (afnet string, a Addr, err error)

これにより、DialListenPacket の内部で、解決されたアドレスファミリーに基づいて適切な処理を行うことが可能になります。

3. ListenPacket 関数の拡張

ListenPacket 関数は、パケット指向のネットワーク接続を確立するために使用されます。このコミットにより、ListenPacket は従来の "udp", "unixgram" に加えて、"ip", "ip4", "ip6" ネットワークタイプもサポートするようになりました。

変更前は、ListenPacket の内部でRaw IPソケットを扱うための特別なロジックが splitNetProto を使って存在していましたが、このコミットで parseDialNetwork を利用する統一的なアプローチに置き換えられました。これにより、ListenPacket("ip4:icmp", "0.0.0.0") のように、特定のIPプロトコルでパケットを受信するリスナーを簡単に作成できるようになりました。

4. iprawsock_posix.go および iprawsock_plan9.go の変更

これらのファイルは、それぞれPOSIX互換システムとPlan 9システムにおけるRaw IPソケットの低レベルな実装を含んでいます。

  • メソッドシグネチャの統一: Read, Write, ReadFromIP, WriteToIP, DialIP, ListenIP などのメソッドの戻り値の型が、エラーを返す際に (int, error)(*IPConn, error) のように統一されました。これは、Goのエラーハンドリングの慣習に沿った変更です。
  • splitNetProto の削除と parseDialNetwork の利用: 以前はこれらのファイル内に splitNetProto という関数がありましたが、これは dial.go に移動され、parseDialNetwork に統合されました。これにより、ネットワーク文字列のパースロジックが一元化され、コードの重複が排除されました。
  • エラーメッセージの改善: OpError の生成時に、より具体的な操作名(例: "writetoip")やネットワークタイプが渡されるようになり、エラーメッセージがより分かりやすくなりました。

5. ipraw_test.go のテスト強化

新しい機能が正しく動作することを確認するため、ipraw_test.go にはICMPプロトコルを使用したRaw IPソケットのテストが大幅に強化されました。

  • IPv4/IPv6対応: icmpTests という構造体が導入され、IPv4とIPv6の両方でICMPエコーリクエスト/リプライの送受信をテストできるようになりました。
  • newICMPEchoRequest などのヘルパー関数: ICMPエコーリクエストパケットを生成するためのヘルパー関数が追加され、テストコードの可読性と再利用性が向上しました。
  • exchangeICMPEchoicmpEchoTransponder: クライアントとサーバーの両方の役割をシミュレートし、ICMPエコーパケットの送受信と検証を行うテストロジックが実装されました。これにより、DialListenPacket を使ったRaw IPソケット通信のEnd-to-Endテストが可能になりました。

これらの技術的変更により、Goの net パッケージはRaw IPソケットをより柔軟かつ強力に扱えるようになり、ネットワークプログラミングの幅広いニーズに対応できるようになりました。

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

このコミットのコアとなるコードの変更は、主に src/pkg/net/dial.gosrc/pkg/net/ipraw_test.go に集中しています。

src/pkg/net/dial.go

  1. parseDialNetwork 関数の新規追加: この関数は、ネットワーク文字列(例: "ip4:icmp")を解析し、アドレスファミリーとプロトコル番号を抽出します。

    func parseDialNetwork(net string) (afnet string, proto int, err error) {
        i := last(net, ':')
        if i < 0 { // no colon
            // ... 既存のネットワークタイプ (tcp, udp, unixなど) の処理 ...
            return net, 0, nil
        }
        afnet = net[:i]
        switch afnet {
        case "ip", "ip4", "ip6":
            protostr := net[i+1:]
            proto, i, ok := dtoi(protostr, 0)
            if !ok || i != len(protostr) {
                proto, err = lookupProtocol(protostr) // プロトコル名から番号を解決
                if err != nil {
                    return "", 0, err
                }
            }
            return afnet, proto, nil
        }
        return "", 0, UnknownNetworkError(net)
    }
    
  2. resolveNetAddr 関数のシグネチャ変更と利用: parseDialNetwork の結果を受け取るようにシグネチャが変更され、内部で parseDialNetwork を呼び出すようになりました。

    // 変更前: func resolveNetAddr(op, net, addr string) (a Addr, err error)
    // 変更後: func resolveNetAddr(op, net, addr string) (afnet string, a Addr, err error) {
    //             afnet, _, err = parseDialNetwork(net)
    //             // ... 既存のロジック ...
    //         }
    
  3. Dial 関数の変更: resolveNetAddr の戻り値の変更に合わせて、呼び出し箇所が修正されました。

    // 変更前: addri, err := resolveNetAddr("dial", net, addr)
    // 変更後: _, addri, err := resolveNetAddr("dial", net, addr)
    
  4. ListenPacket 関数の変更: Raw IPソケットのサポートが明示的に追加され、parseDialNetwork の結果に基づいて ListenIP が呼び出されるようになりました。

    func ListenPacket(net, addr string) (PacketConn, error) {
        afnet, a, err := resolveNetAddr("listen", net, addr)
        if err != nil {
            return nil, err
        }
        switch afnet {
        case "udp", "udp4", "udp6":
            // ... UDPの処理 ...
        case "ip", "ip4", "ip6": // 新しく追加されたRaw IPソケットの処理
            var la *IPAddr
            if a != nil {
                la = a.(*IPAddr)
            }
            return ListenIP(net, la) // ListenIPを呼び出す
        case "unixgram":
            // ... Unixgramの処理 ...
        }
        return nil, UnknownNetworkError(net)
    }
    

src/pkg/net/ipraw_test.go

  1. icmpTests 構造体の追加: IPv4とIPv6のICMPテストケースを定義します。

    var icmpTests = []struct {
        net   string
        laddr string
        raddr string
        ipv6  bool
    }{
        {"ip4:icmp", "", "127.0.0.1", false},
        {"ip6:icmp", "", "::1", true},
    }
    
  2. TestICMP 関数の修正: icmpTests をループして、各テストケースを実行するように変更されました。

    func TestICMP(t *testing.T) {
        // ... root権限チェック ...
        seqnum := 61455
        for _, tt := range icmpTests {
            // ... IPv6サポートチェック ...
            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) // 新しいヘルパー関数を呼び出し
        }
    }
    
  3. exchangeICMPEcho 関数の新規追加: ICMPエコーリクエストを送信し、リプライを受信するテストロジックをカプセル化します。

    func exchangeICMPEcho(t *testing.T, net, laddr, raddr string, ipv6 bool, echo []byte) {
        c, err := ListenPacket(net, laddr) // ListenPacketでRaw IPソケットをオープン
        // ... エラーハンドリング、タイムアウト設定 ...
        _, err = c.WriteTo(echo, ra) // エコーリクエストを送信
        // ... リプライの受信と検証 ...
    }
    
  4. icmpEchoTransponder 関数の新規追加: ICMPエコーリクエストを受信し、エコーリプライを返すサーバー側のテストロジックをカプセル化します。

    func icmpEchoTransponder(t *testing.T, net, raddr string, ipv6 bool, waitForReady chan bool) {
        c, err := Dial(net, raddr) // DialでRaw IPソケットをオープン
        // ... エラーハンドリング、タイムアウト設定 ...
        // ... エコーリクエストの受信 ...
        // ... エコーリプライの送信 ...
    }
    

これらの変更により、net パッケージはRaw IPソケットをより柔軟に扱えるようになり、その機能がテストによって検証されるようになりました。

コアとなるコードの解説

このコミットのコアとなる変更は、Goの net パッケージがRaw IPソケットを扱う方法を根本的に改善し、より直感的で強力なAPIを提供することにあります。その中心となるのが、parseDialNetwork 関数の導入と、それを利用した Dial および ListenPacket 関数の拡張です。

parseDialNetwork の役割

以前の net パッケージでは、Raw IPソケットを扱う際に、ネットワークタイプとして "ip", "ip4", "ip6" を指定できました。しかし、これだけではどのIPプロトコル(例: ICMP, OSPF)を対象とするかを直接指定する方法がありませんでした。このため、ユーザーはRaw IPソケットを開いた後、受信したパケットのIPヘッダを自分で解析してプロトコルを識別する必要がありました。

parseDialNetwork 関数は、この問題を解決するために導入されました。この関数は、"ip:protocol" という新しいネットワーク文字列の形式を解析します。

  • ip:1: これは、IPv4またはIPv6のRaw IPソケットで、プロトコル番号が1(ICMP)のパケットを対象とすることを示します。
  • ip4:icmp: これは、IPv4のRaw IPソケットで、プロトコル名が"icmp"(プロトコル番号1)のパケットを対象とすることを示します。
  • ip6:ospf: これは、IPv6のRaw IPソケットで、プロトコル名が"ospf"(プロトコル番号89)のパケットを対象とすることを示します。

parseDialNetwork は、この新しい形式を解析し、IPバージョン(afnet)と対応するプロトコル番号(proto)を返します。プロトコル名が指定された場合は、内部で lookupProtocol 関数を呼び出して、OSのプロトコルデータベース(例: /etc/protocols)から対応するプロトコル番号を解決します。これにより、ユーザーはプロトコル番号を直接知らなくても、分かりやすいプロトコル名でRaw IPソケットを指定できるようになりました。

Dial および ListenPacket の拡張

parseDialNetwork が導入されたことで、Dial および ListenPacket 関数は、この新しいネットワーク文字列形式を理解し、Raw IPソケットを適切に初期化できるようになりました。

  • Dial: クライアント側で特定のIPプロトコルを持つRaw IPソケットを確立する際に使用されます。例えば、net.Dial("ip4:icmp", "127.0.0.1") とすることで、ローカルホストへのICMPエコーリクエストを送信するためのソケットを作成できます。
  • ListenPacket: サーバー側で特定のIPプロトコルを持つRaw IPソケットでパケットを受信する際に使用されます。例えば、net.ListenPacket("ip4:icmp", "0.0.0.0") とすることで、すべてのインターフェースで受信されるIPv4 ICMPパケットをリッスンするソケットを作成できます。

ListenPacket の内部では、parseDialNetwork が返した afnet"ip", "ip4", "ip6" のいずれかである場合、ListenIP 関数が呼び出されます。ListenIP は、指定されたIPプロトコル番号でRaw IPソケットを作成し、パケットの送受信を可能にします。

テストコードの重要性

src/pkg/net/ipraw_test.go に追加されたテストコードは、この新しい機能が正しく動作することを検証する上で非常に重要です。特に、exchangeICMPEchoicmpEchoTransponder は、クライアントとサーバーの両方の役割をシミュレートし、ICMPエコーリクエストとリプライの送受信をEnd-to-Endでテストします。これにより、DialListenPacket を使ったRaw IPソケット通信の信頼性が保証されます。

これらの変更により、Goの net パッケージは、より低レベルなネットワークプログラミングのニーズに対応できるようになり、ICMPベースのツール(pingなど)やカスタムIPプロトコルをGoで実装する際の利便性が大幅に向上しました。

関連リンク

  • Go Issue 2654: net: Dial, ListenPacket with "ip:protocol" network for raw IP sockets
  • Gerrit Change-Id: I2222222222222222222222222222222222222222 (コミットメッセージに記載されている https://golang.org/cl/5545058 は、Gerritの変更リストへのリンクです。これはGoプロジェクトの開発プロセスで使われるコードレビューシステムです。)

参考にした情報源リンク