パケットキャプチャといえば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
sudo yum -y install libpcap-devel
sudo yum -y install libnet
gopacket
# パケットに関してのライブラリ
go get github.com/google/gopacket
ソースコード
pcapを実行するときは基本的にsudoを付けて実行するので、$PATHを引き継ぐようにオプションをつけるか、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のパケットの中身を見る
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リクエストのパケットを見てみる。
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サーバからのレスポンスだってことが読み取れる。
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.プロトコル)
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&!rgq�
���)^�A�\I9����_1dv
パケットを送信する
handle.WritePacketDataでパケットを送信する。
送信するIP、MACアドレス、ポートはgopacket.SerializeLayersにて指定する。
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で苦しみながらやるのが良いかなーと思いました。