nodesight
EngineeringJune 202512 min read

Kernel-Level Packet Capture at Scale: eBPF, XDP, and AF_XDP for Tor Exit Monitoring

The Problem with Userspace Capture

Traditional packet capture stacks — libpcap, DPDK, even PF_RING — impose architectural tradeoffs that make them unsuitable for sustained, high-throughput monitoring at Tor exit relays. Libpcap copies every packet from kernel to userspace. DPDK bypasses the kernel entirely, sacrificing the operating system's networking stack, security primitives, and management tooling. PF_RING offers a middle ground but requires proprietary kernel modules.

None of these approaches were designed for what we needed: real-time, zero-drop ingestion of every packet traversing a Tor exit relay at 10Gbps line rate, running on commodity hardware, without taking the NIC away from the kernel.

Enter XDP: eXpress Data Path

XDP (eXpress Data Path) allows eBPF programs to execute at the earliest possible point in the Linux networking stack — directly in the NIC driver, before the kernel even allocates a socket buffer (sk_buff). This means:

  • No memory allocation overhead — packets are processed in-place in the driver's DMA ring buffer
  • No context switches — the eBPF program runs in softirq context
  • No copies — the program sees the raw frame and can make forwarding decisions immediately

Our XDP program performs three operations in under 200 nanoseconds per packet:

  • Flow key extraction — parses Ethernet, IP, and TCP/UDP headers to extract a 5-tuple flow key
  • Protocol fingerprinting — checks for Tor cell markers (512-byte cells, specific relay command bytes)
  • Ring buffer submission — selected flows are submitted to a BPF ring buffer for userspace processing
  • SEC("xdp")
    

    int nodesight_capture(struct xdp_md *ctx) {

    void *data = (void *)(long)ctx->data;

    void *data_end = (void *)(long)ctx->data_end;

    struct flow_key key;

    if (extract_flow_key(&key, data, data_end) < 0)

    return XDP_PASS;

    if (!is_tor_candidate(&key, data, data_end))

    return XDP_PASS;

    bpf_ringbuf_output(&events, &key, sizeof(key), 0);

    return XDP_PASS;

    }

    AF_XDP: Zero-Copy Userspace Delivery

    For flows that require deep analysis, we use AF_XDP sockets — a mechanism that redirects packets from the NIC's DMA ring buffer directly into a userspace memory region, bypassing the entire kernel networking stack while still allowing the kernel to manage the NIC.

    The key innovation is the UMEM shared memory region: both the kernel and our userspace application see the same memory. No copies. The XDP program redirects selected packets to an AF_XDP socket, and our Rust-based analysis engine picks them up from the completion queue.

    Performance benchmarks (Intel X710, single core):

    MetricValue
    Throughput10 Gbps sustained
    Packets/sec14.2M pps
    Latency (XDP → userspace)0.8μs p50, 2.1μs p99
    Drop rate0.000% at 10G
    CPU utilization43% single core

    BPF CO-RE: Deploy Once, Run Everywhere

    Our XDP programs are compiled using BPF CO-RE (Compile Once — Run Everywhere), which means a single binary runs across kernel versions 5.10 through 6.8 without recompilation. This is critical for enterprise deployment where sensor hosts may run different kernel versions.

    The libbpf loader handles BTF (BPF Type Format) relocations at load time, mapping our program's field accesses to the running kernel's actual struct layouts. This eliminates the need for per-kernel-version builds and reduces our deployment surface from hundreds of binaries to one.

    What This Means for Detection

    By operating at the XDP layer, our sensors see every packet — including those that would be dropped by iptables, nftables, or connection tracking. This is particularly important for Tor exit monitoring, where we need visibility into:

    • Connection setup and teardown (SYN/FIN patterns)
    • Payload entropy measurements (detecting encrypted exfiltration)
    • Inter-arrival timing signatures (used for circuit correlation)
    • Fragmented packets (often used to evade IDS)

    The result is a capture pipeline that delivers full-fidelity packet metadata to our inference engine in under a microsecond, at line rate, with zero drops.