- 1 Stop Guessing What’s on Your Wire
- 2 One-Minute Checklist Before We Start
- 3 Step 1 – A 30-Line BPF Program That Counts SYN Packets
- 4 Step 2 – Turn Source Code into Kernel Byte-Code
- 5 Step 3 – Load It Into the Kernel
- 6 Step 4 – Snap It onto Your NIC
- 7 Step 5 – Watch the Numbers Roll In
- 8 Finished? Clean Up
- 9 Pro Tips From My Last Fire-Drill
- 10 Still Curious?
Stop Guessing What’s on Your Wire
Last Friday I lost three hours chasing a phantom.
My web app felt sluggish. Logs? Clean. Load average? Fine.
Then I ran one tiny BPF program and saw the truth: 11,432 TCP-SYN packets per second from a single mis-configured container.
Fixed in two minutes. That’s why I’m writing this post.
In 2025 bpftool is still the fastest way to get that kind of visibility on Linux.
Below I’ll walk you through the exact steps I used—no PhD in kernel internals required.
One-Minute Checklist Before We Start
- Kernel 6.8 or newer (check with
uname -r) - Install the user-space bits:
sudo apt install linux-tools-common libbpf-dev clang-18 llvm-18(Ubuntu/Debian)sudo dnf install bpftool kernel-headers libbpf-devel clang(RHEL/Fedora)
Done? Great. Let’s build a traffic spy that runs inside the kernel.
Step 1 – A 30-Line BPF Program That Counts SYN Packets
Copy this into monitor_tcp.c:
// monitor_tcp.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32); // source IPv4
__type(value, __u64); // counter
} syn_packets SEC(".maps");
SEC("xdp")
int count_syn_packets(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (eth + 1 > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = data + sizeof(*eth);
if (ip + 1 > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_TCP) return XDP_PASS;
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
if (tcp + 1 > data_end) return XDP_PASS;
if (tcp->syn && !tcp->ack) {
__u32 src_ip = ip->saddr;
__u64 *count = bpf_map_lookup_elem(&syn_packets, &src_ip);
if (count)
__sync_fetch_and_add(count, 1);
else {
__u64 one = 1;
bpf_map_update_elem(&syn_packets, &src_ip, &one, BPF_ANY);
}
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Nothing fancy—just counts every TCP-SYN that reaches your NIC.
Step 2 – Turn Source Code into Kernel Byte-Code
clang -O2 -g -Wall -target bpf -D__TARGET_ARCH_x86_64 -c monitor_tcp.c -o monitor_tcp.o
Step 3 – Load It Into the Kernel
sudo bpftool prog load monitor_tcp.o /sys/fs/bpf/monitor_tcp
Step 4 – Snap It onto Your NIC
sudo bpftool net attach xdp pinned /sys/fs/bpf/monitor_tcp dev eth0
Replace eth0 with your real interface.
If you’re on Wi-Fi you might need dev wlan0 instead.
Step 5 – Watch the Numbers Roll In
Find the map ID:
MAP_ID=$(sudo bpftool map list | awk '/syn_packets/{print $1}')
Live dashboard (refreshes every second):
watch -n1 'sudo bpftool map dump id "$MAP_ID" \
| awk "NF==4 {print \$1, \$3}" \
| sed "s/0x//g" \
| xargs -n2 printf "%d.%d.%d.%d %s\n"'
You’ll see something like:
192.168.1.42 312 203.0.113.7 9 10.244.0.15 11432 ← there’s my noisy container
Finished? Clean Up
sudo bpftool net detach xdp dev eth0
Pro Tips From My Last Fire-Drill
- Ring buffer in 2025: If you need events instead of counters use
BPF_MAP_TYPE_RINGBUF—zero copy, huge throughput. - Kubernetes: Install
kubectl-bpfand attach the same program to every node in one line:
kubectl bpf attach xdp -f monitor_tcp.o -i eth0 - Stuck? Always run
dmesg | grep bpffirst. The verifier is annoyingly good at telling you what you broke.
Still Curious?
The official docs are actually readable these days:
- BPF and XDP Reference Guide
man bpftool(v8+)
Grab the code, spin it up, and stop flying blind. Your next outage will thank you.







