Help us understand the problem. What is going on with this article?

Goでパケットキャプチャを実践してみる

More than 3 years have passed since last update.

パケットキャプチャといえばpcapと言うくらいCで書かれた有名なライブラリがあるらしく、
そのpcapをGoでラップしたgopacket/pcapライブラリを使って試してみました。

参考ドキュメント

環境

CentOS 6.5 (vagrant)
Go 1.5.2

インストール

pcap

sudo yum -y install libpcap-devel
sudo yum -y install libnet

gopacket

# パケットに関してのライブラリ
go get github.com/google/gopacket

ソースコード

pcapを実行するときは基本的にsudoを付けて実行するので、$PATHを引き継ぐようにオプションをつけるか、goコマンドをフルパスで実行するかします。
以降の例ではフルパスで実行しています。

ネットワークデバイスを表示

find_devise.go
package main

import (
    "fmt"
    "log"
    "github.com/google/gopacket/pcap"
)

func main() {
    // Find all devices
    devices, err := pcap.FindAllDevs()
    if err != nil {
        log.Fatal(err)
    }

    // Print device information
    fmt.Println("Devices found:")
    for _, device := range devices {
        fmt.Println("\nName: ", device.Name)
        fmt.Println("Description: ", device.Description)
        fmt.Println("Devices addresses: ", device.Description)
        for _, address := range device.Addresses {
            fmt.Println("- IP address: ", address.IP)
            fmt.Println("- Subnet mask: ", address.Netmask)
        }
    }
}

実行

[vagrant@vagrant-centos65 ~]$ sudo  /usr/local/go/bin/go run find_devise.go
Devices found:

Name:  eth0
Description:
Devices addresses:
- IP address:  10.0.2.15
- Subnet mask:  ffffff00
- IP address:  fe80::a00:27ff:fe4f:b806
- Subnet mask:  ffffffffffffffff0000000000000000

Name:  nflog
Description:  Linux netfilter log (NFLOG) interface
Devices addresses:  Linux netfilter log (NFLOG) interface

Name:  nfqueue
Description:  Linux netfilter queue (NFQUEUE) interface
Devices addresses:  Linux netfilter queue (NFQUEUE) interface

Name:  eth1
Description:
Devices addresses:
- IP address:  192.168.33.28
- Subnet mask:  ffffff00
- IP address:  fe80::a00:27ff:fe2e:e4a5
- Subnet mask:  ffffffffffffffff0000000000000000

Name:  any
Description:  Pseudo-device that captures on all interfaces
Devices addresses:  Pseudo-device that captures on all interfaces

Name:  lo
Description:
Devices addresses:
- IP address:  127.0.0.1
- Subnet mask:  ff000000
- IP address:  ::1
- Subnet mask:  ffffffffffffffffffffffffffffffff

ネットワークデバイスeth1のパケットの中身を見る

devise_for_live_cap.go
package main

import (
    "fmt"
    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
    "log"
    "time"
)

var (
    device       string = "eth1"
    snapshot_len int32  = 1024
    promiscuous  bool   = false
    err          error
    timeout      time.Duration = 30 * time.Second
    handle       *pcap.Handle
)

func main() {
    // Open device
    handle, err = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)
    if err != nil {log.Fatal(err) }
    defer handle.Close()

    // Use the handle as a packet source to process all packets
    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    for packet := range packetSource.Packets() {
        // Process packet here
        fmt.Println(packet)
    }
}

実行

sudo  /usr/local/go/bin/go run devise_for_live_cap.go

何もパケットが通っていなければずっと表示されないままなので、pingをホストマシンからvagrantに対して送ってみます。

# vagrantのIP
ping 192.168.1.2

pingを送ったのでゲスト側ではこのようにパケットの内容が表示されます。

Layer 3 (08 bytes) = ICMPv4 となっているのがpingのパケット
pingを送るとなぜかARPのパケットまで表示されるが、なぜかわからない。。。
2つ出るのはarp要求と応答?
pingコマンドのオプションで -w 1000 などつけると パケットのlengthも変わる。

PACKET: 42 bytes, wire length 42 cap length 42 @ 2015-12-15 05:01:19.220701 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..28..] SrcMAC=08:00:27:2e:e4:a5 DstMAC=0a:00:27:00:00:00 EthernetType=ARP Length=0}
- Layer 2 (28 bytes) = ARP  {Contents=[..28..] Payload=[] AddrType=Ethernet Protocol=IPv4 HwAddressSize=6 ProtAddressSize=4 Operation=1 SourceHwAddress=[..6..] SourceProtAddress=[192, 168, 33, 28] DstHwAddress=[..6..] DstProtAddress=[192, 168, 33, 1]}


PACKET: 42 bytes, wire length 42 cap length 42 @ 2015-12-15 05:01:19.221124 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..28..] SrcMAC=0a:00:27:00:00:00 DstMAC=08:00:27:2e:e4:a5 EthernetType=ARP Length=0}
- Layer 2 (28 bytes) = ARP  {Contents=[..28..] Payload=[] AddrType=Ethernet Protocol=IPv4 HwAddressSize=6 ProtAddressSize=4 Operation=2 SourceHwAddress=[..6..] SourceProtAddress=[192, 168, 33, 1] DstHwAddress=[..6..] DstProtAddress=[192, 168, 33, 28]}


PACKET: 98 bytes, wire length 98 cap length 98 @ 2015-12-15 05:01:27.90845 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=0a:00:27:00:00:00 DstMAC=08:00:27:2e:e4:a5 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4 {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=19530 Flags= FragOffset=0 TTL=64 Protocol=ICMPv4 Checksum=27377 SrcIP=192.168.33.1 DstIP=192.168.33.28 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoRequest(0) Checksum=37266 Id=24886 Seq=0}
- Layer 4 (56 bytes) = Payload  56 byte(s)

キャプチャするポートやTCP/UDPを指定する

前項目では全てのパケットの内容を表示していたが、今回はフィルタリングを追加してみる。
代表的な80ポートのHTTPリクエストのパケットを見てみる。

settings_filter.go
package main

import (
    "fmt"
    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
    "log"
    "time"
)

var (
    device       string = "eth1"
    snapshot_len int32  = 1024
    promiscuous  bool   = false
    err          error
    timeout      time.Duration = 30 * time.Second
    handle       *pcap.Handle
)

func main() {
    // Open device
    handle, err = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)
    if err != nil {
        log.Fatal(err)
    }
    defer handle.Close()

    // Set filter
    var filter string = "tcp and port 80"
    err = handle.SetBPFFilter(filter)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Only capturing TCP port 80 packets.")

    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    for packet := range packetSource.Packets() {
        // Do something with a packet here.
        fmt.Println(packet)
    }
}

nginx

Webサーバにnginxを使用するので、まずはインストールしてみる。

# インストール
sudo yum -y install nginx
# 起動
sudo service nginx restart

実行

settings_filter.goを実行したらホスト側でゲスト側のIPを入力してみる
今回は先頭の2つのパケットのみ表示させているが、SrcIPとDstIPの値が1つ目と2つ目で逆になっている。
これはホスト側のリクエストとWebサーバからのレスポンスだってことが読み取れる。

settings_filter.go
sudo /usr/local/go/bin/go run settings_filter.go

PACKET: 54 bytes, wire length 54 cap length 54 @ 2015-12-15 05:30:39.730514 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..40..] SrcMAC=0a:00:27:00:00:00 DstMAC=08:00:27:2e:e4:a5 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4 {Contents=[..20..] Payload=[..20..] Version=4 IHL=5 TOS=0 Length=40 Id=13015 Flags= FragOffset=0 TTL=64 Protocol=TCP Checksum=33931 SrcIP=192.168.33.1 DstIP=192.168.33.28 Options=[] Padding=[]}
- Layer 3 (20 bytes) = TCP  {Contents=[..20..] Payload=[] SrcPort=61656 DstPort=80(http) Seq=2755802527 Ack=3043451509 DataOffset=5 FIN=false SYN=false RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=4096 Checksum=63870 Urgent=0 Options=[] Padding=[]}

PACKET: 66 bytes, wire length 66 cap length 66 @ 2015-12-15 05:30:39.730534 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..52..] SrcMAC=08:00:27:2e:e4:a5 DstMAC=0a:00:27:00:00:00 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4 {Contents=[..20..] Payload=[..32..] Version=4 IHL=5 TOS=0 Length=52 Id=39 Flags=DF FragOffset=0 TTL=64 Protocol=TCP Checksum=30511 SrcIP=192.168.33.28 DstIP=192.168.33.1 Options=[] Padding=[]}
- Layer 3 (32 bytes) = TCP  {Contents=[..32..] Payload=[] SrcPort=80(http) DstPort=61656 Seq=3043451509 Ack=2755802528 DataOffset=8 FIN=false SYN=false RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=361 Checksum=17752 Urgent=0 Options=[NOP, NOP, TSOPT:11667222/479225165] Padding=[]}




パケットを他レイヤーへキャストする

// キャスト出来るプロトコルは下記パッケージから参照できる
// https://github.com/google/gopacket/blob/master/layers/layertypes.go
packet.Layer(layers.プロトコル)
decoding_packet.go
package main

import (
    "fmt"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
    "log"
    "strings"
    "time"
)

var (
    device      string = "eth0"
    snapshotLen int32  = 1024
    promiscuous bool   = false
    err         error
    timeout     time.Duration = 30 * time.Second
    handle      *pcap.Handle
)

func main() {
    // Open device
    handle, err = pcap.OpenLive(device, snapshotLen, promiscuous, timeout)
    if err != nil {log.Fatal(err) }
    defer handle.Close()

    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    for packet := range packetSource.Packets() {
        printPacketInfo(packet)
    }
}

func printPacketInfo(packet gopacket.Packet) {
    // Ethernet Packetへキャスト
    // Let's see if the packet is an ethernet packet
    ethernetLayer := packet.Layer(layers.LayerTypeEthernet)
    if ethernetLayer != nil {
        fmt.Println("Ethernet layer detected.")
        ethernetPacket, _ := ethernetLayer.(*layers.Ethernet)
        fmt.Println("Source MAC: ", ethernetPacket.SrcMAC)
        fmt.Println("Destination MAC: ", ethernetPacket.DstMAC)
        // Ethernet type is typically IPv4 but could be ARP or other
        fmt.Println("Ethernet type: ", ethernetPacket.EthernetType)
        fmt.Println()
    }

    // IPパケットへキャスト
    // Let's see if the packet is IP (even though the ether type told us)
    ipLayer := packet.Layer(layers.LayerTypeIPv4)
    if ipLayer != nil {
        fmt.Println("IPv4 layer detected.")
        ip, _ := ipLayer.(*layers.IPv4)

        // IP layer variables:
        // Version (Either 4 or 6)
        // IHL (IP Header Length in 32-bit words)
        // TOS, Length, Id, Flags, FragOffset, TTL, Protocol (TCP?),
        // Checksum, SrcIP, DstIP
        fmt.Printf("From %s to %s\n", ip.SrcIP, ip.DstIP)
        fmt.Println("Protocol: ", ip.Protocol)
        fmt.Println()
    }

    // IPパケットへキャスト
    // Let's see if the packet is TCP
    tcpLayer := packet.Layer(layers.LayerTypeTCP)
    if tcpLayer != nil {
        fmt.Println("TCP layer detected.")
        tcp, _ := tcpLayer.(*layers.TCP)

        // TCP layer variables:
        // SrcPort, DstPort, Seq, Ack, DataOffset, Window, Checksum, Urgent
        // Bool flags: FIN, SYN, RST, PSH, ACK, URG, ECE, CWR, NS
        fmt.Printf("From port %d to %d\n", tcp.SrcPort, tcp.DstPort)
        fmt.Println("Sequence number: ", tcp.Seq)
        fmt.Println()
    }

    // All packet layers???
    // Iterate over all layers, printing out each layer type
        fmt.Println("All packet layers:")
    for _, layer := range packet.Layers() {
        fmt.Println("- ", layer.LayerType())
    }

    // アプリケーションレイヤパケットへキャスト
    // When iterating through packet.Layers() above,
    // if it lists Payload layer then that is the same as
    // this applicationLayer. applicationLayer contains the payload
    applicationLayer := packet.ApplicationLayer()
    if applicationLayer != nil {
        fmt.Println("Application layer/Payload found.")
        fmt.Printf("%s\n", applicationLayer.Payload())

        // Search for a string inside the payload
        if strings.Contains(string(applicationLayer.Payload()), "HTTP") {
            fmt.Println("HTTP found!")
        }
    }

    // Check for errors
    if err := packet.ErrorLayer(); err != nil {
        fmt.Println("Error decoding some part of the packet:", err)
    }
}

実行

アプリケーションレイヤだけ文字化けた...
1つのパケットをキャプチャするたびに下記のブロックが出力されるので、大量ログに注意

sudo /usr/local/go/bin/go run decoding_packet.go

############## Ethernet layers ############## 
Ethernet layer detected.
Source MAC:  08:00:27:4f:b8:06
Destination MAC:  52:54:00:12:35:02
Ethernet type:  IPv4

############## IP layers ############## 
IPv4 layer detected.
From 10.0.2.15 to 10.0.2.2
Protocol:  TCP

############## TCP layers ############## 
TCP layer detected.
From port 22 to 59180
Sequence number:  262547368

############## All packet layers ############## 
All packet layers:
-  Ethernet
-  IPv4
-  TCP
-  Payload

############## Application layers ############## 
Application layer/Payload found.
�!$�}�K~E2�(m����v޹�-
                      է��(�At��q��܀Nƛ�E�Nݚ�˹���x]�@)���n�L_9'��T<AUU]0qo�P@�?���t��8��b�W  �s�H�"�
                                                                                                       ;bs�XL����t��X���ļ*�"��3�V� �:�ᱩ�{$�i��s�Xˁ�u
p/���N�K��{����Nu�E�G2��i�����[
��F9��/����>x:@Lxw+�{��四�4���
�
 ޿�#UO���ӶjT�y��!~ �u��΂�Њ�X�Y��1�C.��aG<���A+���\���4��ʬ���uB�}�w
                                                                  ������wYP1�]�O>
�  л$$x�
.8�+��65�<RZ�y5�sε�M�8��,�Y���{_��׌#9�@�A?Sғ�*���#.�Cb���Z?AQ'l�88d��}䎪��V�G�&V��-����*0*B
                                                                                           �.Ơu�UrEJ/����;��6oS-����L����l���_�*{rL�5���
�D_t<��2(                                                                                                                                ]U!s�#�V���(_@(��,
         ��wqJ-uh���1�g`�ӫm\Y�����B��F"�~��px��8lD��Rݘ�TŻ�.�I
(E���G���#c������{��i��p� �_!C�o��Rx�]B��7�nW���>�:�
bO�Kd�Bon��9p��b��s�@_j�S�,(3���K��s��j�+Џ����`(��#�ݟ���
                                                       ���4���HlF�wa�#�XH�ݥ�9O�:MhM]*��f&!rg՗q�
���)^�A�\I9����_1dv

パケットを送信する

handle.WritePacketDataでパケットを送信する。
送信するIP、MACアドレス、ポートはgopacket.SerializeLayersにて指定する。

create_and_send.go
package main

import (
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
    "log"
    "net"
    "time"
)

var (
    device       string = "eth1"
    snapshot_len int32  = 1024
    promiscuous  bool   = false
    err          error
    timeout      time.Duration = 30 * time.Second
    handle       *pcap.Handle
    buffer       gopacket.SerializeBuffer
    options      gopacket.SerializeOptions
)

func main() {
    // Open device
    handle, err = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)
    if err != nil {log.Fatal(err) }
    defer handle.Close()

    // Send raw bytes over wire
    rawBytes := []byte{10, 20, 30}
    err = handle.WritePacketData(rawBytes)
    if err != nil {
        log.Fatal(err)
    }

    // Create a properly formed packet, just with
    // empty details. Should fill out MAC addresses,
    // IP addresses, etc.
    buffer = gopacket.NewSerializeBuffer()
    gopacket.SerializeLayers(buffer, options,
        &layers.Ethernet{},
        &layers.IPv4{},
        &layers.TCP{},
        gopacket.Payload(rawBytes),
    )
    outgoingPacket := buffer.Bytes()
    // Send our packet
    err = handle.WritePacketData(outgoingPacket)
    if err != nil {
        log.Fatal(err)
    }

    // This time lets fill out some information
    ipLayer := &layers.IPv4{
        SrcIP: net.IP{127, 0, 0, 1},
        DstIP: net.IP{8, 8, 8, 8},
    }
    ethernetLayer := &layers.Ethernet{
        SrcMAC: net.HardwareAddr{0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA, 0xFA, 0xAA},
        DstMAC: net.HardwareAddr{0xBD, 0xBD, 0xBD, 0xBD, 0xBD, 0xBD, 0xBD, 0xBD},
    }
    tcpLayer := &layers.TCP{
        SrcPort: layers.TCPPort(4321),
        DstPort: layers.TCPPort(80),
    }
    // And create the packet with the layers
    buffer = gopacket.NewSerializeBuffer()
    gopacket.SerializeLayers(buffer, options,
        ethernetLayer,
        ipLayer,
        tcpLayer,
        gopacket.Payload(rawBytes),
    )
    outgoingPacket = buffer.Bytes()
}

実行

# パケットキャプチャ用に作ったgoスクリプトを実行しておく
sudo /usr/local/go/bin/go run devise_for_live_cap.go
# パケットを送信するgoスクリプトを実行
sudo  /usr/local/go/bin/go run create_and_send.go

PACKET: 3 bytes, wire length 3 cap length 3 @ 2015-12-15 07:25:51.101255 +0000 UTC
- Layer 1 (03 bytes) = DecodeFailure    Packet decoding error: Ethernet packet too small

PACKET: 43 bytes, wire length 43 cap length 43 @ 2015-12-15 07:25:51.101924 +0000 UTC
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[] SrcMAC=00:00:00:00:00:00 DstMAC=00:00:00:00:00:00 EthernetType=LLC Length=0}

ルーター自作でわかるパケットの流れにpcapを実装するコードが乗っていたのでpcapコマンドを実装しましたが、比較するまでも無くGoの方が楽ですね。
ただライブラリが色々やってくれちゃうと車輪の再発明する意味自体がそれほど無くなってしまうので、勉強するならCで苦しみながらやるのが良いかなーと思いました。

kkyouhei
アドネットワークとかアプリとかEC
basicinc
マーケティングとテクノロジーで社会のあらゆる問題を解決する集団
https://tech.basicinc.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away