Headless BPF

December 30, 2023
golang pcap bpf

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.

Byte Replace

October 19, 2023

You are probably very familiar with the strings function to replace a substring:

golang benchmark patch

That String Array

October 11, 2019

If you are using gqlgen to generate your GraphQL endpoint and sqlx or some other kind of ORM for your database, you can benefit from using one type or struct in both directions. You create one type User struct for GraphQL and it maps to your db schema. Now if this user contains more complex types, things become interesting…

golang gqlgen graphql sql cockroachdb