← Back to list

The Ultimate Guide to AF_XDP: High Performance Networking in Rust

Learn how to build lightning fast packet processors using Linux’s AF_XDP technology and Rust with complete working code examples

Shradhesh Jodawat · 2026-01-27 09:00 · 6 claps · 12.6 min read
#rust #linux #networking #xdp #performance-engineering
Open on Medium ↗
Wiki topics: 🔓 · Open Source

The Ultimate Guide to AF_XDP: High Performance Networking in Rust

Learn how to build lightning fast packet processors using Linux’s AF_XDP technology and Rust with complete working code examples

Imagine processing millions of packets per second with near bare metal performance while still benefiting from the safety and productivity of the Linux kernel. That’s exactly what AF_XDP (Address Family XDP) delivers.

In this comprehensive guide, I’ll walk you through building a production-ready AF_XDP application in Rust from scratch. Whether you’re building high frequency trading systems, DDoS mitigation tools, or custom packet processors, this guide will give you everything you need.

What You’ll Learn:

  • What AF_XDP is and why it matters
  • The complete architecture of AF_XDP sockets
  • How to implement AF_XDP in Rust without any libraries
  • Real-world code examples with detailed explanations
  • Common pitfalls and how to avoid them

Prerequisites:

  • Basic Rust knowledge
  • Linux system (kernel 4.18+)
  • Root/sudo access
  • A network interface for testing

In this guide, I’ll show you exactly how to build an AF_XDP application in Rust from scratch. No hand waving, no “figure it out yourself” just complete, working code with detailed explanations.

📚 Table of Contents Part 1: Understanding AF_XDP Part 2: Architecture Deep Dive Part 3: Setting Up Rust Project Part 4: The XDP Program Part 5: The Rust Implementation Part 6: Testing Your Code Part 7: Advanced Topics (Resources)

Part 1: Understanding AF_XDP

What is AF_XDP?

AF_XDP is a Linux socket type that provides a fast path for packet processing by bypassing most of the kernel's network stack. Unlike complete kernel bypass solutions like DPDK, AF_XDP cooperates with the kernel while delivering exceptional performance.

Key Advantages:

  • Performance: Process millions of packets per second
  • Selective Bypass: Choose which traffic to accelerate
  • Kernel Integration: Works with existing network drivers
  • Zero-Copy: Direct DMA access (when supported)
  • Resource Efficiency: Lower CPU usage than traditional sockets

How AF_XDP Works: The Big Picture

┌─────────────────────────────────────────────┐
│          User Space Application             │
│    (Your Rust Program)                      │
│  ┌─────────────┐      ┌──────────────┐      │
│  │  Fill Ring  │      │   RX Ring    │      │
│  │ (Producer)  │      │ (Consumer)   │      │
│  └─────────────┘      └──────────────┘      │
│         ↓                      ↑            │
│  ┌──────────────────────────────────────┐   │
│  │         UMEM (Packet Memory)         │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
                    ↕ (mmap)
┌─────────────────────────────────────────────┐
│              Kernel Space                   │
│  ┌──────────────────────────────────────┐   │
│  │        XDP Program (eBPF)            │   │
│  │    (Redirects to XSKMAP)             │   │
│  └──────────────────────────────────────┘   │
│                    ↓                        │
│  ┌──────────────────────────────────────┐   │
│  │         Network Driver               │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
                    ↕
              Network Interface

The Four Ring Buffers

AF_XDP uses four Single Producer/Single Consumer (SPSC) ring buffers:

  1. Fill Ring: Your app tells the kernel which UMEM frames are available for receiving packets
  2. RX Ring: The kernel tells your app where received packets are in UMEM
  3. TX Ring: Your app tells the kernel which packets to transmit
  4. Completion Ring: The kernel tells your app which transmitted packets are complete

For receive only applications (like we’re building), we only need the Fill Ring and RX Ring.

Part 2: Architecture Deep Dive

UMEM: The Shared Memory Region

UMEM (User Memory) is the heart of AF_XDP. It’s a pre allocated memory region divided into equal-sized frames (chunks) that both the kernel and your application can access.

Key Characteristics:

  • Pre-allocated: No per-packet memory allocation overhead
  • Frame-based: Divided into fixed-size chunks (typically 2048 or 4096 bytes)
  • Shared: Accessible from both userspace and kernel
  • Zero-copy capable: With driver support, NIC writes directly to UMEM via DMA
// UMEM Configuration
const FRAME_SIZE: usize = 2048;      // Each frame is 2KB
const NUM_FRAMES: usize = 4096;      // 4096 frames
const UMEM_SIZE: usize = FRAME_SIZE * NUM_FRAMES;  // Total: 8MB

The XSK Map: Connecting XDP to Your Socket

The XDP program uses a special BPF map called XSKMAP to redirect packets to your AF_XDP socket. This map is created by your XDP program and is typically pinned to the filesystem at /sys/fs/bpf/xsks_map.

How it works:

  1. XDP program loads and creates an XSKMAP
  2. Your userspace program opens this map
  3. You insert your socket FD into the map at the queue ID index
  4. XDP program redirects packets to your socket using this map

Part 3: Setting Up Your Rust Project

Project Structure

af_xdp/
├── Cargo.toml
├── Cargo.lock
├── src/
│   └── main.rs
└── README.md

Cargo.toml

[package]
name = "af_xdp"
version = "0.1.0"
edition = "2021"

[dependencies]
libc = "0.2.180"

Why so minimal? We’re using only libc to call system functions directly. This gives us complete control and understanding of what's happening under the hood. In production, you might want to use libraries like libbpf-rs or xdp-rs, but for learning, raw syscalls are invaluable.

Part 4: The XDP Program (Kernel Side)

Before we dive into Rust, we need an XDP program to redirect packets to our socket. Here’s a minimal XDP program:

XDP.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

// Map to store AF_XDP socket file descriptors
struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
    __uint(max_entries, 64);
} xsks_map SEC(".maps");
SEC("xdp")
int xdp_sock_prog(struct xdp_md *ctx) {
    // Get the RX queue index
    __u32 index = ctx->rx_queue_index;

    // Redirect packet to the AF_XDP socket at this queue
    return bpf_redirect_map(&xsks_map, index, 0);
}
char _license[] SEC("license") = "GPL";

What’s happening:

  • We define an XSKMAP to hold socket file descriptors
  • The xdp_sock_prog function runs for every incoming packet
  • It redirects packets to the socket mapped to the current RX queue

you can find full XDP code: link

Compiling the XDP Program

# Install required tools
sudo apt-get install clang llvm libelf-dev

# Compile XDP program
clang -O2 -target bpf -c xdp.c -o xdp.o

# Load and pin the program to BPF filesystem
sudo ip link set dev veth0 xdp obj xdp.o sec xdp

sudo bpftool map pin name xsks_map /sys/fs/bpf/xsks_map
# The XSKMAP is now accessible at /sys/fs/bpf/xsks_map

Part 5: The Rust Implementation

Now for the main event! Let’s build our AF_XDP receiver in Rust.

Constants and Structures

#![allow(non_camel_case_types)]
use libc::*;
use std::{ffi::CString, mem, ptr, os::raw::c_void, os::fd::RawFd};

// Configuration constants
const FRAME_SIZE: usize = 2048;           // Each frame is 2KB
const NUM_FRAMES: usize = 4096;           // Total frames in UMEM
const UMEM_SIZE: usize = FRAME_SIZE * NUM_FRAMES;
const IFNAME: &str = "veth0";             // Network interface name
const QUEUE_ID: u32 = 0;                  // RX queue to bind to

// BPF syscall constants
const SYS_BPF: i64 = 321;                 // x86_64 syscall number
const BPF_OBJ_GET: u32 = 7;               // Open pinned BPF object
const BPF_MAP_UPDATE_ELEM: u32 = 2;       // Update map element

// XDP socket options (from kernel headers)
const XDP_MMAP_OFFSETS: i32 = 1;
const XDP_RX_RING: i32 = 2;
const XDP_TX_RING: i32 = 3;
const XDP_UMEM_REG: i32 = 4;
const XDP_UMEM_FILL_RING: i32 = 5;
const XDP_UMEM_COMPLETION_RING: i32 = 6;

// AF_XDP socket family and level
const AF_XDP: i32 = 44;
const SOL_XDP: i32 = 283;

// mmap page offset values for ring mapping
const XDP_PGOFF_RX_RING: i64 = 0;
const XDP_UMEM_PGOFF_FILL_RING: i64 = 0x1_0000_0000_u64 as i64;

Data Structures

// UMEM registration structure
#[repr(C)]
struct xdp_umem_reg {
    addr: u64,              // UMEM base address
    len: u64,               // UMEM size
    chunk_size: u32,        // Size of each frame
    headroom: u32,          // Reserved space at frame start
    flags: u32,             // Configuration flags
    tx_metadata_len: u32,   // TX metadata length
}

// Packet descriptor in ring buffers
#[repr(C)]
struct xdp_desc {
    addr: u64,      // Frame offset in UMEM
    len: u32,       // Packet length
    options: u32,   // Descriptor options
}

// Ring buffer offset structure
#[repr(C)]
struct xdp_ring_offset {
    producer: u64,  // Producer index offset
    consumer: u64,  // Consumer index offset
    desc: u64,      // Descriptor ring offset
    flags: u64,     // Flags offset
}

// Complete mmap offsets for all rings
#[repr(C)]
struct xdp_mmap_offsets {
    rx: xdp_ring_offset,  // RX ring offsets
    tx: xdp_ring_offset,  // TX ring offsets
    fr: xdp_ring_offset,  // Fill ring offsets
    cr: xdp_ring_offset,  // Completion ring offsets
}

// Socket address for AF_XDP
#[repr(C)]
struct sockaddr_xdp {
    sxdp_family: u16,         // AF_XDP
    sxdp_flags: u16,          // Bind flags
    sxdp_ifindex: u32,        // Interface index
    sxdp_queue_id: u32,       // Queue ID
    sxdp_shared_umem_fd: u32, // For shared UMEM
}

Ring Buffer Abstractions

rust

// RX Ring structure
#[repr(C)]
struct Ring {
    producer: *mut u64,     // Kernel writes here
    consumer: *mut u64,     // We read/write here
    desc: *mut xdp_desc,    // Descriptor array
    mask: u64,              // Ring size mask for wrapping
}

// Fill Queue structure
#[repr(C)]
struct FillQueue {
    producer: *mut u64,     // We write here
    consumer: *mut u64,     // Kernel reads here
    ring: *mut u64,         // Array of frame addresses
    mask: u64,              // Ring size mask
}

Ring Operations

These functions handle the lock free producer-consumer operations:

// Push a frame address to the fill queue
unsafe fn fq_push(fq: &FillQueue, addr: u64) {
    unsafe {
        // Read current producer index
        let prod = ptr::read_volatile(fq.producer);

        // Calculate ring index with wrapping
        let idx = (prod & fq.mask) as usize;

        // Write frame address to ring
        ptr::write_volatile(fq.ring.add(idx), addr);

        // Update producer index (atomic-like with volatile)
        ptr::write_volatile(fq.producer, prod + 1);
    }
}

// Pop a received packet descriptor from RX ring
unsafe fn rx_pop(rx: &Ring) -> Option<xdp_desc> {
    unsafe {
        // Read indices
        let cons = ptr::read_volatile(rx.consumer);
        let prod = ptr::read_volatile(rx.producer);

        // Check if ring is empty
        if cons == prod {
            return None;
        }

        // Calculate ring index
        let idx = (cons & rx.mask) as usize;

        // Read descriptor
        let desc = ptr::read_volatile(rx.desc.add(idx));

        // Update consumer index
        ptr::write_volatile(rx.consumer, cons + 1);

        Some(desc)
    }
}

Why volatile operations?

  • The kernel also accesses these memory locations
  • Volatile ensures the compiler doesn’t optimize away our reads/writes
  • Note: volatile does NOT provide atomics-level memory ordering
  • This works because producer/consumer indices ensure we never access the same slot simultaneously
  • The ring buffer design prevents data races, not the volatile keyword

💡Checkpoint: You’ve now implemented the core AF_XDP logic! Take a break, grab coffee, and let this sink in

A note on memory ordering:

The volatile operations here prevent compiler optimizations but do NOT provide 
atomic memory ordering guarantees. This code works because:

1. The ring buffer is Single Producer Single Consumer (SPSC)
2. Producer only writes to producer index
3. Consumer only writes to consumer index  
4. We never access the same descriptor slot simultaneously
5. The indices themselves coordinate access

If you needed true atomic ordering (e.g., for MPSC), you'd use 
`std::sync::atomic::AtomicU64` with appropriate ordering like `Acquire/Release`.

For learning purposes, this raw implementation shows the mechanics, but 
production code should use proper atomics or existing AF_XDP libraries

Part 6: The Main Implementation

Step 1: Allocate UMEM

fn main() {
    unsafe {
        // Allocate UMEM with mmap
        let umem = mmap(
            ptr::null_mut(),                    // Let kernel choose address
            UMEM_SIZE,                          // Size
            PROT_READ | PROT_WRITE,            // Readable and writable
            MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,  // Private, anonymous, pre-fault
            -1,                                 // No file backing
            0,                                  // No offset
        );
    if umem == MAP_FAILED {
            panic!("UMEM mmap failed");
        }

    println!("✓ Allocated {}MB UMEM", UMEM_SIZE / 1024 / 1024);

Key points:

  • MAP_POPULATE: Pre faults pages to avoid page faults during packet processing
  • MAP_ANONYMOUS: Not backed by a file, pure memory
  • Total allocation: 8MB (4096 frames × 2KB each)

Step 2: Create AF_XDP Socket

        let fd = socket(AF_XDP, SOCK_RAW, 0);
        if fd < 0 {
            let err = *__errno_location();
            panic!("AF_XDP socket failed: errno={}", err);
        }

        println!("✓ Created AF_XDP socket (fd={})", fd);

Step 3: Register UMEM

// Register UMEM with kernel
        let reg = xdp_umem_reg {
            addr: umem as u64,
            len: UMEM_SIZE as u64,
            chunk_size: FRAME_SIZE as u32,
            headroom: 0,              // No headroom needed
            flags: 0,                 // Default flags
            tx_metadata_len: 0,       // No TX metadata
        };
        if setsockopt(
            fd,
            SOL_XDP,
            XDP_UMEM_REG,
            &reg as *const _ as *const c_void,
            mem::size_of::<xdp_umem_reg>() as socklen_t,
        ) != 0 {
            panic!("XDP_UMEM_REG failed");
        }

        println!("✓ Registered UMEM ({}B frames)", FRAME_SIZE);

What’s happening:

  • We tell the kernel about our UMEM region
  • Kernel validates and registers it
  • Frame size must be aligned (typically 2048 or 4096)

Step 4: Configure Ring Sizes

// Configure ring buffer sizes
        let rx_entries: u32 = 1024;
        let fq_entries: u32 = 1024;
        // Setup RX ring
        if setsockopt(
            fd,
            SOL_XDP,
            XDP_RX_RING,
            &rx_entries as *const _ as *const c_void,
            mem::size_of::<u32>() as socklen_t,
        ) != 0 {
            panic!("XDP_RX_RING setsockopt failed");
        }
  // Setup Fill ring (other rings omitted for RX only)
        if setsockopt(
            fd,
            SOL_XDP,
            XDP_UMEM_FILL_RING,
            &fq_entries as *const _ as *const c_void,
            mem::size_of::<u32>() as socklen_t,
        ) != 0 {
            panic!("XDP_UMEM_FILL_RING setsockopt failed");
        }

        println!("✓ Configured rings (RX={}, Fill={})", rx_entries, fq_entries);

Ring size considerations:

  • Must be powers of 2 for efficient masking
  • Larger rings = more buffering but more memory
  • 1024 is a good balance for most applications

Step 5: Get Memory Map Offsets

// Get kernel-provided mmap offsets
        let mut offsets: xdp_mmap_offsets = mem::zeroed();
        let mut optlen = mem::size_of::<xdp_mmap_offsets>() as socklen_t;
        if getsockopt(
            fd,
            SOL_XDP,
            XDP_MMAP_OFFSETS,
            &mut offsets as *mut _ as *mut c_void,
            &mut optlen as *mut _,
        ) != 0 {
            panic!("getsockopt XDP_MMAP_OFFSETS failed");
        }
 // Calculate proper mmap sizes
        let rx_map_size = (offsets.rx.desc as usize) 
            + (rx_entries as usize * mem::size_of::<xdp_desc>());
        let fq_map_size = (offsets.fr.desc as usize) 
            + (fq_entries as usize * mem::size_of::<u64>());

Why get offsets?

  • The kernel decides the exact memory layout
  • We need these to correctly map and access ring buffers
  • Layout includes metadata (producer/consumer indices) + descriptors

Step 6: Memory Map Ring Buffers

// Map RX ring
        let rx_map = mmap(
            ptr::null_mut(),
            rx_map_size,
            PROT_READ | PROT_WRITE,
            MAP_SHARED | MAP_POPULATE,  // Shared with kernel!
            fd,
            XDP_PGOFF_RX_RING,
        );
        if rx_map == MAP_FAILED {
            panic!("mmap rx ring failed");
        }
// Map Fill ring
        let fq_map = mmap(
            ptr::null_mut(),
            fq_map_size,
            PROT_READ | PROT_WRITE,
            MAP_SHARED | MAP_POPULATE,
            fd,
            XDP_UMEM_PGOFF_FILL_RING,
        );
        if fq_map == MAP_FAILED {
            panic!("mmap fill ring failed");
        }

        println!("✓ Mapped ring buffers to userspace");

Critical: MAP_SHARED makes this memory visible to the kernel. This is how we communicate with the kernel without syscalls!

Step 7: Bind to Network Interface

// Get interface index
        let ifname = CString::new(IFNAME).unwrap();
        let ifindex = if_nametoindex(ifname.as_ptr());
        if ifindex == 0 {
            panic!("Interface {} not found", IFNAME);
        }
// Bind socket to interface and queue
        let sxdp = sockaddr_xdp {
            sxdp_family: AF_XDP as u16,
            sxdp_flags: 0,              // Let kernel choose copy/zerocopy
            sxdp_ifindex: ifindex,
            sxdp_queue_id: QUEUE_ID,
            sxdp_shared_umem_fd: 0,     // No UMEM sharing
        };
        if bind(
            fd,
            &sxdp as *const _ as *const sockaddr,
            mem::size_of::<sockaddr_xdp>() as socklen_t,
        ) != 0 {
            panic!("bind failed");
        }

        println!("✓ Bound to {}:{}", IFNAME, QUEUE_ID);

Binding details:

  • Associates socket with specific network interface and queue
  • Queue ID must match what XDP program redirects to
  • Kernel validates all parameters

Step 8: Setup Ring Pointers

// Create RX ring structure
        let rx = Ring {
            producer: (rx_map as *mut u8)
                .add(offsets.rx.producer as usize) as *mut u64,
            consumer: (rx_map as *mut u8)
                .add(offsets.rx.consumer as usize) as *mut u64,
            desc: (rx_map as *mut u8)
                .add(offsets.rx.desc as usize) as *mut xdp_desc,
            mask: (rx_entries - 1) as u64,
        };
// Create Fill queue structure
        let fq = FillQueue {
            producer: (fq_map as *mut u8)
                .add(offsets.fr.producer as usize) as *mut u64,
            consumer: (fq_map as *mut u8)
                .add(offsets.fr.consumer as usize) as *mut u64,
            ring: (fq_map as *mut u8)
                .add(offsets.fr.desc as usize) as *mut u64,
            mask: (fq_entries - 1) as u64,
        };

Pointer arithmetic:

  • We’re calculating exact memory locations based on kernel provided offsets
  • add(n) on *mut u8 moves forward by n bytes
  • For other types, use byte_add() to advance by bytes instead of elements
  • These pointers let us access shared memory efficiently

Step 9: Pre-fill Frame Buffers

// Pre fill all frames into fill ring
        for i in 0..NUM_FRAMES {
            let addr = (i * FRAME_SIZE) as u64;
            fq_push(&fq, addr);
        }
// Kick the socket to notify kernel
        let ret = recvfrom(
            fd, 
            ptr::null_mut(), 
            0, 
            MSG_DONTWAIT, 
            ptr::null_mut(), 
            ptr::null_mut()
        );

 // EAGAIN is expected and fine here
        if ret < 0 {
            let err = *__errno_location();
            if err != EAGAIN && err != EWOULDBLOCK {
                eprintln!("Warning: recvfrom kick failed: errno={}", err);
            }
        }

        println!("✓ Pre-filled {} frames", NUM_FRAMES);

Why pre-fill?

  • Kernel needs frames to receive packets into
  • We’re saying “here are 4096 frames you can use”
  • The recvfrom call notifies the kernel to start using them

Step 10: Update XSK Map

// BPF attribute structures for syscalls
        #[repr(C)]
        struct bpf_attr_obj_get {
            pathname: u64,
            bpf_fd: u32,
            file_flags: u32,
        }
        #[repr(C)]
        struct bpf_attr_map_update_elem {
            map_fd: u32,
            pad: u32,
            key: u64,
            value: u64,
            flags: u64,
        }
// Open the pinned xsks_map
        let map_path = CString::new("/sys/fs/bpf/xsks_map").unwrap();
        let obj_get = bpf_attr_obj_get {
            pathname: map_path.as_ptr() as u64,
            bpf_fd: 0,
            file_flags: 0,
        };
        let map_fd = syscall(
            SYS_BPF,
            BPF_OBJ_GET,
            &obj_get as *const _,
            mem::size_of::<bpf_attr_obj_get>(),
        ) as RawFd;
        if map_fd < 0 {
            panic!("Failed to open xsks_map");
        }
// Update map: key=queue_id, value=socket_fd
        let key_local: u32 = QUEUE_ID;
        let value_local: u32 = fd as u32;
        let upd = bpf_attr_map_update_elem {
            map_fd: map_fd as u32,
            pad: 0,
            key: &key_local as *const _ as u64,
            value: &value_local as *const _ as u64,
            flags: 0u64,
        };
        if syscall(
            SYS_BPF,
            BPF_MAP_UPDATE_ELEM,
            &upd as *const _,
            mem::size_of::<bpf_attr_map_update_elem>(),
        ) != 0 {
            panic!("Failed to update xsks_map");
        }
        close(map_fd);

        println!("✓ Updated XSK map (queue {} -> socket fd {})", QUEUE_ID, fd);

This is crucial:

  • Opens the BPF map created by our XDP program
  • Inserts our socket FD at the queue ID index
  • XDP program can now redirect packets to our socket

Step 11: Receive Loop

        println!("\n🚀 AF_XDP RX ready on {}:{}", IFNAME, QUEUE_ID);
        let mut pkt_count = 0;

        loop {
            if let Some(desc) = rx_pop(&rx) {
                // Calculate packet location in UMEM
                let pkt_ptr = (umem as *const u8).add(desc.addr as usize);
                let pkt = std::slice::from_raw_parts(pkt_ptr, desc.len as usize);

                pkt_count += 1;
                println!("[{}] Received packet: {} bytes", pkt_count, pkt.len());

                // Optional: Parse Ethernet header
                if pkt.len() >= 14 {
                    println!("  Ethernet: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} -> {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
                        pkt[6], pkt[7], pkt[8], pkt[9], pkt[10], pkt[11],
                        pkt[0], pkt[1], pkt[2], pkt[3], pkt[4], pkt[5]);
                }

                // Return frame to fill ring for reuse
                fq_push(&fq, desc.addr);
            } else {
                // No packets available, poll for new ones
                let mut pfd = pollfd {
                    fd,
                    events: POLLIN,
                    revents: 0,
                };
                poll(&mut pfd as *mut pollfd, 1, 100);  // 100ms timeout
            }
        }
    }
}

Receive loop breakdown:

  1. Try to pop a descriptor from RX ring
  2. If we got one, the packet is at desc.addr in UMEM
  3. Process the packet (here we just print it)
  4. Critical: Return the frame to the fill queue for reuse
  5. If no packets, use poll() to wait efficiently]
Traditional Socket         AF_XDP Socket
─────────────────         ─────────────

Application               Application
    ↕ syscall                ↕ mmap
TCP/IP Stack             AF_XDP Socket
    ↕                        ↕ DMA
Network Driver           Network Driver
    ↕                        ↕
   NIC                      NIC

Latency: ~250µs          Latency: ~10µs
Throughput: 100Kpps      Throughput: 10Mpps

Part 7: Testing Your Implementation

Setting Up a Test Environment

For testing, we’ll create a virtual network interface pair:

# Create veth pair
sudo ip link add veth0 type veth peer name veth1

# Bring up both interfaces
sudo ip link set veth0 up
sudo ip link set veth1 up

# Assign IP addresses
sudo ip addr add 192.168.100.1/24 dev veth0
sudo ip addr add 192.168.100.2/24 dev veth1

# Reduce to single queue (important!)
sudo ethtool -L veth0 combined 1

Why single queue?

  • By default, packets might be distributed across multiple queues
  • We’re only binding to queue 0
  • Single queue ensures all packets go to our socket

Loading the XDP Program

# Compile XDP program
clang -O2 -target bpf -c xdp/xdp_redirect.c -o xdp_redirect.o

# Load and attach to veth0
sudo ip link set dev veth0 xdp obj xdp_redirect.o sec xdp
# Verify XDP program is loaded
sudo ip link show veth0

You should see “xdp” in the output.

Running Your Application

# Build Rust application
cargo build --release

# Run with sudo (required for AF_XDP)
sudo ./target/release/af_xdp

Generating Test Traffic

In another terminal:

# Send UDP packets
# use ping
ping -c 5 192.168.100.1
# Or use hping3 for packet generation
sudo hping3 -c 5 --udp -p 12345 192.168.100.1

🎯 Challenge: Try extending this code to support packet transmission. Share your results in the comments!

Resources

Questions? Drop them in the comments! I’m happy to help troubleshoot or explain any part in more detail.

Found this helpful? Give it a clap and follow for more systems programming content!

Rust #AF_XDP #Linux #Networking #SystemsProgramming #HighPerformance #eBPF #XDP #KernelBypass #PacketProcessing


메타데이터
post_id
0a5ca9e1377a
slug
the-ultimate-guide-to-af-xdp-high-performance-networking-in-rust-0a5ca9e1377a
url
https://medium.com/@shradhesh71/the-ultimate-guide-to-af-xdp-high-performance-networking-in-rust-0a5ca9e1377a
canonical_url
https://medium.com/@shradhesh71/the-ultimate-guide-to-af-xdp-high-performance-networking-in-rust-0a5ca9e1377a
author_url
https://medium.com/@shradhesh71
status
ok
fetched_at
2026-06-14 11:28:49