Author: David Peter, Tangent Networks
License: MIT
Part of: tn_netdog_linux — Linux AF_XDP Inline IPS Deployment Toolchain
tn_xdp_probe is a zero-dependency C utility that authoritatively determines the AF_XDP capability of every physical network interface on a Linux host. It does this by performing real kernel-level socket bind probes against each hardware queue — not by reading driver names, ethtool flags, or any other indirect proxy.
Within tn_netdog_linux, it serves as the single source of truth for XDP hardware classification. tn_preflight.sh delegates all NIC capability decisions to this tool and interprets its JSON output rather than making its own ethtool-based guesses. The separation is intentional: shell scripts are the wrong place to be making kernel capability judgements.
ethtool -k <iface> reports feature flags as the driver advertises them, not as the kernel actually honours them at bind time. A driver may advertise xdp: on but return EINVAL on bind due to a ring size constraint, firmware version mismatch, or LRO conflict. Conversely, igb reports GRO and GSO offloads as [fixed] — which looks like a limitation — but fully supports AF_XDP zero-copy. The only way to know for certain is to attempt the bind and observe the kernel's response. That is exactly what this tool does.
The probe sequence runs once per interface and leaves no persistent state.
Before touching any interface, the tool verifies that CAP_BPF is available by issuing a harmless BPF_PROG_GET_NEXT_ID syscall. If this returns EPERM, it exits immediately with a clear error and setcap instructions rather than producing confusing per-interface failures.
It also checks /proc/sys/vm/nr_hugepages and warns if no 2MB hugepages are allocated, since hugepage-backed UMEM significantly improves zero-copy DMA efficiency.
A minimal two-instruction XDP_PASS BPF program is compiled inline in the binary as raw bytecode — no libbpf, no .o file, no LLVM:
mov64 r0, XDP_PASS (2)
exit
This is the smallest valid XDP program. It passes every packet up the stack unchanged. It exists only to prove that an XDP program can be attached to the interface.
The tool attempts to attach the XDP_PASS program using three methods in preference order:
| Method | Kernel Requirement | Notes |
|---|---|---|
BPF_LINK_CREATE |
≥ 5.7 | Preferred. Kernel manages fd lifecycle. Clean detach on close(). |
netlink DRV_MODE |
Any | Legacy. Confirms native driver hook exists. |
netlink SKB_MODE |
Any | Generic path. Always succeeds. Means no native hook in driver. |
If SKB_MODE is the only method that works, zero-copy probes are skipped for all queues — there is no native XDP hook to bind to.
After successful attach, the program is detached immediately. A RCU_FLUSH_US sleep follows to allow the kernel's RCU subsystem to release ring resources before the queue sweep begins.
If all three methods fail with EEXIST or EBUSY, the tool warns that an XDP program is already attached from a previous run or another process and prints the detach command.
Before sweeping all queues, the tool probes queue 0 for XDP_ZEROCOPY. If it returns EOPNOTSUPP, zero-copy is skipped for the entire interface — the driver has no native ZC hook and probing every queue would just produce the same error repeatedly.
For each queue, two independent bind attempts are made:
XDP_ZEROCOPY— NIC DMA-maps packet buffers directly into userspace memory. No kernel copy on the packet path. Best performance.XDP_COPY— Packets are copied between kernel and userspace rings. Functional for IPS but carries approximately 30% throughput overhead compared to zero-copy.
Each attempt uses a freshly allocated UMEM region that is munmap()'d before the next attempt. This is critical: AF_XDP sockets hold an exclusive reference to their UMEM, and reusing UMEM across probes produces false EBUSY errors from the kernel.
Between the ZC and CP probes on the same queue, a RCU_FLUSH_US sleep allows the kernel to fully release the zero-copy ring resources before the copy-mode bind. On EBUSY, up to EBUSY_RETRIES attempts are made with EBUSY_SLEEP_US delays between them.
After all interfaces are probed, the tool prints:
- A per-interface capability matrix showing best cap, worst cap, queue count, hugepage status, and attach path
- A plain-language explanation of every capability level and what it means
- An IPS deployment verdict for the machine as a whole
- Per-interface action instructions
- Resolution steps for every failure mode
With --json, a machine-readable summary is emitted instead for consumption by tn_preflight.sh.
No driver allowlists. There are no hardcoded lists of "qualified" or "unqualified" drivers. The hardware tells us what it can do; we listen and report. A future driver not known at compile time will be classified correctly by its bind behaviour.
Isolated UMEM per probe. Fresh allocation and munmap() for every bind attempt. No UMEM is shared between probes. This eliminates an entire class of false positives.
RCU discipline. Every attach, detach, and bind is followed by a grace period sleep. The Linux kernel's RCU subsystem needs time to propagate ring state changes. Probes that skip this produce intermittent EBUSY results that are impossible to reproduce reliably.
No system state modified. The tool is purely read-only from the operator's perspective. The BPF program is detached before the queue sweep begins. No interface configuration is changed. No files are written. Safe to run on a production system.
Zero external dependencies. Uses only standard Linux UAPI headers available in any kernel source tree. Links against nothing beyond libc. The binary can be copied to any x86-64 Debian host and run without installing anything.
On a minimal Debian or Ubuntu installation:
sudo apt-get update
sudo apt-get install --no-install-recommends build-essential ethtoolbuild-essential provides GCC and the standard library headers. The Linux UAPI kernel headers (linux/bpf.h, linux/if_xdp.h, linux/netlink.h, etc.) are included with the kernel source and are part of the standard include path on any Debian system. No separate linux-headers package is required.
ethtool is not required by the probe itself. It is listed as a dependency of tn_netdog_linux as a whole for other toolchain components.
The following Linux capabilities are required:
| Capability | Purpose |
|---|---|
CAP_NET_RAW |
Open raw AF_XDP sockets and Netlink routing sockets |
CAP_NET_ADMIN |
Attach and detach XDP programs to interfaces via Netlink |
CAP_BPF |
Load inline eBPF programs via BPF_PROG_LOAD (kernel ≥ 5.8) |
CAP_SYS_ADMIN |
Substitute for CAP_BPF on kernels < 5.8 |
The simplest approach is to run as root. To grant the minimum capabilities to the binary instead:
sudo setcap cap_net_raw,cap_net_admin,cap_bpf+ep ./tn_xdp_probe
./tn_xdp_probeOn kernels older than 5.8 where CAP_BPF does not exist, replace cap_bpf with cap_sys_admin.
AF_XDP zero-copy performance depends on the NIC DMA engine being able to map large contiguous memory regions. 2MB hugepages satisfy this requirement efficiently; 4KB regular pages require many more DMA mapping entries and reduce throughput.
Hugepages are not required for the probe to function — it will fall back to regular pages automatically and note this in the output. They are recommended for production IPS deployments using XDP_ZEROCOPY.
cat /proc/sys/vm/nr_hugepages
grep HugePages /proc/meminfoecho 512 | sudo tee /proc/sys/vm/nr_hugepagesThis allocates 512 × 2MB = 1GB of hugepage memory. The allocation is lost on reboot.
echo "vm.nr_hugepages = 512" | sudo tee -a /etc/sysctl.conf
sudo sysctl -pThe appropriate number of hugepages depends on the number of AF_XDP sockets and queues your IPS deployment will use. Each socket with a 4MB UMEM (the default in this tool) consumes two 2MB hugepages. For a two-interface IPS pair with 4 queues each, 16 hugepages is a reasonable minimum; 512 leaves comfortable headroom.
gcc -O2 -Wall -Wextra -o tn_xdp_probe tn_xdp_probe.cTo install system-wide as part of the tn_netdog_linux toolchain:
sudo install -m 0755 tn_xdp_probe /usr/local/bin/tn_xdp_probeUsage: ./tn_xdp_probe [OPTIONS]
--iface <name> Probe only this interface (default: all physical NICs)
--queues <n> Max queues to probe per interface (default: all)
--json Emit machine-readable JSON (for tn_preflight.sh)
--no-color Disable ANSI colour output
--help Show this message
Requires CAP_NET_RAW + CAP_NET_ADMIN + CAP_BPF, or root.
Probe all physical NICs:
sudo ./tn_xdp_probeProbe a single interface:
sudo ./tn_xdp_probe --iface enp9s0Probe one interface, limit to 4 queues:
sudo ./tn_xdp_probe --iface enp9s0 --queues 4JSON output for script consumption:
sudo ./tn_xdp_probe --json
sudo ./tn_xdp_probe --json > xdp_audit.jsonDisable colour (e.g. for log files):
sudo ./tn_xdp_probe --no-color | tee preflight.logColour is also disabled automatically when stdout is not a terminal.
When run with --json, the tool emits a structured JSON document and exits. No human-readable output is produced. This is the intended interface for tn_preflight.sh.
{
"interfaces": [
{
"name": "enp9s0",
"driver": "igb",
"speed_mbps": "1000",
"queues_found": 4,
"queues_tested": 4,
"attach_method": "BPF_LINK_CREATE",
"best_cap": "Zero-Copy",
"worst_cap": "Zero-Copy",
"zc_queues": 4,
"cp_queues": 4,
"hugepages": false,
"ips_capable": true,
"ips_rating": "EXCELLENT"
},
{
"name": "enp10s0",
"driver": "igb",
"speed_mbps": "1000",
"queues_found": 4,
"queues_tested": 4,
"attach_method": "BPF_LINK_CREATE",
"best_cap": "Zero-Copy",
"worst_cap": "Zero-Copy",
"zc_queues": 4,
"cp_queues": 4,
"hugepages": false,
"ips_capable": true,
"ips_rating": "EXCELLENT"
}
]
}| Field | Type | Description |
|---|---|---|
name |
string | Interface name as reported by the kernel |
driver |
string | Kernel driver module name from sysfs |
speed_mbps |
string | Link speed in Mbps, or "link-down" if interface is DOWN |
queues_found |
integer | Number of RX queues found in sysfs |
queues_tested |
integer | Number of queues actually probed (may be less if --queues is set) |
attach_method |
string | BPF attach path that succeeded: BPF_LINK_CREATE, netlink/DRV_MODE, netlink/SKB_MODE, or FAILED |
best_cap |
string | Best capability observed across all queues |
worst_cap |
string | Worst capability observed across all queues |
zc_queues |
integer | Number of queues that confirmed XDP_ZEROCOPY |
cp_queues |
integer | Number of queues that confirmed XDP_COPY |
hugepages |
boolean | Whether any queue obtained a hugepage-backed UMEM |
ips_capable |
boolean | true if best_cap is Copy-Mode or better |
ips_rating |
string | One-word deployment rating (see capability matrix below) |
The probe classifies each queue into one of the following capability levels. The levels are ordered from best to worst — the tool always reports the best and worst observed across all queues for each interface.
| Level | Bind Flag | Meaning | IPS Verdict |
|---|---|---|---|
Zero-Copy |
XDP_ZEROCOPY |
NIC DMA-maps UMEM frames directly into userspace. No kernel copy on the packet path. Full line rate. | IPS OK — preferred |
Copy-Mode |
XDP_COPY |
Packets copied between kernel and userspace rings. Functional but ~30% throughput overhead vs zero-copy. | IPS OK — acceptable |
EBUSY |
— | Queue is held by another process after all retries. May resolve after reboot or clearing the holder. | IPS WARN |
EINVAL |
— | Driver precondition not met. Common causes: LRO enabled, ring size out of range, firmware too old. | IPS FAIL |
EPERM |
— | Missing CAP_BPF or CAP_NET_RAW. Tool must be run as root or with correct capabilities set. |
IPS FAIL |
EOPNOTSUPP |
— | Driver has no native XDP hook. Only generic SKB-mode XDP is available, which does not support AF_XDP sockets. | IPS FAIL |
FAILED |
— | Socket or UMEM allocation failed before bind could be attempted. Check system memory and dmesg. |
IPS FAIL |
The attach method recorded for each interface indicates which XDP data path is available:
| Attach Method | Meaning |
|---|---|
BPF_LINK_CREATE |
Modern kernel (≥ 5.7). Native driver hook confirmed. Supports XDP_ZEROCOPY. |
netlink/DRV_MODE |
Legacy attach path. Native driver hook confirmed. Supports XDP_ZEROCOPY. |
netlink/SKB_MODE |
No native driver hook. Generic XDP only. XDP_ZEROCOPY will not be available. XDP_COPY may still work. |
FAILED |
All attach methods failed. XDP is not functional on this interface. |
The driver does not implement the XDP receive hook (ndo_bpf). This interface cannot be used for inline AF_XDP IPS.
- Verify the NIC is bound to its native driver and not
vfio-pcioruio_pci_generic:dmesg | grep -i driver - Check the driver module is loaded:
lsmod | grep <driver> - Known drivers with native XDP support:
igb,ixgbe,i40e,ice,bnxt_en,mlx5_core,nfp,mvneta - Virtual/paravirtual drivers (
virtio_net,vmxnet3,e1000) do not support native XDP; use as management ports only
Another AF_XDP socket is holding the queue, or a previous crashed process left a socket open.
# Identify the holder
ss -x
lsof | grep -i xdp
# Detach any existing XDP program from the interface
ip link set <iface> xdp off
# Check for OVS XDP programs
ovs-vsctl list interfaceIf nothing is found, a reboot will clear orphaned socket state.
The driver rejected the bind due to a configuration conflict. Common causes:
# Disable LRO (Large Receive Offload) -- conflicts with XDP on many drivers
ethtool -K <iface> lro off
# Check dmesg for the specific driver error
dmesg | grep -i 'xdp\|invalid\|<driver name>'
# Verify ring size is a power of two and within driver limits
ethtool -g <iface>The probe could not allocate a socket or UMEM before the bind attempt.
# Check available memory
free -h
# Check system limits
ulimit -l # locked memory limit (must be > UMEM_SIZE = 4MB)
ulimit -n # open file limit
# Check dmesg for memory pressure or OOM events
dmesg | grep -iE 'oom|out of memory|alloc'If another process (OVS, a previous tn_xdp_probe run, Suricata, etc.) left an XDP program attached, BPF_LINK_CREATE returns EBUSY or EEXIST. The tool prints a warning with the detach command:
ip link set <iface> xdp offtn_preflight.sh calls tn_xdp_probe --json and parses the output to classify interfaces for IPS deployment. The integration works as follows:
# In tn_preflight.sh -- interface capability section
XDP_JSON=$(tn_xdp_probe --json 2>/dev/null) || {
fail "tn_xdp_probe failed -- check CAP_BPF and interface state"
exit 1
}
# Count IPS-capable interfaces
IPS_COUNT=$(printf '%s' "${XDP_JSON}" | \
awk -F'"' '/"ips_capable": true/{count++} END{print count+0}')
if [ "${IPS_COUNT}" -ge 2 ]; then
ok "IPS pair ready -- ${IPS_COUNT} XDP-capable interfaces found"
else
fail "Need 2 XDP-capable interfaces for inline IPS -- found ${IPS_COUNT}"
fiThis keeps the capability classification logic entirely in the C tool where it belongs. The shell script interprets the verdict, not the kernel internals.
The following NICs have been verified to produce correct results with tn_xdp_probe. This list is informational — the tool does not use it internally.
| Driver | NIC Family | Zero-Copy | Notes |
|---|---|---|---|
igb |
Intel I210, I211, I350 | Yes | GRO/GSO reported as [fixed] by ethtool — this is normal and not a limitation |
ixgbe |
Intel X540, X550, 82599 | Yes | Disable LRO before probing: ethtool -K <iface> lro off |
i40e |
Intel X710, XL710, XXV710 | Yes | |
ice |
Intel E810 | Yes | Requires firmware ≥ 2.x |
bnxt_en |
Broadcom NetXtreme-E | Yes | |
mlx5_core |
Mellanox ConnectX-4/5/6 | Yes | Requires MLNX_OFED or upstream kernel ≥ 5.3 |
virtio_net |
KVM/QEMU virtio | No | Use for management only; PCI passthrough a physical NIC for IPS |
vmxnet3 |
VMware | No | Use for management only |
e1000 / e1000e |
Intel GbE (legacy) | No | Too old; no XDP hook |
r8169 |
Realtek | No | Consumer NIC; no native XDP |
tn_xdp_probe requires elevated privileges and briefly touches the network data path of each probed interface. The following properties limit its attack surface:
- Read-only by design. No files are written. No interface configuration is changed. No kernel state persists after the tool exits.
- Minimal BPF program. The XDP_PASS bytecode is two instructions, GPL-licensed, and does nothing except return
XDP_PASS. It cannot modify, drop, or redirect packets. - Immediate detach. The BPF program is detached before any socket bind probes begin. The data path is never in a modified state during the queue sweep.
- No network transmission. The tool opens
AF_XDPsockets to probe bind capability but never writes to TX rings or transmits any frames. - No external input parsed. The tool reads only sysfs files,
/proc/sys/vm/nr_hugepages, and command-line arguments. No network input is parsed.
MIT License. See LICENSE for full text.
Copyright (c) 2026 David Peter, Tangent Networks
Part of the Tangent Networks tn_netdog_linux open source IPS deployment toolchain.
End of README.md