Capturing traffic with Golang
Aug 31, 2024Introduction #
Most of the people from Software Engineering world heard about TCPDump, WireShark, etc. And most likely you have heard about libpcap1 library developed by TCPDump team to capture traffic and used by Wireshark as well.
This library1 provides a flexible interface to capture a traffic from a network interface and process it in a code. It provides a flexibility to write performant code and include as much business logic as needed to collect only required data.
Recently I’ve identified a task where it would be a good idea to collect required packets, analyze them and save for the future manual review. To review manually WireShak can be used to load pcap file and manually review the collected packets using nice UI.
And the most important part, why Go? Go is a nice simple enough language to write code and support it by multiple team members. It’s much more safe than C and C++ and requires much less experience for other people to support it without unexpected surpises. Until we don’t have requirements when we HAVE TO go with much performant language (C, C++, rust, etc.), I prefer to choose Golang.
Task definition #
Before we will do something, it would be great to understand, what do we want to achieve in a result? Let’s define a short list of requirements.
Requirements #
To keep implementation as simple as possible, let’s define only few points:
- We want to collect outcoming traffic
- Let’s collect IPv4 traffic
- Ignore private networks traffic
- And let’s keep UDP packets
These simple few points will be enough to get an idea on how to use libpcap1 from Golang. After that it’s only matter of imagination on what logic will be added on top of that.
Implementation #
Before we will start, let’s define that code is not suppose to be a production ready. Our goal is to see the minimum example and test that it works well.
We will use these libraries:
slog
for loggingithub.com/google/gopacket
for capturing packets and formattingpcap
file format
An interface in the future code will be eth0
, even though most likely your system will have a different interaface name.
Working code #
This is the code with comments you can copy and try in your own environment. Since we use libpcap1, our application will use CGO and we will need to run the app from root user.
package main
import (
"bytes"
"log/slog"
"net"
"os"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"github.com/google/gopacket/pcapgo"
)
const (
interfaceName = "eth0"
snaplen = 1500
)
func main() {
slog.Info("Running our applicaiton...")
// Get handler attached to an interface.
handle, err := pcap.OpenLive(interfaceName, snaplen, true, pcap.BlockForever)
if err != nil {
slog.Error("Could not OpenLive", slog.String("err", err.Error()))
os.Exit(1)
}
iface, err := net.InterfaceByName(interfaceName)
if err != nil {
slog.Error("Could not OpenLive", slog.String("err", err.Error()))
os.Exit(1)
}
// Start new Source reader.
source := gopacket.NewPacketSource(handle, handle.LinkType())
// This is suppose to be a file writer, but we will use memory, just for simplification.
fileWriter := bytes.NewBuffer(nil)
pcapWriter := pcapgo.NewWriterNanos(fileWriter)
err = pcapWriter.WriteFileHeader(snaplen, handle.LinkType())
if err != nil {
slog.Error("Could not write pcap header", slog.String("err", err.Error()))
os.Exit(1)
}
// Reading packages.
for packet := range source.Packets() {
// Filter by outcoming traffic only.
// To filter it, we need to compare MAC addresses from out interface and source MAC.
// To access a mac Address we need to get an Ethernet layer.
layer := packet.Layer(layers.LayerTypeEthernet)
ethernet, ok := layer.(*layers.Ethernet)
if !ok {
slog.Error("Could not get Ethernet layer")
continue
}
if !bytes.Equal(ethernet.SrcMAC, iface.HardwareAddr) {
// Our interface did not send this packet. It's not outcoming.
continue
}
// Now we need to identify IPv4 layer.
layer = packet.Layer(layers.LayerTypeIPv4)
ipv4, ok := layer.(*layers.IPv4)
if !ok {
// It's not IPv4 traffic.
continue
}
if ipv4.DstIP.IsPrivate() {
// Do not collect private traffic.
continue
}
if ipv4.Protocol != layers.IPProtocolUDP {
// Ignore not UDP protocol.
continue
}
err = pcapWriter.WritePacket(packet.Metadata().CaptureInfo, packet.Data())
if err != nil {
slog.Error("Could not write a packet to a pcap writer", slog.String("err", err.Error()))
continue
}
slog.Info("Stored packet", slog.Any("packet", packet))
// Let's collect ONLY 100K bytes, just for example perposes.
if fileWriter.Len() > 100000 {
break
}
}
slog.Info("We have successfuly collected bytes", slog.Int("bytes", fileWriter.Len()))
}
And after running there the truncated output looks like that:
2024/08/31 13:35:36 INFO Running our applicaiton...
2024/08/31 13:37:48 INFO Stored packet packet="PACKET: 105 bytes, wire length 105 cap length 105 ..."
...
2024/08/31 13:37:48 INFO Stored packet packet="PACKET: 1291 bytes, wire length 1291 cap length 1291 ..."
2024/08/31 13:37:48 INFO We have successfuly collected bytes bytes=101018
I hope this minimum example will help someone to start their jorney in this area using Go.
Afterword #
If you meet any new requirements in your everyday tasks that you have no idea how to do. Don’t hasitate to ask people around, write me in a private and do your own research.
Research and communication is a key to all problems solving.