Linux terminal window showing eBPF XDP program compilation and bpftool loading commands
eBPF Use Cases
William  

Block IP Addresses With eBPF: XDP Attach Syntax

Run ip link set dev eth0 xdp pinned /sys/fs/bpf/xdp_pass only after you have already loaded and pinned a real XDP program at that path. The command does not compile or load anything. It takes a program object that is already sitting in the BPF filesystem and attaches it to eth0. That is the whole trick to block IP addresses with eBPF from a pinned object: load and pin first with bpftool, then let ip link do the attach. If the attach throws an errno, the answer is in dmesg and the verifier log, not in retyping the line with different spacing.

Last updated: 2026-07-21

What does ip link set dev eth0 xdp pinned actually do

It attaches an already-loaded, already-pinned XDP program to a network device. Nothing more. The pinned keyword tells ip to reopen a file descriptor from /sys/fs/bpf/ instead of loading an ELF object off disk. So the program has to exist in the kernel first, with a pin at that exact path.

Here is what happens under the hood. When you loaded the program earlier, the kernel gave it an id and a file descriptor. Pinning wrote that descriptor into bpffs so it survives after your loader exits. ip link ... xdp pinned reopens that descriptor and hangs the program off the interface's XDP hook.

The official syntax has two attach forms. One is object FILE where ip loads the ELF itself. The other is pinned FILE, which is what you want when bpftool already did the loading. Mixing them up is the first mistake I see: people pin with bpftool, then run the object form and wonder why nothing lines up.

How do you load and pin an XDP program with bpftool first

Load and pin in one command before you touch ip link:

bpftool prog load xdp_pass.o /sys/fs/bpf/xdp_pass

That reads xdp_pass.o, runs it through the verifier, loads it into the kernel, and pins the resulting program at /sys/fs/bpf/xdp_pass. If the verifier rejects it, this is where you find out, and the rejection prints right there. No pin file gets created on failure, so a missing pin after this command means the load never succeeded.

The full form is bpftool prog { load | loadall } OBJ PATH [ type TYPE ] [ map ... ] [ dev NAME ] [ pinmaps MAP_DIR ]. For a single program, load is what you want. Use loadall when the object holds several programs and you want every one pinned under a directory.

One gotcha: bpffs must be mounted at /sys/fs/bpf. On most systemd distros it already is. If the pin fails with an error about the filesystem, run mount | grep bpf and mount it yourself if it is absent.

What does a full bpftool prog load xdp_pass.o /sys/fs/bpf/xdp_pass example look like end to end

Start from the C source and walk it through to an attached program. This is the shortest path that actually works.

clang -O2 -g -target bpf -c xdp_pass.c -o xdp_pass.o

bpftool prog load xdp_pass.o /sys/fs/bpf/xdp_pass

bpftool prog show pinned /sys/fs/bpf/xdp_pass

ip link set dev eth0 xdp pinned /sys/fs/bpf/xdp_pass

ip link show dev eth0

Step 5 should now print xdp next to the interface. If it does not, the attach silently failed or fell back to a mode you did not ask for. That is your cue to read the next two sections before you rerun step 4.

The -g flag on the compile keeps BTF and line info in the object. You want it. When the verifier complains, BTF turns a wall of register numbers into a log that points at a source line.

Why do you need type xdp when the section name should already say so

Add type xdp when the ELF section name does not follow the xdp prefix convention. bpftool infers the program type from the section name, so a section called xdp or xdp/pass loads as an XDP program with no extra flag. Name it something bpftool does not recognize and the load either fails or guesses wrong.

bpftool prog load xdp_pass.o /sys/fs/bpf/xdp_pass type xdp

That line forces the type regardless of the section name. I reach for it when I inherit an object I did not compile and cannot trust the naming. Guessing wrong here is nasty. A program loaded as the wrong type pins fine, then refuses to attach to the XDP hook. The error points at the attach, not the load.

The real problem in most "it won't attach" tickets is a section name the loader could not map to xdp. Set the type explicitly and that ambiguity disappears.

Verify a pinned program before you attach it

Read the pin before you attach it:

bpftool prog show pinned /sys/fs/bpf/xdp_pass

The output tells you three things that matter. The type field must say xdp. If it says xdpgeneric, cgroup_skb, or anything else, stop, because ip link ... xdp will not attach it. You also get the program id and the fact that it is loaded, which confirms the pin holds a program and not a map or a bpf_link.

That last point trips people up. A pin path can hold a prog, a map, or a link, and they all live in the same bpffs. Point ip link at a pinned map and the attach fails with a type error that reads like nonsense until you realize you pinned the wrong object kind. bpftool prog show pinned on a map path errors out plainly, which is itself the tell.

Cross-check with bpftool prog show (no path) to see every loaded program by id. If your pinned id shows up there with xdp type, you are clear to attach.

What is the difference between xdp, xdpgeneric, and xdpdrv attach modes

Server rack with network equipment and ethernet cables representing kernel-level packet processing

Three attach modes, and picking the wrong one is a common silent failure. Here is the split.

ModeWhere it runsNeeds driver supportWhen to use
xdpdrvNative, in the NIC driverYesProduction, real throughput
xdpgenericIn the network stack, after the driverNoTesting, unsupported NICs
xdpKernel picks native, falls back to genericPrefers nativeGeneral use when unsure

Plain xdp asks the kernel to use native mode and quietly fall back to generic if the driver cannot do it. That fallback is the trap. Your program attaches, ip link reports success, and you think you are running at driver speed while you are actually in the slow generic path. Native mode is where the big win lives, roughly a 4 to 5x jump once JIT compilation is on. Generic mode gives you correctness for testing and little else.

Force xdpdrv when you need to know you got native mode:

ip link set dev eth0 xdpdrv pinned /sys/fs/bpf/xdp_pass

If that errors, your driver does not support native XDP, and now you know it instead of guessing. Common virtual interfaces and some cloud NICs only do generic. Test on real hardware or a driver you have confirmed, which is one reason I keep XDP experiments on a box I can afford to break rather than a production gateway. The same split shows up when you set up an XDP firewall on OpenWRT.

Why does BPF program section naming matter for xdp_pass.o

Name the section with an xdp prefix or the type auto-detection breaks. In the C source, the SEC("xdp") macro sets the ELF section that bpftool and libbpf read to decide the program type. Get it right and load is a one-liner with no type flag.

SEC("xdp")
int xdp_pass(struct xdp_md *ctx) {
 return XDP_PASS;
}

SEC("xdp") or SEC("xdp/pass") both map cleanly to the XDP type. Something like SEC("myfilter") does not, and then you are back to passing type xdp on every load or watching the guess go wrong. This is not cosmetic. The section string is the contract between your object file and the loader.

When you build the real filter that reads struct ethhdr then struct iphdr and compares the source address against a map, the same rule holds. The verdict logic changes, the section convention does not. Keep the naming boring and the tooling stays out of your way.

What should you check when ip link set xdp pinned fails or hangs

Read the failure before you retry any variant. When the attach returns an errno, run these in order and let the output tell you which layer broke.

  1. dmesg | tail -20 – the kernel logs XDP attach failures here, often with the exact reason a driver refused native mode.
  2. bpftool prog show pinned /sys/fs/bpf/xdp_pass – confirms the pin still holds a loaded xdp-type prog and not a stale or wrong-type object.
  3. strace -f ip link set dev eth0 xdp pinned /sys/fs/bpf/xdp_pass – shows the exact syscall that failed and its return value, which turns a vague CLI error into a specific bpf() or setsockopt() errno.
  4. ip link show dev eth0 – tells you whether a previous attach is still sitting there and blocking the new one.

The verifier log is your other friend. It prints at load time, not attach time, so if the program never loaded cleanly, fix that first with bpftool. Retrying the ip link command with different whitespace does nothing, and I have watched people burn an hour on exactly that. Understand the errno or it comes back.

A hang, as opposed to an error, usually means the driver is renegotiating queues to enter native XDP mode. Give it a moment. If it never returns, dmesg will show the driver reset, and dropping to xdpgeneric confirms whether native mode is the culprit. For higher-rate paths, the same diagnostic habits carry over to low-latency packet processing with XDP.

How do you unpin and detach an XDP program cleanly

Detach from the interface and remove the pin, in that order:

ip link set dev eth0 xdp off
rm /sys/fs/bpf/xdp_pass

The xdp off clears the program from the interface hook. The rm removes the bpffs pin. Skip the detach and the program stays live on eth0 even after you delete the file, because the interface still holds a reference. That is the origin of "I deleted it but it is still dropping packets."

Order matters the other way too. Leave a stale pin in place and a future load to the same path fails with File exists. So when a load complains the path is taken, check for an orphaned pin from a crashed loader and rm it. Once both the interface reference and the pin are gone, the kernel frees the program.

Verify with ip link show dev eth0 afterward. No xdp on the line means the hook is clear and you can attach again.

Prerequisites and toolchain to block IP addresses with eBPF

Confirm kernel support and install four tools before anything else. XDP first appeared around kernel 4.8, and newer releases add protocol helpers and verifier fixes, so run uname -r and prefer a recent kernel. Check that the JIT is on, because compiled XDP runs several times faster than the interpreter. If you need to trim a kernel down for this work, the minimal kernel build for eBPF walks the config.

Package / CommandInstall examplePurposeNotes
clang, llvmapt install clang llvmCompile the kernel C programRequired for verifier-friendly object files
bpftoolapt install bpftoolInspect prog, maps, and trace logsUse bpftool prog tracelog piped for realtime monitoring
Go + bpf2goapt install golang-go; go install github.com/cilium/ebpf/cmd/bpf2go@latestGenerate bindings and build the user appInclude cilium/ebpf in go.mod
iproute2apt install iproute2Attach and query interfacesProvides ip link set dev eth0 xdp

Check ulimit -a and raise the locked-memory and file-descriptor limits if a load fails. Change permissions on the sysfs map file to allow non-root write access for safe updates from an application. Keep a single throwaway interface ready and attach there during development, not on anything people depend on.

Building and attaching with Go, bpf2go, and a Makefile

Generate Go bindings first so your user program can reference maps and the XDP routine directly. Add a small gen.go with a //go:generate directive, then run go generate. That produces endianness-specific files like drop_bpfel.go and drop_bpfeb.go. Commit them so builds stay repeatable across systems.

In the application, call the generated loader, for example loadDropObjects, which loads the XDP code and map definitions into the kernel. Convert a dotted address to a big-endian uint32 before writing it to the map. Use key 0 to set the blocked entry. Attach via link.AttachXDP, passing the interface index. Handle SIGINT and SIGTERM so the link closes and the interface detaches cleanly on exit.

Read traces with bpftool prog tracelog piped to confirm behavior during tests on your system. You will test with ping and a tiny HTTP server, and trace decision logs with bpftool so debugging stays fast and reliable. Wrap the steps in a Makefile with generate, build, and run targets that accept BLOCKED_IP and INTERFACE variables. For the pinned-object path in this article, though, bpftool plus ip link is the faster loop. If your goal is defense at scale, the same building blocks extend to stopping DDoS traffic with eBPF.

The official syntax for both commands lives in the ip-link manual page and the bpftool-prog manual page. Read them once; they settle most argument-order questions.

FAQ

Can I attach an XDP program without pinning it at all?

Yes, use the object form: ip link set dev eth0 xdp object xdp_pass.o section xdp. That makes ip load the ELF and attach in one shot, no bpffs pin. Pinning matters when you want the program to outlive the loader process or you want bpftool to manage the load and verification separately from the attach.

What errno usually means the driver rejected native XDP?

Look for EOPNOTSUPP in the strace output or a driver message in dmesg. It means the NIC driver has no native XDP support, so plain xdp will fall back to generic and xdpdrv will fail outright. Either test on a driver that supports it or accept generic mode for functional testing only.

Does the pin survive a reboot?

No. bpffs lives in memory, so everything under /sys/fs/bpf is gone after a reboot. If you need the program back automatically, load and attach it from a systemd unit at boot rather than expecting the pin to persist.

How do I see which program id is attached to an interface right now?

Run ip link show dev eth0 and read the prog/xdp id N field, then bpftool prog show id N for the full detail. That maps the interface back to a specific loaded program, which is how you confirm the thing dropping packets is the thing you meant to attach.

Why does a second load to the same path fail?

Because the pin already exists. bpffs will not overwrite a pin, so a leftover file from a previous run gives you File exists. Remove the stale pin with rm /sys/fs/bpf/xdp_pass after detaching, then load again. A crashed loader that never cleaned up is the usual source.