I’ve been using the Berkeley Packet Filter from Golang’s wrapper around the PCAP library whenever I needed to filter network packets programmatically:
handle, _ := pcap.OpenLive("eth0", 1600, true, pcap.BlockForever)
matcher, _ := handle.CompileBPFFilter("tcp dst port 21")
matched, _ := matcher.Run(data)
This approach works well if you are capturing packets with PCAP or processing a PCAP file offline. However, it becomes a limitation if you want to operate in a more “headless” manner and have a socket listener for your packets.
In my specific case, I’m running a single listener receiving arbitrary network traffic, which I’d like to process based on the outcome of applying BPF matchers.
Fortunately, the wrapper also allows the creation of a “headless” matcher:
matcher, _ = pcap.NewBPF(layers.LinkTypeEthernet, 65535, "tcp dst port 21")
matched := matcher.Matches(gopacket.CaptureInfo{}, data)
My challenge now is that data
in this context is the entire packet with all layers. With my socket listener, I don’t capture full packets but read the payloads from the connection. To make use of the “headless” matcher, I had to mock the packet structure and then serialize it into a data stream.
I created a helper function that receives the IPs and ports required for the TCP layer (UDP is omitted for brevity). The Ethernet layer is just mocked as it wasn’t required for my case, but it can be populated if necessary.
func fakePacketBytes(srcIP, dstIP string, srcPort, dstPort uint16, payload []byte) ([]byte, error) {
eth := &layers.Ethernet{
SrcMAC: net.HardwareAddr{0x0, 0x11, 0x22, 0x33, 0x44, 0x55},
DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
EthernetType: layers.EthernetTypeIPv4,
}
ipv4 := &layers.IPv4{
SrcIP: net.ParseIP(srcIP),
DstIP: net.ParseIP(dstIP),
Version: 4,
}
ipv4.Protocol = layers.IPProtocolTCP
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(dstPort),
}
if err := tcp.SetNetworkLayerForChecksum(ipv4); err != nil {
return nil, err
}
transport := tcp
buf := gopacket.NewSerializeBuffer()
if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
},
eth,
ipv4,
transport,
gopacket.Payload(payload)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
Now the returned byte slice from this helper can passed to the matcher with the BPF rule tcp dst port 21
like so:
matcher, _ = pcap.NewBPF(layers.LinkTypeEthernet, 65535, "tcp dst port 21")
data, _ := fakePacketBytes("127.0.0.1", "127.0.0.1", 3345, 21, []byte{})
matched := matcher.Matches(gopacket.CaptureInfo{}, data)
Which will be true in this case since we passed the destination port 21
when compiling the packet.