Skip to content

XDP — Express Data Path

Blog series: Part 9 — XDP-based packet filter · Part 13 — Packet logger with TC and XDP · Part 14 — Firewall with Java & eBPF
Talk: Building a Lightning Fast Firewall with Java & eBPF (JavaZone 2024)
Javadoc: XDPHook Source: XDPHook.java See also: TC Hook · BPF Maps · Global Variables

Java config/log component talking to the XDP kernel program

XDP (eXpress Data Path) is the fastest hook point in the Linux network stack. BPF programs run directly in the network driver, before any SKB allocation, achieving multi-million-packets-per-second rates on commodity hardware.

When to use XDP

  • Line-rate packet filtering (DDoS mitigation, firewalls)
  • Load balancing with direct server return
  • Packet sampling and monitoring
  • Dropping unwanted traffic before it reaches the kernel network stack

Implementing XDPHook

import me.bechberger.ebpf.annotations.bpf.BPF;
import me.bechberger.ebpf.annotations.bpf.BPFFunction;
import me.bechberger.ebpf.bpf.BPFProgram;
import me.bechberger.ebpf.bpf.XDPContext;
import me.bechberger.ebpf.bpf.XDPHook;
import me.bechberger.ebpf.runtime.XdpDefinitions.xdp_action;

@BPF(license = "GPL")
public abstract class MyXDPProg extends BPFProgram implements XDPHook {

    @Override
    @BPFFunction
    public xdp_action xdpHandlePacket(XDPContext ctx) {
        return xdp_action.XDP_PASS;
    }
}

XDPHook requires you to implement xdpHandlePacket. The compiler plugin generates the SEC("xdp") section automatically.

The XDPContext and xdp_md context

The preferred API is XDPContext, which wraps xdp_md with ergonomic helpers:

Method Description
ctx.length() Packet length in bytes
ctx.boundsOk(offset, size) Verifier-safe bounds check
ctx.byteAt(offset) Read one byte (unsigned)
ctx.shortAtNetworkOrder(offset) Read a big-endian short

For raw access via xdp_md, the underlying struct fields are:

Field Type Description
data int Pointer to start of packet data (as u32)
data_end int Pointer to end of packet data (exclusive)
data_meta int Pointer to metadata area (before data)
ingress_ifindex int Interface index the packet arrived on
rx_queue_index int RX queue index

Access packet bytes via raw Ptr<xdp_md> (lower-level, but needed for header parsing):

@BPFFunction
public xdp_action xdpHandlePacket(Ptr<xdp_md> ctx) {
    // Cast data offset to pointer
    Ptr<ethhdr> eth = Ptr.cast(Ptr.of(ctx.val().data));
    // Bounds check required by verifier
    if ((long)(eth + 1) > ctx.val().data_end) {
        return xdp_action.XDP_PASS;
    }
    if (eth.val().h_proto == bpf_htons(ETH_P_IP)) {
        return handleIPv4(ctx, eth);
    }
    return xdp_action.XDP_PASS;
}

Return values

Constant Value Meaning
xdp_action.XDP_ABORTED 0 Drop with error (increments counter)
xdp_action.XDP_DROP 1 Drop silently
xdp_action.XDP_PASS 2 Pass to normal kernel network stack
xdp_action.XDP_TX 3 Hairpin — transmit back out on the same interface
xdp_action.XDP_REDIRECT 4 Redirect to another interface / CPU / socket

Attaching the program

public static void main(String[] args) throws Exception {
    try (MyXDPProg prog = BPFProgram.load(MyXDPProg.class)) {
        // Attach to all non-loopback interfaces that are up
        prog.xdpAttach();

        // Or attach to a specific interface by ifindex (from `ip link`)
        // prog.xdpAttach(ifindex);

        System.out.println("Running. Ctrl-C to stop.");
        Thread.currentThread().join();
    }
    // Program is detached automatically on close()
}

Byte-order helpers

Network headers are big-endian; x86 is little-endian. Use the kernel macros (available as static constants in the generated C):

// In @BPFFunction
short ethProto = bpf_ntohs(eth.val().h_proto);
int dstIp      = bpf_ntohl(ip.val().daddr);

On the Java side use Short.reverseBytes() / Integer.reverseBytes() or ByteBuffer with ByteOrder.BIG_ENDIAN.

Full example — block a specific source IP

This example uses Ptr<xdp_md> (rather than XDPContext) because it needs raw pointer arithmetic to walk Ethernet and IP headers — the pattern that requires bounds checks and Ptr.cast.

@BPF(license = "GPL")
public abstract class BlockIP extends BPFProgram implements XDPHook {

    /** IP to block in network byte order, set from Java. */
    final GlobalVariable<Integer> blockedIp = new GlobalVariable<>(0);

    @Override
    @BPFFunction
    public xdp_action xdpHandlePacket(Ptr<xdp_md> ctx) {
        Ptr<ethhdr> eth = Ptr.cast(Ptr.of(ctx.val().data));
        if ((long)(eth + 1) > ctx.val().data_end) return xdp_action.XDP_PASS;
        if (eth.val().h_proto != bpf_htons(ETH_P_IP)) return xdp_action.XDP_PASS;

        Ptr<iphdr> ip = Ptr.cast(eth + 1);
        if ((long)(ip + 1) > ctx.val().data_end) return xdp_action.XDP_PASS;

        if (ip.val().saddr == blockedIp.get()) {
            return xdp_action.XDP_DROP;
        }
        return xdp_action.XDP_PASS;
    }

    public static void main(String[] args) throws Exception {
        try (BlockIP prog = BPFProgram.load(BlockIP.class)) {
            // Block 192.168.1.100 — store in network byte order
            prog.blockedIp.set(Integer.reverseBytes(0xC0A80164));
            prog.xdpAttach();
            System.out.println("Blocking 192.168.1.100");
            Thread.currentThread().join();
        }
    }
}

Performance tips

  • Prefer native XDP mode (load without the SKB fallback path) when the driver supports it — most modern NICs and virtio do.
  • Process as much as possible inside the BPF program. Passing packets up to the kernel incurs SKB allocation overhead.
  • Use BPFPerCpuArray for counters to avoid inter-CPU synchronisation.
  • Return xdp_action.XDP_DROP as early as possible after the bounds checks.

Benchmark data (JavaZone 2024): Using Cloudflare's published data, XDP reaches ~10 Mpps — roughly 10× faster than iptables in PREROUTING, and even faster with NIC offloading.

Talk: Building a Lightning Fast Firewall with Java & eBPF


Examples


Further reading