For decades, high-performance packet processing on Linux—required for Telco 5G user planes, high-frequency trading platforms, and massive DDoS mitigation appliances—relied on kernel bypass frameworks like Intel’s DPDK (Data Plane Development Kit). While DPDK offers incredible speed by mapping network interface (NIC) memory directly into user space, it requires dedicating entire CPU cores exclusively to polling, entirely bypasses the Linux network stack, and demands bespoke drivers.
The modern, native Linux alternative is AF_XDP (Address Family eXpress Data Path), powered by eBPF. AF_XDP provides the performance of DPDK—achieving millions of packets per second (Mpps) per core—while remaining fully integrated with the Linux kernel and utilizing standard network drivers.
This guide explains the architecture of AF_XDP and how to configure a zero-copy socket to dramatically accelerate user-space networking applications.
The Architecture of AF_XDP
Standard Linux networking operates by transferring a packet from the NIC hardware queue into a kernel sk_buff structure, passing it through the complex iptables/Netfilter stack, and finally copying it into user space via a standard socket (AF_INET).
AF_XDP introduces a new socket address family. It operates by attaching an eBPF XDP program directly to the NIC driver (at the lowest possible point in the software stack). The XDP program inspects the incoming packet. If the packet matches a specific rule (e.g., UDP port 53 for a high-speed DNS server), the eBPF program returns the XDP_REDIRECT action.
This action redirects the raw packet directly into an AF_XDP socket’s memory buffer (UMEM), which is mapped directly into the user-space application’s memory space. Crucially, if the NIC driver supports “Zero-Copy” mode, the packet is DMA’d (Direct Memory Access) by the hardware directly into the application’s memory buffer. The CPU performs zero copies.
Prerequisites for Zero-Copy AF_XDP
To achieve the highest performance, three components must align:
- Kernel Version: Linux kernel 4.18+ (AF_XDP introduced), though 5.4+ is recommended for stability and zero-copy enhancements.
- NIC Driver Support: The network card driver must explicitly support XDP zero-copy (
XDP_SETUP_XSK_UMEM). Modern Intel (i40e, ice), Mellanox (mlx5), and Broadcom drivers support this. - libbpf: The user-space application should be written in C/C++ or Rust using the
libbpflibrary to handle the complex ring-buffer mathematics.
Step 1: Setting up the UMEM (User Memory)
The foundation of an AF_XDP socket is the UMEM. This is a contiguous block of memory allocated by your user-space application and registered with the kernel. The kernel and the application share this memory via four lockless ring buffers:
- Fill Ring: Application passes empty memory frames to the kernel.
- Receive (RX) Ring: Kernel passes filled memory frames (containing packets) to the application.
- Transmit (TX) Ring: Application passes filled memory frames to the kernel for sending.
- Completion Ring: Kernel notifies the application that transmission is complete and the frame is empty.
In your C application, using libbpf, you allocate the memory and create the UMEM:
#include <bpf/xsk.h>
// Allocate memory (e.g., via posix_memalign)
void *umem_area;
posix_memalign(&umem_area, getpagesize(), NUM_FRAMES * FRAME_SIZE);
// Create the UMEM and the Fill/Completion rings
struct xsk_umem *umem;
struct xsk_ring_prod fill_ring;
struct xsk_ring_cons comp_ring;
struct xsk_umem_config umem_cfg = {
.fill_size = RING_SIZE,
.comp_size = RING_SIZE,
.frame_size = FRAME_SIZE,
.frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
};
xsk_umem__create(&umem, umem_area, NUM_FRAMES * FRAME_SIZE, &fill_ring, &comp_ring, &umem_cfg);
Step 2: Creating the AF_XDP Socket (xsk)
Once the UMEM is registered, you bind the actual AF_XDP socket to a specific network interface and a specific hardware queue on that interface.
struct xsk_socket *xsk;
struct xsk_ring_cons rx_ring;
struct xsk_ring_prod tx_ring;
struct xsk_socket_config xsk_cfg = {
.rx_size = RING_SIZE,
.tx_size = RING_SIZE,
.libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
// Enforce Zero-Copy mode
.bind_flags = XDP_USE_NEED_WAKEUP | XDP_ZEROCOPY,
};
xsk_socket__create(&xsk, "eth0", 0, umem, &rx_ring, &tx_ring, &xsk_cfg);
If the NIC does not support zero-copy, xsk_socket__create will fail because we strictly enforced XDP_ZEROCOPY. You can fallback to XDP_COPY (where the kernel copies the packet once into the UMEM), which is slower but still vastly faster than standard sockets.
Step 3: Loading the eBPF XDP Program
The AF_XDP socket is useless without an eBPF program telling the kernel which packets to send to it.
An XDP C program (compiled to eBPF byte code) is attached to the interface (eth0). It uses a BPF map of type BPF_MAP_TYPE_XSKMAP. When a packet arrives, the XDP program executes:
SEC("xdp")
int xdp_redirect_prog(struct xdp_md *ctx) {
// Inspect packet (e.g., check if it's UDP)
// If it matches our criteria, redirect to the AF_XDP socket map
return bpf_redirect_map(&xsk_map, ctx->rx_queue_index, XDP_PASS);
}
If the packet is redirected, it skips the entire Linux network stack and lands directly in the UMEM RX ring. If the program returns XDP_PASS, the packet continues up the normal Linux stack (e.g., for SSH management traffic).
Step 4: Processing Packets in User Space
The user-space application now enters a high-speed polling loop. It checks the RX ring for new packets, processes the payload directly from the memory pointer, and then recycles the memory frame by placing it back into the Fill ring.
uint32_t idx_rx;
if (xsk_ring_cons__peek(&rx_ring, BATCH_SIZE, &idx_rx) > 0) {
// We have a batch of packets
const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx_ring, idx_rx);
// Access the raw packet data directly from memory
void *pkt_data = xsk_umem__get_data(umem_area, desc->addr);
// Process packet...
// Release the ring entry
xsk_ring_cons__release(&rx_ring, 1);
}
Conclusion
AF_XDP represents the maturation of Linux networking. By uniting the programmability of eBPF with the hardware DMA capabilities of modern NICs, developers can construct ultra-low latency, zero-copy user-space applications without abandoning the safety, tooling, and routing capabilities of the Linux kernel.