This is a daemon that reads config files that describe BPF (tcpdump) filter expressions that should be applied to a network interface via XDP, and when the filter is matched the matching packet is dropped.
The tool relies heavily on the Cloudflare
cbpfc library for generating eBPF from
classic BPF. On top of this we use
gopacket to allow us the write actual BPF
filter strings (like you would do when using tcpdump) which it then compiles
into the BPF instructions that cbpfc wants.
A given filter can be applied in a "monitor" mode, and when this is done the packets are matched but not dropped, which can be helpful for figuring out the impact of adding a given filter.
The eBPF code is instrumented to call hook maps for
xdpcap, so that you can capture pcap of
matched packets for further analysis. There is one hook per interface for
dropped packets (<ifname>-drop) and one for packets matched by a filter in
monitor mode (<ifname>-monitor), e.g. for eth0:
xdpcap /sys/fs/bpf/sunet-xdpd/eth0-drop - "" | tcpdump -nr -
xdpcap /sys/fs/bpf/sunet-xdpd/eth0-drop - "tcp and port 80" | tcpdump -nr -
xdpcap /sys/fs/bpf/sunet-xdpd/eth0-drop dropped.pcap "tcp and port 80"
xdpcap /sys/fs/bpf/sunet-xdpd/eth0-monitor - "" | tcpdump -nr -
Matched packets are counted per filter and are visible in prometheus metrics available at 127.0.0.1:2112/metrics, e.g.:
curl http://127.0.0.1:2112/metrics | grep ^filter
The config is read from every *.json file in the directory given by
-config-dir (default /etc/sunet-xdpd/conf.d), and the filters for an
interface are appended across files. This way separate processes can each own a
file, for example:
/etc/sunet-xdpd/conf.d/00-base.json # from config management
/etc/sunet-xdpd/conf.d/50-ddos.json # from a DDoS mitigation tool
See conf.d.sample for the file format. The rules are:
- Files are read in name order, sorted as strings, so
10-x.jsoncomes before9-x.json. Names starting with., other suffixes and directories are skipped. - To change a file, write a temporary file starting with
.in the same directory and rename it into place, so a half written file is never read. Then reload withpkill -HUP sunet-xdpd, once all files of a change are in place. - A reload is all or nothing: if any file is broken (invalid JSON, an unknown
field or one given twice, a missing or empty
expr, an expression that doesn't compile) nothing changes and the error, naming the file, is logged. A broken file from one writer blocks changes from all of them until it is fixed. - Two filters on one interface with the same description, expr and monitor setting are an error, also when they are in different files.
- Removing a file removes its filters on the next reload. A directory without
*.jsonfiles is an error rather than detaching every filter, so to run without filters use a file containing{}, orsunet-xdpd -unload. - The log line after a load or reload lists the files that were read and how many filters each added.
The Dockerfile is not meant for creating a container, rather it is a way to more easily build a static executable even when the google/gopacket dependency requires use of CGO and needs to link with libpcap for BPF expression compilation. This way we do not have to care about installing libpcap on machines running the tool.
Example for building binaries for two different targets with binaries ending up in the "dist" directory can look like this:
docker buildx build --platform linux/amd64,linux/arm64 -o dist .
When working with this code at least the following tools are expected to be run at the top level directory prior to commiting:
gofumpt -l -w .(see gofumpt)go vet ./...staticcheck ./...(see staticcheck)gosec ./...(see gosec)golangci-lint run(see golangci-lint)go test ./...govulncheck ./...(see govulncheck)