APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

GoPacketでネットワークと直接お喋りしよう

はじめに

先進サービス開発事業部の山岡です。

Go言語は標準でかなり強力なネットワーク用のライブラリーがありますが、GoPacket *1 を使うと更に細かいところをイジれるようになります。

仕事でパケットをごにょごにょしたところ結構面白かったのでここで紹介したいと思います。

コード

早速ですがコードを貼り付けます。ネットワークの素養がある方ならかなり直感的に見ることができるかと思います。

なお環境は前回の記事を前提としてserver1からserver2へICMP Echo Requestの送信までをやってみます。

main.go

package main

import (
    "net"
    "time"

    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
)

func main() {
    srcMAC, _ := net.ParseMAC("ca:50:4c:06:7d:5f")
    dstMAC, _ := net.ParseMAC("fe:8e:41:70:79:ec")

    ethernetLayer := &layers.Ethernet{
        SrcMAC:       srcMAC,
        DstMAC:       dstMAC,
        EthernetType: layers.EthernetTypeIPv4,
    }

    ipLayer := &layers.IPv4{
        Version:  uint8(4),
        Flags:    layers.IPv4DontFragment,
        SrcIP:    net.ParseIP("10.0.0.1"),
        DstIP:    net.ParseIP("10.0.1.1"),
        Protocol: layers.IPProtocolICMPv4,
        TTL:      uint8(64),
        Id:       uint16(1),
    }

    icmp := &layers.ICMPv4{
        TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, uint8(0)),
        Id:       uint16(1),
        Seq:      uint16(1),
    }

    buf := gopacket.NewSerializeBuffer()
    gopacket.SerializeLayers(
        buf,
        gopacket.SerializeOptions{
            ComputeChecksums: true,
            FixLengths:       true,
        },
        ethernetLayer,
        ipLayer,
        icmp,
    )

    h, _ := pcap.OpenLive("server1-veth1", int32(1500), false, 3*time.Second)
    defer h.Close()
    h.WritePacketData(buf.Bytes())
}

使い方

基本的な考え方はOSI参照モデル *2 のレイヤーで捉えます。

「ネットワークと直接お喋り」とは銘打ちましたが、流石にビット演算して云々というわけではありません。GoPacketのサブパッケージである layers 内にはレイヤー毎に構造体が定義されており、ここにヘッダー情報を埋め込んでいきます。設定内容についても基本的には定数が提供されているので「ICMP Echo RequestはType8」等のように丸暗記しなくても大体なんとかなるでしょう(まぁ実際に使っているとすぐ覚えてしまいますが)。

チェックサムやデータ長を書くためのフィールドは自動計算することができる(後述)ので通常手入力する必要はありません。

L2 (Ethernet)

ここで設定すべき内容は「送信元MAC」「宛先MAC」「イーサネットタイプ」の3つです。

本来であれば宛先MACはARPを使って求めるところですが、今回は単純化のため既にARP解決済みという想定で手入力します。実際の値は sudo ip netns exec server1 ip addrsudo ip netns exec router ip addr で求めて下さい。

L3 (IP)

IPヘッダーを入力しますが送信元IPアドレス、宛先IPアドレス以外はほぼ決め打ちです。よりリアルにやるのであればIDに乱数を入れたりしてもいいでしょう。

L3 (ICMP)

Type/Codeを入力します。今回はEcho RequestなのでType8 Code0をセットします。IDに乱数、シーケンス番号に通し番を振るようなコードにするとよりPingっぽくなります。

シリアライズ

gopacket.SerializeLayers() にシリアライズ用のバッファ、シリアライズオプション、各レイヤー構造体を放り込みます。 ComputeChecksumsFixLengths をtrueにしておくと各所のヘッダー長等を勝手に入れてくれるので便利です。

ビルドと動作確認

ビルドは通常通り go build して下さい。なおGoPacketのビルドには libpcap-dev が、実行には libpcap が必要になります。

Echo Request -> Echo Replyのやり取りを見るため中間のルーターでパケットキャプチャーしてみましょう。

$ go build main.go
$ sudo ip netns exec server1 ./main
$ sudo ip netns exec router tcpdump -nn -l
12:57:56.950376 IP 10.0.0.1 > 10.0.1.1: ICMP echo request, id 1, seq 1, length 8
12:57:56.950410 IP 10.0.1.1 > 10.0.0.1: ICMP echo reply, id 1, seq 1, length 8

今回書いたコードはあくまでEcho Requestを送るだけですが、通信相手である server2 は有効なICMPパケットとみなしてEcho Replyが返されていることが確認できました。

諸注意

上記の通りかなり自由度が高い通信を行えるため、コードによっては予期しない事象を引き起こす可能性があります。例えばヘッダーにデタラメな値を入れてIPSから怒られが発生したりするかもしれません。自分以外のARP RequestにもARP Replyを送るようなロジックを書くと結果的にネットワークが落ちたりするかもしれません。

くれぐれも他人の通信にイタズラしたりネットワーク管理者の手を煩わせたりすることのないようご注意下さい。