Skip to content
← ENGINEERING NOTES
NETWORKING7 min read

XDP: Drop Packets Before Linux Notices

XDP and eBPF explained: write a small C program that drops UDP traffic at the earliest hook, load it with ip link, and compare it fairly against nftables.

BY ASHLIN DARIUS GOVINDASAMY

The cheapest packet to handle is the one you throw away before the kernel has bothered to wrap it in anything. That is the pitch for XDP, the eXpress Data Path: a hook so early in the receive path that your eBPF program sees the raw frame, makes a one-word decision, and moves on. This post builds a tiny XDP program that drops UDP traffic for one port, loads it with plain iproute2, and then shows how to compare it honestly against nftables, because the answer is not simply that XDP is faster.

What XDP actually is

An XDP program is an eBPF program attached to a network interface. For every received frame it runs with a context, struct xdp_md, and returns an action. The XDP packet-parsing tutorial from the xdp-project lists the context fields as data, data_end, data_meta, ingress_ifindex and rx_queue_index, with the first three stored as 32-bit integers that you cast to pointers before use.

  • XDP_PASS: hand the packet to the normal kernel network stack.
  • XDP_DROP: discard it silently.
  • XDP_ABORTED: drop it and fire the xdp:xdp_exception tracepoint, which is handy for debugging.
  • XDP_TX: send it back out of the interface it arrived on.
  • XDP_REDIRECT: send it out of another interface (or to an AF_XDP socket); a helper has to set the target first.

The kernel's AF_XDP documentation also describes two attach flavours that matter for performance. XDP_DRV uses the driver's own XDP support. XDP_SKB relies on the generic XDP support, which is used when the driver lacks XDP support or when you ask for it explicitly. In iproute2 terms you will see these as xdpdrv and xdpgeneric. Generic mode runs later, after an skb exists, so it is perfect for learning and testing but gives up much of the early-hook advantage.

A program that drops one UDP port

Here is the whole thing. It parses Ethernet, IPv4 and UDP, bounds-checking each step, and drops datagrams addressed to port 9999. Everything else passes. Note the variable-length IPv4 header: the UDP header starts at ihl times four bytes past the IP header, and that pointer needs its own check.

c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define BLOCKED_PORT 9999

SEC("xdp")
int xdp_drop_udp(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;
    if (ip->protocol != IPPROTO_UDP)
        return XDP_PASS;

    struct udphdr *udp = (void *)ip + ip->ihl * 4;
    if ((void *)(udp + 1) > data_end)
        return XDP_PASS;

    if (udp->dest == bpf_htons(BLOCKED_PORT))
        return XDP_DROP;
    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";
xdp_drop.c

Two details worth knowing. Header fields are in network byte order, which is why the port is compared against bpf_htons rather than a bare constant. And the license string matters: some BPF helpers are only available to GPL-compatible programs.

QUICK CHECK

Why does the program compare pointers against data_end before reading each header?

Build, load, test in a throwaway namespace

Do not experiment on your real uplink. A veth pair between two network namespaces gives you a safe sandbox, the same trick as in our network namespaces post. You need clang, libbpf headers (libbpf-dev on Debian and Ubuntu) and iproute2. On multiarch Debian-style systems the asm headers live under /usr/include/<arch>-linux-gnu, hence the extra include path.

bash
clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o \
      -I/usr/include/$(uname -m)-linux-gnu

sudo ip netns add rx
sudo ip link add veth-tx type veth peer name veth-rx
sudo ip link set veth-rx netns rx
sudo ip addr add 10.9.0.1/24 dev veth-tx && sudo ip link set veth-tx up
sudo ip -n rx addr add 10.9.0.2/24 dev veth-rx
sudo ip -n rx link set veth-rx up

# attach (the xdp-project tutorial uses this exact ip link form)
sudo ip -n rx link set dev veth-rx xdpgeneric obj xdp_drop.o sec xdp
sudo ip -n rx link show dev veth-rx   # look for the xdpgeneric line

# detach when done
sudo ip -n rx link set dev veth-rx xdpgeneric off
Compile, then attach in generic mode inside a namespace

Now prove it works. Listen in the namespace on the blocked port and an allowed port, then send one datagram to each from the host. Only the allowed one should arrive.

bash
sudo ip netns exec rx timeout 5 nc -ul 9999 &
sudo ip netns exec rx timeout 5 nc -ul 9998 &
sleep 1
echo "blocked" | nc -u -w1 10.9.0.2 9999
echo "allowed" | nc -u -w1 10.9.0.2 9998
wait
Expect output only for port 9998

XDP versus nftables: compare fairly

nftables can also drop early. A netdev-family table with an ingress hook sees packets on a specific device before the IP layer, so it is the right opponent, not the usual input chain. Here is the equivalent rule on the same veth.

bash
sudo ip netns exec rx nft -f - <<'EOF'
table netdev early {
  chain ingress {
    type filter hook ingress device "veth-rx" priority -500; policy accept;
    udp dport 9999 drop
  }
}
EOF
The nftables equivalent, in the netdev ingress hook

I am deliberately not quoting throughput numbers here, because they depend entirely on your NIC, driver, CPU and packet size, and a veth pair in generic mode says almost nothing about a physical NIC in native mode. Run the comparison on your own hardware with a packet generator, such as the kernel's pktgen or trafgen from netsniff-ng, and hold everything else constant.

  • Test one variant at a time: no filter, nftables netdev ingress, XDP generic, XDP native.
  • Use the same packet size, same rate and same receiving CPU affinity for every run.
  • Watch CPU time spent in softirq (mpstat -P ALL 1) as well as dropped packets per second, since the real win is usually CPU headroom.
  • Repeat each run several times; first runs are noisy.
›Why a missing win is possible

In generic mode the skb has already been allocated before your program runs, so most of the cost XDP is famous for avoiding has already been paid. If you see no gap between xdpgeneric and nftables, that is expected. Driver (native) mode is where early dropping can pay off, and it requires driver support. Check the driver documentation and your attach command's error message rather than assuming.

Where XDP breaks down

  • Stateful filtering is hard. nftables gives you connection tracking, sets and NAT; in XDP you build state yourself with BPF maps.
  • No automatic reassembly. Fragmented packets, IPv6 extension headers and nested VLAN tags all need explicit parsing, and the tutorial's VLAN lesson handles tags with an unrolled loop of vlan_hdr structs.
  • Bounded programs only. Helpers must be always-inline because eBPF function calls are limited, and the verifier must be able to prove termination.
  • Driver support varies. Where there is no native support you fall back to generic mode and lose most of the benefit.
  • One slot per interface with ip link, which is the reason xdp-loader exists.

QUICK CHECK

Which return code silently discards a packet without raising a tracepoint?

Takeaway

XDP is a scalpel: ideal for stateless, high-volume decisions like dropping a known-bad port, steering traffic with XDP_REDIRECT, or feeding AF_XDP sockets. nftables is the Swiss army knife you keep for everything stateful. Learn XDP in a namespace with generic mode, measure on real hardware in native mode, and let your own numbers decide where each tool lives in your network.

SOURCES

  1. XDP tutorial: packet parsing lesson (README) — xdp-project
  2. XDP tutorial: basic01 xdp-pass lesson (ip link and xdp-loader commands) — xdp-project
  3. AF_XDP documentation (XDP_DRV, XDP_SKB, XDP_REDIRECT) — Linux kernel documentation

Frequently asked questions

What is XDP in Linux?

XDP (eXpress Data Path) lets an eBPF program run on each received packet at a very early point in the network receive path and return an action such as XDP_PASS, XDP_DROP or XDP_REDIRECT.

Is XDP faster than nftables?

It can be, especially in native driver mode where it avoids work the stack would otherwise do, but it depends on your hardware and traffic. Generic mode shows little advantage, so benchmark both on your own setup.

What is the difference between xdpgeneric and xdpdrv?

xdpgeneric uses the generic, skb-based XDP support and works on any driver, while xdpdrv uses the driver's native XDP support, which performs better but needs driver support.

Why does my XDP program fail to load?

The most common cause is the verifier rejecting a packet read that is not preceded by a bounds check against data_end. Check every pointer you dereference.

NEWSLETTER

Get the engineering notes in your inbox.

Kernels, GPUs, networks, AI infrastructure — and the things we broke along the way. No spam, one click to leave.

We store only your email address, to send you new articles. See our privacy policy.

TAGS

XDPeBPFnftablesLinux networkingpacket filteringDDoSclang

RELATED

ENGINEERING NOTES

Things we built.Things we broke.Things we learned.