Berkeley Packet Filter in Golang
Recently I stumbled upon a use case where I was looking to setup a Wireguard (udp) tunnel across two nodes behind different NATs. I wanted to implement something like
UDP Hole Punching and came across this mailing list message
from 2016 which linked to the contrib/nat-hole-punching
source in Wireguard.
I’ll cover the whole hole punching mechanism, perhaps, in a different article. But for context, the mentioned source used raw
linux sockets
and classic Berkeley Packet Filter to inject custom packets and do hole punching.
The source is written in C but I wanted my implementation in Golang. So, I started looking and soon came across golang.org/x/net/bpf
which provides an implementation of raw bpf
assembly codes and a virtual machine written in Golang. I wanted to use the assembly codes but not necessarily depend on the bpf.VM
(that would’ve defeated the purpose of filters in the first place 😅).
Instead, I started looking for ways in which I could use setsockopt
with SO_ATTACH_FILTER
to attach the filter directly to the native
file descriptor. I came across ipv4#PacketConn.SetBPF
method from golang.org/x/net/ipv4
which takes in a []bpf.RawInstructions
and applies it onto the underlying socket, although the socket in this case wasn’t exactly the raw
socket I was working with.
To circumvent this, I started looking further into the source, till I found sockOpt.setAttachFilter
and, consequently, setsockopt
’s implementation on unix. The setsockopt
used syscall.Syscall6
with
syscall.SYS_SETSOCKOPT
to implement the system call and passed through the given arguments as unsafe.Pointer
to the syscall.
Putting it all together⌗
After all this I came with something close the following code snippet to enable using berkeley filter with sockets in Golang 🚀
package filter
import (
"golang.org/x/net/bpf"
"golang.org/x/sys/unix"
"syscall"
"unsafe"
)
// Filter represents a classic BPF filter program that can be applied to a socket
type Filter []bpf.Instruction
// ApplyTo applies the current filter onto the provided file descriptor
func (filter Filter) ApplyTo(fd int) (err error) {
var assembled []bpf.RawInstruction
if assembled, err = bpf.Assemble(filter); err != nil {
return err
}
var program = unix.SockFprog{
Len: uint16(len(assembled)),
Filter: (*unix.SockFilter)(unsafe.Pointer(&assembled[0])),
}
var b = (*[unix.SizeofSockFprog]byte)(unsafe.Pointer(&program))[:unix.SizeofSockFprog]
if _, _, errno := syscall.Syscall6(syscall.SYS_SETSOCKOPT,
uintptr(fd), uintptr(syscall.SOL_SOCKET), uintptr(syscall.SO_ATTACH_FILTER),
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), 0); errno != 0 {
return errno
}
return nil
}
Using this is simple too! You just have to define the filter program using bpf
assembly codes, like:
// filter packet by checking if they are destined to local port 8080
var filter = Filter{
bpf.LoadAbsolute{Off: 22, Size: 2}, // load the destination port
bpf.JumpIf{Val: 8080, SkipFalse: 1}, // if Val != 8080 skip next instruction
bpf.RetConstant{Val: 0xffff}, // return 0xffff bytes (or less) from packet
bpf.RetConstant{Val: 0x0}, // return 0 bytes, effectively ignore this packet
}
and call ApplyTo
on the socket’s file descriptor.
// import "syscall"
// open a raw socket
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_UDP)
if err != nil {
// ... define error handling
}
// then apply the filter
err = filter.ApplyTo(fd)
if err != nil {
// ... define error handling
}
And that’s it! the kernel would filter out packets based on specified filter program and next time you do a syscall.Read
on your fd
you’d only see the packets you’re interested in 🎉 🚀