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


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

## 参考ドキュメント
- http://www.devdungeon.com/content/packet-capture-injection-and-analysis-gopacket
色々な方法が解説されていてほぼこれを参考通りにやった。

- https://github.com/google/gopacket
こまったらソースを見る


## 環境
CentOS 6.5 (vagrant)
Go 1.5.2


## インストール

### pcap

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

### gopacket

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


## ソースコード

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

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

```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)
        }
    }
}
```

#### 実行

```bash
[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のパケットの中身を見る

```go: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)
    }
}
```

#### 実行

```bash
sudo  /usr/local/go/bin/go run devise_for_live_cap.go
```
何もパケットが通っていなければずっと表示されないままなので、pingをホストマシンからvagrantに対して送ってみます。

```host
# vagrantのIP
ping 192.168.1.2
```

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

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

```guest
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リクエストのパケットを見てみる。

```go: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を使用するので、まずはインストールしてみる。

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

#### 実行

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

```go: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=[]}

・
・
・
```

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

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

```go: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つのパケットをキャプチャするたびに下記のブロックが出力されるので、大量ログに注意

```bash
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にて指定する。

```go: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}
```

[ルーター自作でわかるパケットの流れ]("ルーター自作でわかるパケットの流れ" http://www.amazon.co.jp/%E3%83%AB%E3%83%BC%E3%82%BF%E3%83%BC%E8%87%AA%E4%BD%9C%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B%E3%83%91%E3%82%B1%E3%83%83%E3%83%88%E3%81%AE%E6%B5%81%E3%82%8C-%E5%B0%8F%E4%BF%A3-%E5%85%89%E4%B9%8B/dp/4774147451)にpcapを実装するコードが乗っていたのでpcapコマンドを実装しましたが、比較するまでも無くGoの方が楽ですね。
ただライブラリが色々やってくれちゃうと車輪の再発明する意味自体がそれほど無くなってしまうので、勉強するならCで苦しみながらやるのが良いかなーと思いました。