034 Project 34: eBPF XDP Packet Counter
034 Build an eBPF XDP Packet Counter
Attach an XDP program to a network interface, increment a packet counter in a BPF map, and poll the counter from userspace every second. Linux lab only—use a VM or spare interface.
NIC RX -> XDP program -> map++ -> userspace Lookup every 1s
Problem statement
Goals:
- Attach XDP to an interface index (or name)
- Kernel program counts packets (and optionally bytes)
- Userspace prints
packets=Neach second - Clean detach on exit
Acceptance criteria
- Document interface selection (
ip link) - Counter increases under traffic (
ping,curl) - Userspace reads map without racey panics
- Defer close/detach on SIGINT
- Lab safety notes (do not brick remote prod NIC)
Setup
mkdir xdp-count && cd xdp-count
go mod init example.com/xdp-count
go get github.com/cilium/ebpf@latest
go install github.com/cilium/ebpf/cmd/bpf2go@latest
# clang/llvm + headers as in project 33
# go 1.27Kernel sketch count.bpf.c
//go:build ignore
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} packet_count SEC(".maps");
SEC("xdp")
int xdp_count(struct xdp_md *ctx) {
__u32 key = 0;
__u64 *val = bpf_map_lookup_elem(&packet_count, &key);
if (val) {
__sync_fetch_and_add(val, 1);
}
return XDP_PASS; // do not drop traffic
}Generate:
# //go:generate bpf2go -cc clang bpf count.bpf.c -- -I...
go generate ./...Userspace main.go
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
func ifaceIndex(name string) (int, error) {
iface, err := net.InterfaceByName(name)
if err != nil {
return 0, err
}
return iface.Index, nil
}
func main() {
ifname := flag.String("iface", "lo", "interface name")
flag.Parse()
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
idx, err := ifaceIndex(*ifname)
if err != nil {
log.Fatal(err)
}
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("load: %v", err)
}
defer objs.Close()
l, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpCount, // name from bpf2go
Interface: idx,
})
if err != nil {
log.Fatalf("attach xdp on %s (%d): %v", *ifname, idx, err)
}
defer l.Close()
fmt.Printf("XDP attached on %s index=%d; Ctrl+C to stop\n", *ifname, idx)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-sig:
fmt.Println("detaching")
return
case <-ticker.C:
var key uint32 = 0
var value uint64
if err := objs.PacketCount.Lookup(&key, &value); err != nil {
log.Println("lookup:", err)
continue
}
fmt.Printf("packets=%d\n", value)
}
}
}Field names (
XdpCount,PacketCount) must matchbpf2gooutput—rename to match generated code.
Run and verification
ip -br link
go generate ./... && go build -o xdp-count .
sudo ./xdp-count -iface lo
# other terminal:
ping -c 20 127.0.0.1
# counter should riseSafety
- Prefer lab VMs and
loor a throwaway veth pair. - Always return
XDP_PASSuntil you know what you are dropping. - Remote servers: a bad XDP program can cut SSH—use out-of-band console.
Stretch goals
- Per-protocol counters (parse Ethernet/IP safely with helpers).
- Bytes counter as well as packets.
bpftool prog show/ip link showverification notes.- Pin maps under
/sys/fs/bpffor multi-process readers.
Pitfalls
| Pitfall | Fix |
|---|---|
| Hard-coded ifindex | resolve by name |
XDP_DROP by mistake |
start with PASS |
| No memlock rlimit | rlimit.RemoveMemlock |
| Generated names mismatch | read *_bpfel.go |
Learning goals
- XDP attach path vs tracepoints
- Array maps for counters
- Operational safety for datapath programs