Gopacketでパケットをキャプチャしたり生成したりしてみる

この記事は[富士通クラウドテクノロジーズ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fjct) 22日目の投稿です。

昨日は@kodakkuさんの「**[教師なし学習で周期表(金属)を分析してみる【主成分分析・R・Python】](https://qiita.com/kodakku/items/e082965d2792bc51e2f1)**」でした。
主成分分析することで、同族の元素が似たような性質をもっているということが可視化されてて面白かったです。
私はデータ分析については素人なのですが、主成分分析をするときにはこの記事を参考にしたいと思います！

## はじめに

こんにちは。FJCT21卒新入社員の@kswです。
10月に仮想ネットワークの運用・開発を行うチームに配属され、一人前のインフラエンジニアになるために日々奮闘しています。

今回は、一人前のインフラエンジニアの必須スキルの一つである（と私が勝手に思っている）「パケットの気持ちがわかる」を身に付けるため、Gopacketを使って実際にパケットを扱ってみたいと思います。

この記事では、Gopacketを実際に触りながら何ができるのか簡単に紹介します。

## Gopacketとは

<https://github.com/google/gopacket>

Gopacketは、パケットを扱うことができるGoパッケージです。
Gopacketには以下のサブパッケージも含まれています。

- layers
- pcap
- pfring
- afpacket
- tcpassembly

## 早速使ってみる

では、実際に簡単なコードを書いてパケットを読み込んだりパケットを生成してみたいと思います。

### 環境
- クライアント：Ubuntu 20.04（WSL）
    - こちらでGopacketを用いたプログラムを実行
    - Goのバージョンは1.16
- サーバー：Ubuntu20.04（RaspberryPi 4）

### パケットを読み込んでみる
パケット（pcapファイル）の読み込みには[OpenOffline](https://pkg.go.dev/github.com/google/gopacket/pcap#OpenOfflineFile)関数を使い、パケットのデコードには[NewPacketSource](https://pkg.go.dev/github.com/google/gopacket#NewPacketSource)を使います。
これらを使ってpcapファイルを読み込んでprintしてみます。

<details>
<summary>read_pcap.go</summary><div>

```go
package main

import (
	"fmt"
	"log"

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

func main() {
	handle, err := pcap.OpenOffline("icmp.pcap")
	if err != nil {
		log.Fatal(err)
	}
	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
	for packet := range packetSource.Packets() {
		fmt.Print(packet)
	}
}

```

</div></details>

**実行結果**

```
$ go run read_pcap.go 
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:57.440015 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:71:96:76:62:f4 DstMAC=dc:a6:32:bb:8e:a2 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=35386 Flags=DF FragOffset=0 TTL=63 Protocol=ICMPv4 Checksum=6978 SrcIP=192.168.10.108 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoRequest Checksum=11109 Id=1000 Seq=1}
- Layer 4 (56 bytes) = Payload  56 byte(s)
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:57.440169 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:a6:32:bb:8e:a2 DstMAC=dc:71:96:76:62:f4 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=38138 Flags= FragOffset=0 TTL=64 Protocol=ICMPv4 Checksum=20354 SrcIP=192.168.10.112 DstIP=192.168.10.108 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoReply Checksum=13157 Id=1000 Seq=1}
- Layer 4 (56 bytes) = Payload  56 byte(s)
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:58.441814 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:71:96:76:62:f4 DstMAC=dc:a6:32:bb:8e:a2 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=35407 Flags=DF FragOffset=0 TTL=63 Protocol=ICMPv4 Checksum=6957 SrcIP=192.168.10.108 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoRequest Checksum=59228 Id=1000 Seq=2}
- Layer 4 (56 bytes) = Payload  56 byte(s)
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:58.441877 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:a6:32:bb:8e:a2 DstMAC=dc:71:96:76:62:f4 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=38312 Flags= FragOffset=0 TTL=64 Protocol=ICMPv4 Checksum=20180 SrcIP=192.168.10.112 DstIP=192.168.10.108 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoReply Checksum=61276 Id=1000 Seq=2}
- Layer 4 (56 bytes) = Payload  56 byte(s)
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:59.4441 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:71:96:76:62:f4 DstMAC=dc:a6:32:bb:8e:a2 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=35448 Flags=DF FragOffset=0 TTL=63 Protocol=ICMPv4 Checksum=6916 SrcIP=192.168.10.108 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoRequest Checksum=17491 Id=1000 Seq=3}
- Layer 4 (56 bytes) = Payload  56 byte(s)
PACKET: 98 bytes, wire length 98 cap length 98 @ 2021-12-21 09:35:59.444199 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..84..] SrcMAC=dc:a6:32:bb:8e:a2 DstMAC=dc:71:96:76:62:f4 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..64..] Version=4 IHL=5 TOS=0 Length=84 Id=38355 Flags= FragOffset=0 TTL=64 Protocol=ICMPv4 Checksum=20137 SrcIP=192.168.10.112 DstIP=192.168.10.108 Options=[] Padding=[]}
- Layer 3 (08 bytes) = ICMPv4   {Contents=[..8..] Payload=[..56..] TypeCode=EchoReply Checksum=19539 Id=1000 Seq=3}
- Layer 4 (56 bytes) = Payload  56 byte(s)
```

パケットがデコードされて各レイヤーの情報が表示されていることが確認できました。

### パケットをキャプチャしてみる
[OpenLive](https://pkg.go.dev/github.com/google/gopacket/pcap#OpenLive)を使うとリアルタイムでのパケットのやりとりができます。
パケットのフィルタリングには[SetBPFFilter](https://pkg.go.dev/github.com/google/gopacket/pcap#Handle.SetBPFFilter)を使います。

<details>
<summary>capture_packet.go</summary><div>

```go
package main

import (
	"fmt"
	"log"

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

func main() {
	handle, err := pcap.OpenLive("eth0", 1600, true, pcap.BlockForever)
	if err != nil {
		log.Fatal(err)
	}
	if err := handle.SetBPFFilter("tcp and port 54321"); err != nil {
		log.Fatal(err)
	}
	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())

	for packet := range packetSource.Packets() {
		fmt.Println(packet)
	}
}
```

</div></details>

実行にはlibpcapライブラリが必要なのでインストールしておきます。

```
sudo apt install libpcap-dev
```

パケットのキャプチャは一般ユーザーではできないのでsudoで実行します。
実行後、ncコマンドを用いてTCPパケットを送信してみたところ、以下のようにパケットをキャプチャできました。

**実行結果**

```
$ sudo go run capture_packet.go
PACKET: 74 bytes, wire length 74 cap length 74 @ 2021-12-21 08:42:15.746802 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..60..] SrcMAC=00:15:5d:c9:b8:96 DstMAC=00:15:5d:a7:47:b1 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..40..] Version=4 IHL=5 TOS=0 Length=60 Id=9234 Flags=DF FragOffset=0 TTL=64 Protocol=TCP Checksum=59083 SrcIP=172.28.184.169 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (40 bytes) = TCP      {Contents=[..40..] Payload=[] SrcPort=55676 DstPort=54321 Seq=3966412498 Ack=0 DataOffset=10 FIN=false SYN=true RST=false PSH=false ACK=false URG=false ECE=false CWR=false NS=false Window=29200 Checksum=12301 Urgent=0 Options=[..5..] Padding=[]}

PACKET: 74 bytes, wire length 74 cap length 74 @ 2021-12-21 08:42:15.747786 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..60..] SrcMAC=00:15:5d:a7:47:b1 DstMAC=00:15:5d:c9:b8:96 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..40..] Version=4 IHL=5 TOS=0 Length=60 Id=0 Flags=DF FragOffset=0 TTL=63 Protocol=TCP Checksum=3038 SrcIP=192.168.10.112 DstIP=172.28.184.169 Options=[] Padding=[]}
- Layer 3 (40 bytes) = TCP      {Contents=[..40..] Payload=[] SrcPort=54321 DstPort=55676 Seq=1414423462 Ack=3966412499 DataOffset=10 FIN=false SYN=true RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=65160 Checksum=145 Urgent=0 Options=[..5..] Padding=[]}

PACKET: 66 bytes, wire length 66 cap length 66 @ 2021-12-21 08:42:15.747821 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..52..] SrcMAC=00:15:5d:c9:b8:96 DstMAC=00:15:5d:a7:47:b1 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..32..] Version=4 IHL=5 TOS=0 Length=52 Id=9235 Flags=DF FragOffset=0 TTL=64 Protocol=TCP Checksum=59090 SrcIP=172.28.184.169 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (32 bytes) = TCP      {Contents=[..32..] Payload=[] SrcPort=55676 DstPort=54321 Seq=3966412499 Ack=1414423463 DataOffset=8 FIN=false SYN=false RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=229 Checksum=12293 Urgent=0 Options=[TCPOption(NOP:), TCPOption(NOP:), TCPOption(Timestamps:256925883/312843622 0x0f5060bb12a59d66)] Padding=[]}

PACKET: 72 bytes, wire length 72 cap length 72 @ 2021-12-21 08:42:15.747854 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..58..] SrcMAC=00:15:5d:c9:b8:96 DstMAC=00:15:5d:a7:47:b1 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..38..] Version=4 IHL=5 TOS=0 Length=58 Id=9236 Flags=DF FragOffset=0 TTL=64 Protocol=TCP Checksum=59083 SrcIP=172.28.184.169 DstIP=192.168.10.112 Options=[] Padding=[]}
- Layer 3 (32 bytes) = TCP      {Contents=[..32..] Payload=[..6..] SrcPort=55676 DstPort=54321 Seq=3966412499 Ack=1414423463 DataOffset=8 FIN=false SYN=false RST=false PSH=true ACK=true URG=false ECE=false CWR=false NS=false Window=229 Checksum=12299 Urgent=0 Options=[TCPOption(NOP:), TCPOption(NOP:), TCPOption(Timestamps:256925883/312843622 0x0f5060bb12a59d66)] Padding=[]}
- Layer 4 (06 bytes) = Payload  6 byte(s)

PACKET: 66 bytes, wire length 66 cap length 66 @ 2021-12-21 08:42:15.749807 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..52..] SrcMAC=00:15:5d:a7:47:b1 DstMAC=00:15:5d:c9:b8:96 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..32..] Version=4 IHL=5 TOS=0 Length=52 Id=9330 Flags=DF FragOffset=0 TTL=63 Protocol=TCP Checksum=59251 SrcIP=192.168.10.112 DstIP=172.28.184.169 Options=[] Padding=[]}
- Layer 3 (32 bytes) = TCP      {Contents=[..32..] Payload=[] SrcPort=54321 DstPort=55676 Seq=1414423463 Ack=3966412505 DataOffset=8 FIN=false SYN=false RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=510 Checksum=11231 Urgent=0 Options=[TCPOption(NOP:), TCPOption(NOP:), TCPOption(Timestamps:312843624/256925883 0x12a59d680f5060bb)] Padding=[]}
```


### パケットを生成してみる

SerializeToを用いることで各レイヤーをSerializeBufferに追加することができます。

<details>
<summary>create_packet.go</summary><div>

```go
package main

import (
	"fmt"
	"log"
	"net"

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

func main() {
	srcMAC, err := net.ParseMAC("00:00:5e:00:53:01")
	if err != nil {
		log.Fatal(err)
	}
	dstMAC, err := net.ParseMAC("00:00:5e:00:53:02")
	if err != nil {
		log.Fatal(err)
	}

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

	ip := &layers.IPv4{
		Version:  4,
		IHL:      5,
		TTL:      64,
		Protocol: layers.IPProtocolTCP,
		SrcIP:    net.IP{192, 168, 1, 10},
		DstIP:    net.IP{192, 168, 1, 11},
	}

	tcp := &layers.TCP{
		SrcPort:    54321,
		DstPort:    65432,
		DataOffset: 5,
		ACK:        true,
	}

	app := &gopacket.Payload{104, 101, 108, 108, 111, 10}

	buf := gopacket.NewSerializeBuffer()
	opts := gopacket.SerializeOptions{}

	if err := app.SerializeTo(buf, opts); err != nil {
		log.Fatal(err)
	}

	if err := tcp.SerializeTo(buf, opts); err != nil {
		log.Fatal(err)
	}

	if err := ip.SerializeTo(buf, opts); err != nil {
		log.Fatal(err)
	}

	if err := ethernet.SerializeTo(buf, opts); err != nil {
		log.Fatal(err)
	}

	fmt.Println(gopacket.NewPacket(buf.Bytes(), layers.LayerTypeEthernet, gopacket.Default))

}

```

</div></details>

**実行結果**

```
PACKET: 60 bytes
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..46..] SrcMAC=00:00:5e:00:53:01 DstMAC=00:00:5e:00:53:02 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..26..] Version=4 IHL=5 TOS=0 Length=46 Id=0 Flags= FragOffset=0 TTL=64 Protocol=TCP Checksum=0 SrcIP=192.168.1.10 DstIP=192.168.1.11 Options=[] Padding=[]}
- Layer 3 (20 bytes) = TCP      {Contents=[..20..] Payload=[..6..] SrcPort=54321 DstPort=65432 Seq=0 Ack=0 DataOffset=5 FIN=false SYN=false RST=false PSH=false ACK=true URG=false ECE=false CWR=false NS=false Window=0 Checksum=0 Urgent=0 Options=[] Padding=[]}
- Layer 4 (06 bytes) = Payload  6 byte(s)
```

各レイヤーを追加したSerializeBufferをNewPacketでデコードしてみると、実際にパケットが生成されていることが確認できます。

また、上の例では各レイヤーを一つずつSerializeToでSerializeBufferに追加しましたが、[SerializeLayers](https://pkg.go.dev/github.com/google/gopacket#SerializeLayers)を用いると複数のレイヤーを一度にSerializeBufferに追加することができます。


### パケットを送信してみる

次に、パケットを生成して送信してみます。
[パケットを読み込んでみる](/#パケットを読み込んでみる)で読み込んだパケットを参考にしてICMPパケットを生成しました。
パケットの送信には[Handle.WritePacketData](https://pkg.go.dev/github.com/google/gopacket/pcap#Handle.WritePacketData)を使います。

<details>
<summary>send_packet.go</summary><div>

```go
package main

import (
	"log"
	"net"

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

func main() {
	srcMAC, err := net.ParseMAC("00:15:5d:c9:b8:96")
	if err != nil {
		log.Fatal(err)
	}
	dstMAC, err := net.ParseMAC("00:15:5d:a7:47:b1")
	if err != nil {
		log.Fatal(err)
	}

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

	ip := &layers.IPv4{
		Version:  4,
		TTL:      64,
		Protocol: layers.IPProtocolICMPv4,
		SrcIP:    net.IP{172, 28, 184, 169},
		DstIP:    net.IP{192, 168, 10, 112},
	}

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

	buf := gopacket.NewSerializeBuffer()
	opts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}

	if err := gopacket.SerializeLayers(
		buf,
		opts,
		ethernet,
		ip,
		icmp,
	); err != nil {
		log.Fatal(err)
	}

	handle, err := pcap.OpenLive("eth0", 1600, true, pcap.BlockForever)
	if err != nil {
		log.Fatal(err)
	}

	handle.WritePacketData(buf.Bytes())

}

```

</div></details>

**実行結果（サーバー側のtcpdumpの結果）**

```
23:23:24.960041 IP 192.168.10.108 > 192.168.10.112: ICMP echo request, id 1000, seq 0, length 8
23:23:24.960230 IP 192.168.10.112 > 192.168.10.108: ICMP echo reply, id 1000, seq 0, length 8
```

パケットの宛先のサーバーにパケットが届いていることが確認できました！

## まとめ

今回は、Gopacketを使って実際にパケットキャプチャしたりパケットを生成したりしてみました。
パケットを実際に生成して送信する際に、各レイヤーのヘッダーに定義されているフィールドの種類や、通信を行うのに必要な情報などを改めて調べることで、「パケットの気持ちがわかる」に一歩近づくことができたのではないかと思っています。

明日は@s_aveさんの「LiteSpeedに関する記事を予定」です。お楽しみに！