Auto-sync a Linux host's real listening ports into XDP or nftables, so public servers stay locked down without hand-maintaining firewall rules.
Auto XDP is a host-side firewall for public and self-hosted Linux machines. It watches the services that are actually listening, keeps the active backend in sync automatically, preserves return traffic with a tc egress helper, and falls back to nftables when XDP cannot be attached.
It is built for single-host self-protection: VPSes, public cloud instances, homelab nodes, and small Internet-facing Linux machines that keep getting scanned, probed, and hit with random L3/L4 attacks.
- Overview
- How It Works
- Components
- Key Features
- Requirements
- Quick Start
- Install From Source
- What
setup_xdp.shDoes - Automated Distro Checks
- Testing
- BPF Maps
- Why ARRAY instead of HASH?
- Auto-Sync Daemon
- Configuration
- Statistics
- Post-Install Quick Commands
- Packet Event Stream
- Threat-Intel Blocklist
- Packet Filtering Logic
- Uninstall
- Real-World Performance Benchmark
- Contributing
- Star History
- Behavior Change
- Special Thanks
- License
XDP (eXpress Data Path) is an eBPF-based, high-performance packet processing path that runs before packets enter the Linux networking stack (at the NIC driver level). This makes it significantly faster than traditional iptables/nftables filtering.
Personal cloud instances are constantly scanned and probed. Traditional firewalls like iptables and nftables work, but they usually rely on static allowlists that drift away from what the host is really exposing. Open a service and forget to allow it, it breaks. Stop a service and forget to close it, the hole stays open.
Auto XDP keeps the ingress policy aligned with the host's real listening sockets. It filters traffic at the NIC driver level when XDP is available, records outbound state so return traffic is not broken, and degrades to a synced nftables ruleset instead of leaving the host unprotected when XDP is unavailable.
You can. They are good tools.
The difference is operational:
nftables/ufwusually start from a static ruleset that you keep in sync yourself.- Auto XDP starts from the host's real listening sockets and keeps the active backend aligned automatically.
- When XDP is available, unwanted traffic is dropped before the normal kernel networking path instead of later in the stack.
- When XDP is not available, the same control plane still drives a fallback
nftablesruleset instead of leaving you with two separate systems to maintain.
Incoming Packet
│
▼
┌─────────────┐
│ NIC Driver │ ← XDP hooks here (before kernel stack)
└──────┬──────┘
│
▼
┌──────────────────────────────────────────────┐
│ xdp_firewall │
│ │
│ VLAN strip → nesting > limit ────────→ DROP │
│ │ │
│ ├─ ARP / non-IP ─────────────→ PASS │
│ │ │
│ └─ IPv4 / IPv6 │
│ ├─ Fragment ──────────────→ DROP │
│ ├─ Bogon src (if enabled) → DROP │
│ │ │
│ ├─ TCP │
│ │ ├─ Malformed flags/doff → DROP│
│ │ ├─ Trusted src / ACL ──→ PASS │
│ │ ├─ Conntrack hit ──────→ PASS │
│ │ ├─ SYN + whitelist ────→ PASS │
│ │ ├─ SYN rate exceeded ──→ DROP │
│ │ └────────────────────→ DROP │
│ │ │
│ ├─ UDP │
│ │ ├─ Malformed ──────────→ DROP │
│ │ ├─ Conntrack hit ──────→ PASS │
│ │ ├─ Whitelist miss ─────→ DROP │
│ │ ├─ Trusted src / ACL ──→ PASS │
│ │ ├─ Per-src rate ───────→ DROP │
│ │ ├─ Global rate ────────→ DROP │
│ │ └────────────────────→ PASS │
│ │ └────────────────────→ DROP │
│ │ │
│ ├─ ICMP / ICMPv6 │
│ │ ├─ Error types ────────→ PASS │
│ │ ├─ Echo (rate-limited) → PASS │
│ │ └─ Echo (rate exceeded)→ DROP │
│ │ │
│ ├─ Proto-41 (6in4/SIT) │
│ │ ├─ sit4_endpoints hit ─→ PASS │
│ │ └────────────────────→ DROP │
│ │ │
│ └─ Other (GRE / ESP / SCTP / …) │
│ └─ slot handler ───→ PASS/DROP│
└──────────────────────────────────────────────┘
│
▼
XDP_PASS / XDP_DROP
xdp_firewall.c— eBPF/XDP kernel program that filters packets at wire speedtc_flow_track.c— eBPFtcegress helper that records outbound IPv4/IPv6 TCP SYN packets and UDP reply tuplesxdp_port_sync.py— userspace daemon that reconciles discovered sockets and configured policy into the active backend; proc-connector and relay events provide fast triggers, while an independent 30-second full discovery repairs missed events and driftpkt_relay.py— userspace daemon that drains thepkt_ringbufBPF ring buffer and broadcasts packet events (DROP/ALLOW) over a Unix socket; runs as theauto-xdp-relayserviceauto_xdp/tui.py— htop-like live TUI client (axdp tui) that subscribes to the relay socket and displays packet events, port deltas, and per-counter ratesauto_xdp/abuseipdb.py— threat-intel syncer that fetchesborestad/blocklist-abuseipdbIPv4 lists and writes them to theabuseipdb_v4LPM trieaxdp— operator CLI for statistics, sync, service control, and daemon log levelsetup_xdp.sh— installer that compiles the BPF objects, installs the runtime launcher, and sets up boot-time auto-sync
- Wire-speed filtering via XDP (bypasses kernel network stack)
- ~40–65 ns per-packet latency measured on real hardware (see Benchmarks)
- Event-driven plus periodic reconciliation: proc-connector and relay events trigger fast updates, while an independent 30-second full socket discovery reconciles desired and kernel state; a failed Netlink event source does not block relay triggers, the timer, conntrack GC, or drift verification
- IPv4 + IPv6 TCP conntrack hardening: pure SYN creates temporary state with a short configurable SYN timeout; unsolicited ACK packets are dropped
- Kernel-side outbound state tracking: a
tcegress program records host-initiated IPv4/IPv6 TCP SYN packets and UDP reply tuples so return traffic can be matched at XDP without reopening the old bypasses - IPv4 + IPv6 UDP hardening: inbound server ports use
udp_whitelist, reply traffic can be matched by separateudp_ct4/udp_ct6maps, and trusted IPv4/IPv6 sources can bypass UDP rate limits only after the destination port is open - IPv6 support, including extension header traversal on both XDP and tc egress, plus explicit non-initial fragment drops
- 6in4 (SIT) tunnel endpoint filtering: a
sit4_endpointsmap allows only configured outer IPv4 sources for proto-41 encapsulated traffic; unconfigured tunnel packets are dropped - Per-CIDR port ACL rules:
axdp acl add tcp CIDR PORT...can explicitly allow TCP ports for selected source CIDRs independently of auto-discovery; UDP ACL rules apply after the UDP destination port is already whitelisted - Under-attack mode:
axdp under-attack ondisables high-volume packet event emission and reduces TUI map-sampling work; socket discovery and policy reconciliation continue so protection does not drift - Periodic conntrack sync (seeding established flows): the daemon periodically seeds existing IPv4/IPv6 TCP sessions into
tcp_ct4/tcp_ct6, which helps preserve active sessions after re-attaching XDP or manual map clears - Transactional XDP/tc reloads: a fully validated and policy-seeded candidate generation replaces each active program without a detach-first gap; any partial multi-interface failure restores the previous XDP and tc generation on interfaces already switched
- Pinned BPF maps that survive reloads and can be updated at runtime
- ICMP token-bucket rate limiter: XDP-level protection against ICMP/ICMPv6 ping floods; 100 pps burst cap with per-second token refill, while ARP and IPv6 NDP control traffic (RS/RA/NS/NA) are always passed
- Per-source SYN/UDP rate limiting (anti-brute-force): configurable per-port limits tracked per source IP by default, or per configured source CIDR via
rate_limits.source_cidr_v4/source_cidr_v6 - Boot-time loader: restores protection on reboot instead of only syncing userspace state
- Systemd + OpenRC support: installs the service automatically when either init system is present
- Configurable daemon verbosity:
axdp log-level debug|info|warning|errorupdates the installed service config and restarts it - Native + generic XDP: tries native first, then generic
- nftables fallback: if both XDP attach modes fail, keeps automatic port whitelisting with a dynamic
nftablesruleset - AbuseIPDB threat-intel blocklist (opt-in): drops source IPs listed in the
borestad/blocklist-abuseipdbIPv4 feeds before any conntrack/whitelist evaluation; populated by an in-daemon syncer (no API key required); fail-open when the map is empty so a fetch failure cannot lock you out - Per-packet event ring buffer + relay: kernel emits both DROP and ALLOW events to the
pkt_ringbufBPF ring buffer; theauto-xdp-relayservice broadcasts them over a Unix socket with configurable retention so late-attaching clients can replay recent history - Live TUI:
axdp tuisubscribes to the relay socket and renders packet events, current port whitelist, and counter rates side-by-side
- Linux kernel ≥ 4.18 for the XDP backend
- Popular Linux distro with a supported package manager: Debian/Ubuntu, Fedora/RHEL, openSUSE, Arch, or Alpine
sudoaccess — the installer runs as an ordinary user and escalates only for the steps that need it (package installs, writes under/usr/local/lib&/etc, service management, and loading the XDP backend); running the whole script as root still worksnftablessupport is used automatically as the compatibility fallback when XDP cannot be attached
clang,llvm— compile BPFlibbpforlibbpf-dev/libbpf-devel— BPF headers, depending on distrobpftool— manage BPF mapsiproute2oriproute— provides bothipandtcfor XDP attach and UDP egress trackingpython33.10 or newer — sync daemon runtime; Python 3.11+ uses the stdlibtomllib, and the installer addstomliautomatically for Python 3.10nftables— compatibility fallback backend
(
set -e
AUTO_XDP_VERSION=v26.7.7a
auto_xdp_tmp=$(mktemp -d)
trap 'rm -rf "$auto_xdp_tmp"' EXIT
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -sSfL \
"https://github.com/Kookiejarz/Auto_XDP/archive/refs/tags/${AUTO_XDP_VERSION}.tar.gz" \
| tar -xz -C "$auto_xdp_tmp" --strip-components=1
cd "$auto_xdp_tmp"
sudo bash setup_xdp.sh
)(
set -e
AUTO_XDP_VERSION=<version_here>
auto_xdp_tmp=$(mktemp -d)
trap 'rm -rf "$auto_xdp_tmp"' EXIT
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -sSfL \
"https://github.com/Kookiejarz/Auto_XDP/archive/refs/tags/${AUTO_XDP_VERSION}.tar.gz" \
| tar -xz -C "$auto_xdp_tmp" --strip-components=1
cd "$auto_xdp_tmp"
bash setup_xdp.sh
)The release archive keeps the installer, build inputs, Python runtime, and handlers on the same tag instead of mixing a tagged entry script with files from main.
Direct curl | bash execution is rejected unless AUTO_XDP_SOURCE_REF explicitly identifies a tag, branch, or full commit SHA. The release archive flow above is preferred because every source file is already present locally before privilege escalation.
Run the installer as your normal user — it asks for sudo once, when the first
step that needs root begins. (Prefixing the whole command with sudo also works.)
git clone https://github.com/Kookiejarz/Auto_XDP.git
cd Auto_XDP
# Auto-detect interface
bash setup_xdp.sh
# Or specify interface
bash setup_xdp.sh eth0
# Deploy to every active non-loopback interface
bash setup_xdp.sh --all-interfaces
# Or deploy to a specific set of interfaces
bash setup_xdp.sh eth0 eth1
# Preview the detected OS, init system, packages, and target interfaces (no root needed)
bash setup_xdp.sh --dry-run
# Compare local files with GitHub first, then decide interactively
bash setup_xdp.sh --check-update
# Non-interactive mode for CI / automation
bash setup_xdp.sh --check-update --forceBy default, the installer uses the interface from the default route. For hosts with multiple public NICs, pass every protected interface explicitly or use --all-interfaces.
# Protect all active non-loopback interfaces
bash setup_xdp.sh --all-interfaces
# Protect only selected interfaces
bash setup_xdp.sh eth0 ens5The generated runtime config stores the interface list, and the boot-time loader re-attaches XDP and the tc egress tracker to those interfaces after service restart or reboot.
- Checks for root privileges
- Resolves the target interface or interface list
- Installs missing dependencies via the detected package manager
- Uses local
xdp_firewall.c/tc_flow_track.c/xdp_port_sync.py/axdpby default from a local checkout; when run fromstdin, it prefers the matching GitHub copies - Compiles the XDP and tc BPF objects when the host has the required toolchain
- Installs
xdp_required_maps.txtbefore attaching XDP so the map readiness check uses the current version - Loads a candidate BPF pin generation, validates its complete map ABI, and pre-seeds runtime config, discovered port policy, ACLs, rate-limit state, and current IPv4/IPv6 sessions before switching traffic
- Replaces XDP on each target interface in native mode or generic fallback mode while retaining the previous pinned generation for rollback
- Replaces the fixed-priority
tc clsactegress tracker; if any XDP or tc switch fails, restores the previous programs on every interface already changed - Commits the candidate generation only after the complete XDP/tc switch succeeds; otherwise keeps the previous generation active, or uses
nftableswhen XDP cannot be established - Installs the runtime launcher at
/usr/local/bin/auto_xdp_start.sh - Installs the sync daemon at
/usr/local/bin/xdp_port_sync.py - Installs the packet event relay at
/usr/local/bin/pkt_relay.py - Runs an initial port sync using the selected backend
- Registers and starts
xdp-port-syncandauto-xdp-relayonsystemdorOpenRCwhen available
The repository includes a GitHub Actions matrix that installs each supported distro's native build dependencies inside that distro's own container image and compiles the BPF objects there directly.
This CI is meant to answer one question clearly: does this distro's native toolchain and header layout build xdp_firewall.c, tc_flow_track.c, and the slot handlers successfully?
You can run the same native compile check locally on a machine that already has the build dependencies installed:
bash ./tests/bash/test_bpf_build.shIf you only want the package-manager and init-system probe from the installer, use:
bash setup_xdp.sh --check-envRun the portable repository suite from the project root:
bash ./tests/run-distro-suite.shIt performs shell and Python syntax checks, installer and CLI tests, the Python unit suite, and Linux installer smoke checks. The installer tests include deterministic failure injection for candidate validation, partial XDP attach, tc attach, rollback, and successful generation commit. Syncer tests separately verify that relay-triggered and timer-triggered reconciliation continue when the Netlink proc connector is unavailable.
The kernel integration suite requires root, clang, bpftool, iproute2, Python 3, a mounted bpffs, network namespaces, and generic XDP support:
sudo bash ./tests/bash/test_integration.shIts eight checks cover real BPF program/map loading, exact map re-pinning after a missing pin, generic-mode attachment, TCP whitelist allow/drop behavior, UDP reply conntrack, TCP ACL admission, per-port SYN limiting, and detach/reload/re-attach behavior. The suite uses an isolated RFC1918 veth subnet and disables only the bogon policy in its test runtime map so packets reach the admission branch each case is intended to verify; production bogon defaults are unchanged.
The native compile-only matrix remains available separately:
bash ./tests/bash/test_bpf_build.shTests that cannot acquire their required kernel capabilities must report a skip before assertions run; an exit status of zero caused only by an early prerequisite skip is not equivalent to an integration pass.
Pinned directory: /sys/fs/bpf/xdp_fw/
| Map | Type | Max Entries | Key | Value |
|---|---|---|---|---|
tcp_whitelist |
ARRAY | 65536 | __u32 port (host byte order) |
__u32 (1 = allow) |
udp_whitelist |
ARRAY | 65536 | __u32 port (host byte order) |
__u32 (1 = allow) |
tcp_ct4 |
LRU_HASH | 262144 | struct flow_key_v4 { sport, dport, saddr, daddr } |
__u64 ktime_ns |
tcp_ct6 |
LRU_HASH | 262144 | struct flow_key_v6 { sport, dport, saddr[16], daddr[16] } |
__u64 ktime_ns |
udp_ct4 |
LRU_HASH | 262144 | struct flow_key_v4 |
__u64 ktime_ns |
udp_ct6 |
LRU_HASH | 262144 | struct flow_key_v6 |
__u64 ktime_ns |
trusted_ipv4 |
LPM_TRIE | 256 | struct trusted_v4_key { prefixlen, addr } (IPv4 CIDR) |
__u32 (1 = trusted) |
trusted_ipv6 |
LPM_TRIE | 256 | struct trusted_v6_key { prefixlen, addr[16] } (IPv6 CIDR) |
__u32 (1 = trusted) |
pkt_counters |
PERCPU_ARRAY | 35 | __u32 counter index |
__u64 packet count |
byte_counters |
PERCPU_ARRAY | 4 | __u32 index (0=total_bytes, 1=drop_bytes, 2=total_pkts, 3=drop_pkts) |
__u64 |
icmp_tb |
ARRAY | 1 | __u32 (0) |
struct icmp_token_bucket { last_ns, tokens } |
tcp_port_policies |
HASH | 1024 | __u32 dest port |
per-port SYN rate config |
udp_port_policies |
HASH | 1024 | __u32 dest port |
per-port UDP rate config |
udp_global_rl |
ARRAY | 1 | __u32 (0) |
struct udp_global_state { lock, byte_rate_max, window_start_ns, prev_bytes, curr_bytes } |
sit4_endpoints |
HASH | 256 | __u32 outer IPv4 src addr |
__u32 (1 = allowed) |
slot_ctx_map |
ARRAY | 16 | __u32 slot index |
slot handler context |
proto_handlers |
ARRAY | 256 | __u32 IP proto number |
handler slot index |
tcp_port_handlers |
HASH | 1024 | __u32 dest port |
handler slot index |
udp_port_handlers |
HASH | 1024 | __u32 dest port |
handler slot index |
abuseipdb_v4 |
LPM_TRIE | 262144 | struct trusted_v4_key { prefixlen, addr } |
__u32 (1 = blocked) |
# Allow TCP port 8080
bpftool map update pinned /sys/fs/bpf/xdp_fw/tcp_whitelist \
key 0x90 0x1f 0x00 0x00 value 0x01 0x00 0x00 0x00
# Remove TCP port 8080
bpftool map update pinned /sys/fs/bpf/xdp_fw/tcp_whitelist \
key 0x90 0x1f 0x00 0x00 value 0x00 0x00 0x00 0x00
# View current TCP whitelist
bpftool map dump pinned /sys/fs/bpf/xdp_fw/tcp_whitelistKey encoding note: the whitelist maps are ARRAY maps (BPF_MAP_TYPE_ARRAY), so entries are not deleted. Set the value to 1 to allow a port and 0 to close it. The key is a 4-byte little-endian __u32 port number (host byte order). Example: 8080 = 0x00001F90 → bytes 0x90 0x1f 0x00 0x00.
Originally, this project used BPF_MAP_TYPE_HASH for the whitelist. It transitioned to BPF_MAP_TYPE_ARRAY for several critical reasons:
- O(1) Lookup Time: An Array map provides constant-time lookup ($O(1)$) by directly indexing into memory using the port number. A Hash map averages O(1) but degrades under hash collisions, whereas an Array map guarantees O(1) by direct index access with no collision possible. :))))
- Zero Hash Collisions: With 65,536 entries (one for every possible port), there is no possibility of hash collisions. In a Hash map with a small max_entries (e.g., 64), collisions frequently occur during high-volume scans, causing latency spikes.
- CPU Cache Efficiency: Because the Array is a contiguous block of memory, the CPU's prefetcher can handle it much more efficiently than the pointer-chasing required by Hash map buckets.
The daemon xdp_port_sync.py runs behind the launcher /usr/local/bin/auto_xdp_start.sh and provides event-driven updates plus periodic correction for either backend:
- Independent Event Sources: Linux Netlink Process Connector events are debounced, while relay
port_changeevents trigger immediate reconciliation. Either source can reconnect or fail without disabling the other. - Efficient Discovery: Uses
psutilto read/procdirectly for listening ports (no slowssornetstatsubprocesses, yeahhh). - Independent Safety Reconcile: Performs a full discovery and reconcile every 30 seconds, even when no event source is available, to repair missed events and kernel-map drift.
- Backend Sync: Updates either pinned BPF maps or
nftablessets, depending on what the host supports. - UDP Discovery Rule: Because UDP has no
LISTENstate, the daemon syncs unconnected bound UDP sockets (no remote peer) intoudp_whitelist, which is a practical approximation of server-style UDP ports. - Trusted Source IPs/CIDRs: Optional IPv4/IPv6 addresses or CIDR ranges can be synced into the XDP-side
trusted_ipv4/trusted_ipv6LPM trie maps. In XDP mode, trusted TCP sources can pass pure SYN packets without the auto-discovered TCP whitelist; trusted UDP sources still require the destination UDP port to be whitelisted first. - Backend Guard Rails: In
automode, the daemon only selects XDP when the required pinned maps are present; otherwise it falls back tonftablesinstead of crashing.
Outbound TCP/UDP reply tracking is kernel-side: a tc egress program records reverse reply tuples into tcp_conntrack and udp_conntrack. The XDP ingress path checks those maps before falling back to the TCP/UDP admission rules.
The installed runtime config lives at /etc/auto_xdp/config.toml. Use axdp for common changes because it edits the TOML file and reloads the daemon for you.
# Show the current TOML config
sudo axdp config show
# Create the default config if it is missing
sudo axdp config init
# Apply a manual edit after changing /etc/auto_xdp/config.toml
sudo axdp restart| Goal | Command | Notes |
|---|---|---|
| Always keep a port open | sudo axdp permanent add tcp 2222 alt-ssh |
Supports tcp, udp, and sctp; useful when a port must remain allowed even if the process restarts |
| Trust an admin/source network | sudo axdp trust add 10.0.0.0/8 office-net |
TCP trusted sources may reach non-discovered ports; UDP trusted sources still require the destination UDP port to be open |
| Allow selected CIDRs to selected ports | sudo axdp acl add tcp 203.0.113.0/24 443 8443 |
TCP ACLs can open specific ports for the CIDR; UDP ACLs apply only after the UDP port is already whitelisted |
| Reduce observability overhead during an incident | sudo axdp under-attack on |
Disables packet event emission and high-churn TUI map sampling; port discovery and reconciliation continue |
| Change daemon verbosity | sudo axdp log-level debug |
Valid levels: debug, info, warning, error |
Examples:
# Permanent ports
sudo axdp permanent list
sudo axdp permanent add tcp 22 ssh
sudo axdp permanent add udp 50000 game-udp
sudo axdp permanent del tcp 22
# Trusted sources
sudo axdp trust list
sudo axdp trust add 203.0.113.5/32 monitoring
sudo axdp trust add 2001:db8::/32 office-v6
sudo axdp trust del 203.0.113.5/32
# Per-CIDR ACLs
sudo axdp acl list
sudo axdp acl add tcp 198.51.100.0/24 5432 6379
sudo axdp acl del tcp 198.51.100.0/24For bulk edits, change /etc/auto_xdp/config.toml directly. The main sections operators usually touch are:
[daemon]
log_level = "warning"
preferred_backend = "auto" # auto, xdp, or nftables
[discovery]
exclude_loopback = true
exclude_bind_cidrs = ["10.0.0.0/8"]
exclude_ports = [5432, 6379]
[permanent_ports]
tcp = [22, 443]
udp = [50000]
sctp = []
[trusted_ips]
"203.0.113.5/32" = "monitoring"
"2001:db8::/32" = "office-v6"
[[acl]]
proto = "tcp"
cidr = "198.51.100.0/24"
ports = [5432, 6379]After a manual edit, run sudo axdp restart to reload the runtime services. The axdp trust, axdp acl, axdp permanent, axdp log-level, and axdp under-attack commands perform their own reload.
Runtime data-path tunables live under [xdp.runtime] and are synced into the xdp_runtime_cfg map by the daemon:
[xdp.runtime]
tcp_timeout_seconds = 300
udp_timeout_seconds = 60
conntrack_refresh_seconds = 30
conntrack_gc_interval_seconds = 300
syn_timeout_seconds = 20
icmp_burst_packets = 100
icmp_rate_pps = 100
udp_global_window_seconds = 1
udp_global_byte_rate_mbps = 997
rate_window_seconds = 1
sensitive_port_threshold = 5
default_tcp_syn_rate_strict = 5
default_tcp_syn_rate = 100
default_tcp_syn_agg_rate_strict = 50
default_tcp_syn_agg_rate = 1000
default_tcp_established_per_src_strict = 5
default_tcp_established_per_src = 50
default_tcp_established_per_prefix_strict = 20
default_tcp_established_per_prefix = 200
default_tcp_established_per_port_strict = 200
default_tcp_established_per_port = 5000# systemd
systemctl status xdp-port-sync
journalctl -u xdp-port-sync -u auto-xdp-relay -f
# OpenRC
rc-service xdp-port-sync status
rc-service auto-xdp-relay status
# Manual foreground run
/usr/local/bin/auto_xdp_start.sh
# One-shot sync with automatic backend selection
sudo axdp sync
# Increase foreground verbosity temporarily
sudo axdp log-level debugAuto XDP installs a convenience command /usr/local/bin/axdp. Statistics are now built directly into axdp, so you only need one operational command after installation.
# Single snapshot
sudo axdp
# Real-time refresh
sudo axdp watch
# Show delta rates (pps / bps)
sudo axdp stats --rates
# Combine both
sudo axdp stats --watch --rates --interval 2
# Live TUI (htop-like view: events, ports, counters)
sudo axdp tui
sudo axdp tui --interval 1
sudo axdp tui --socket /var/run/auto_xdp/pkt_events.sock
# Run one manual sync
sudo axdp sync
# Inspect currently allowed TCP/UDP ports
sudo axdp ports
# Show active backend and XDP attachment state
sudo axdp backend
# Show TCP/UDP conntrack entry visibility
sudo axdp conntrack
sudo axdp conntrack tcp
sudo axdp conntrack udp
# Under-attack mode: reduce packet-event and TUI sampling overhead
sudo axdp under-attack
sudo axdp under-attack on
sudo axdp under-attack off
# Trusted source IPs/CIDRs
sudo axdp trust list
sudo axdp trust add 10.0.0.0/8 office-net
sudo axdp trust del 10.0.0.0/8
# Per-CIDR port ACL rules
sudo axdp acl list
sudo axdp acl add tcp 203.23.2.0/24 443 8443
sudo axdp acl del tcp 203.23.2.0/24
# Permanent ports (never auto-removed)
sudo axdp permanent list
sudo axdp permanent add tcp 2222 alt-ssh
sudo axdp permanent del tcp 2222
# Protocol slot handlers (GRE, ESP, SCTP, custom)
sudo axdp slot list
sudo axdp slot load gre
sudo axdp slot load 47 /etc/auto_xdp/handlers/custom_gre.o
sudo axdp slot unload 47
# View or change daemon log level
sudo axdp log-level
sudo axdp log-level debug
# Service control
sudo axdp start
sudo axdp stop
sudo axdp restart
sudo axdp statusWhat it shows:
xdpbackend: per-category packet counters from/sys/fs/bpf/xdp_fw/pkt_counters, plus interface RX totalsnftablesbackend: current drop counter from theinet auto_xdp inputchain, plus interface RX totals--rates: packet deltas for XDP counters, and packet/bit deltas where byte counters are available
Counter labels in axdp are intentionally human-readable:
TCP_NEW_ALLOW— pure SYN packets admitted bytcp_whitelistor trusted sourceTCP_ESTABLISHED— TCP packets admitted bytcp_conntrackTCP_DROP— TCP packets droppedUDP_PASS— UDP packets passedUDP_DROP— UDP packets droppedIPv4_OTHER— IPv4 non-TCP/UDP (ICMP, GRE, etc.) passedIPv6_ICMP— ICMPv6 and other non-TCP/UDP IPv6 traffic passedFRAG_DROP— fragmented packets dropped (IPv4 MF/offset set, or non-initial IPv6 fragments)ARP_NON_IP— ARP and other non-IP Ethernet traffic passedTCP_CT_MISS— TCP ACK packets dropped because no conntrack entry existedICMP_DROP— ICMP/ICMPv6 echo packets dropped by the token-bucket rate limiterSYN_RATE_DROP— TCP SYN packets dropped by the per-IP SYN rate limiterUDP_RATE_DROP— UDP packets dropped by the per-source-IP rate limiterUDP_GBL_DROP— UDP packets dropped by the global sliding-window rate limiterTCP_NULL— TCP NULL scan (all flags zero)TCP_XMAS— TCP XMAS scan (FIN+URG+PSH)TCP_SYN_FIN— TCP SYN+FIN contradictory flagsTCP_SYN_RST— TCP SYN+RST contradictory flagsTCP_RST_FIN— TCP RST+FIN contradictory flagsTCP_BAD_DOFF— TCP invalid data offset (doff < 5,doff > 15, or truncated header)TCP_PORT0— TCP src or dst port is 0VLAN_DROP— VLAN nesting depth exceeds limit (possible bypass attempt)SLOT_CALL— packets dispatched to a protocol slot handler via tail callSLOT_PASS— slot miss withdefault_action = pass(no handler matched)SLOT_DROP— slot miss withdefault_action = drop(no handler matched)UDP_PORT0— UDP src or dst port is 0UDP_BAD_LEN— UDP length field < 8 or exceeds packet boundaryBOGON_DROP— packet dropped: source address in spoofed/reserved (bogon) rangeTCP_CONN_LIMIT_DROP— TCP SYN dropped by per-source concurrent connection limitSYN_AGG_RATE_DROP— TCP SYN dropped by per-prefix aggregate rate limiterUDP_AGG_RATE_DROP— UDP dropped by per-prefix byte-rate limiterHANDLER_BLOCK_DROP— dropped: source IP inhandler_blockedmapTCP_CONN_PREFIX_LIMIT_DROP— TCP SYN dropped by per-prefix concurrent connection limitTCP_CONN_PORT_LIMIT_DROP— TCP SYN dropped by per-port total concurrent connection limitABUSEIPDB_DROP— dropped: source IP in AbuseIPDB blocklist (when[abuseipdb] enabled = true)
After installation, these are the main commands you will actually use:
# Help
sudo axdp help
# Current statistics snapshot
sudo axdp
# Live statistics
sudo axdp watch
# Delta rates
sudo axdp stats --rates
# Live delta rates
sudo axdp stats --watch --rates --interval 2
# Live TUI (events + ports + counters in one view)
sudo axdp tui
# Run one manual sync
sudo axdp sync
# Inspect currently allowed ports
sudo axdp ports
# Active backend and XDP attachment
sudo axdp backend
# Conntrack visibility
sudo axdp conntrack
# Under-attack mode (discovery/reconciliation remains active)
sudo axdp under-attack on
sudo axdp under-attack off
# Change daemon log verbosity and restart the service
sudo axdp log-level
sudo axdp log-level debug
sudo axdp log-level info
# Service control
sudo axdp start
sudo axdp stop
sudo axdp status
sudo axdp restartWhen you run the installer from a cloned repo, local source files win by default. If you want the script to compare your local copies with GitHub first, use:
bash setup_xdp.sh --check-updateIn --check-update mode, the installer:
- Downloads the GitHub version of
xdp_firewall.c,tc_flow_track.c,xdp_port_sync.py, andaxdpto temporary files - Compares the local and GitHub SHA-256 hashes
- Prompts you when they differ
- Pulls the GitHub copy only if you confirm
For CI or automated deployment, use:
bash setup_xdp.sh --forceOr combine it with source comparison:
bash setup_xdp.sh --check-update --forceIn --force mode, the installer skips confirmation prompts and:
- Pulls the GitHub copy automatically when
--check-updatefinds a hash mismatch - Unloads any existing XDP program automatically before reinstalling
The XDP program emits per-packet events to a BPF ring buffer (pkt_ringbuf) with both DROP and ALLOW verdicts (emit_drop and emit_allow in bpf/include/common.h). A separate userspace daemon, pkt_relay.py, drains the ring buffer and broadcasts events over a Unix socket so multiple clients (TUI, ad-hoc tooling, log shippers) can subscribe without contending for the kernel ring.
# Live TUI client (subscribes, renders events + port whitelist + counters)
sudo axdp tui
# Tail the relay socket directly with socat for ad-hoc inspection
sudo socat - UNIX-CONNECT:/var/run/auto_xdp/pkt_events.sockTunable knobs in config.toml:
[ringbuf]
# Unix socket used by pkt_relay.py and axdp tui.
socket_path = "/var/run/auto_xdp/pkt_events.sock"
# Relay-side history retention for clients that connect later.
retention_seconds = 300
max_events = 100000
max_history_send = 5000
# TUI-side event scrollback kept in the local client process.
tui_max_events = 500The relay runs as a separate systemd/OpenRC service, auto-xdp-relay, installed alongside xdp-port-sync. Event emission is gated by the drop_event_flags array map (bit 0); when clear, the BPF program skips ring-buffer writes entirely so an unattended relay doesn't fill the buffer.
Optional in-daemon syncer that fetches the borestad/blocklist-abuseipdb IPv4 feeds and writes them into the abuseipdb_v4 LPM trie. Source IPs that match the trie are dropped at XDP before any conntrack or whitelist evaluation (counter ABUSEIPDB_DROP, idx 34). No API key required — the lists are public.
IPv4 only: upstream does not publish IPv6 lists (SLAAC privacy churn makes long-window v6 blocklists nearly useless).
Enable in config.toml:
[abuseipdb]
# Threat-intel blocklist: drops source IPs listed in borestad/blocklist-abuseipdb.
# trusted_ips still bypass AbuseIPDB (trusted wins).
# enabled = false → maps stay empty, no traffic blocked (fail-open).
enabled = true
# Confidence-100 windows: 1d, 3d, 7d, 14d, 30d, 60d, 90d, 120d.
# Shorter window = fewer stale entries.
sources = ["s1003d"]
# Refresh interval in seconds. Minimum 60s. Recommended: 3600 (1 hour).
refresh_seconds = 3600Operational notes:
- Fail-open by design — if the GitHub fetch fails or the daemon hasn't yet run a refresh, the trie stays empty and traffic is unaffected. The kernel-side
xdp_runtime_cfg.cfg_flagsfield gates the LPM lookup so a disabled or empty map costs only one cheap ARRAY read per packet. trusted_ipsalways wins: a CIDR added viaaxdp trust addbypasses the AbuseIPDB check.- Dropped traffic is observable as
ABUSEIPDB_DROPinaxdp statsand asverdict=DROP, reason=ABUSEIPDB_DROPinaxdp tui/ the relay event stream. - Server needs HTTPS egress to
raw.githubusercontent.comfor fetches.
- IPv4 + IPv6 stateful path:
- If packet is a pure SYN and source matches
trusted_ipv4/trusted_ipv6→ insert flow key intotcp_conntrackand PASS (auto-discovery whitelist and SYN rate limit bypassed) - If packet is a pure SYN and source/port matches a TCP ACL rule → insert flow key into
tcp_conntrackand PASS (auto-discovery whitelist and SYN rate limit bypassed) - If packet is a pure SYN and destination port is in
tcp_whitelist→ insert flow key intotcp_conntrackand PASS - If ACK is set and the flow key exists in
tcp_conntrack→ PASS - If ACK is set and no conntrack entry exists → count
CNT_TCP_CT_MISSand DROP - Otherwise → DROP
- If packet is a pure SYN and source matches
- Kernel assist: a
tcegress program records host-initiated IPv4/IPv6 TCP SYN packets immediately, closing the race where a very short outbound connection could receive SYN-ACK before conntrack state existed. - Transactional reload assist: the installer and boot loader build and validate a separate map generation, pre-seed current policy and IPv4/IPv6 TCP sessions, switch XDP and
tcwithout detaching first, and restore the previous generation if a later interface ortcstep fails.
Structural validity is checked before conntrack lookup and before the RST fast-path. Each violation increments a dedicated counter in pkt_counters.
| Check | DROP condition |
|---|---|
| Invalid data offset | doff < 5 or doff > 15, or declared header extends past packet end |
| Port zero | src port == 0 or dst port == 0 |
| NULL scan | All control bits zero |
| SYN+FIN | Both bits set simultaneously |
| SYN+RST | Both bits set simultaneously |
| RST+FIN | Both bits set simultaneously |
| XMAS scan | FIN+URG+PSH all set |
The conntrack path contains an RST fast-path that evicts the conntrack entry and immediately passes the packet to the kernel (so the kernel can deliver ECONNRESET to the application). This is the correct behavior for a legitimate RST.
RFC 793 §3.4 defines RST processing only for structurally valid packets — valid doff, valid ports, and no contradictory flag combinations. A packet with RST set alongside SYN or FIN, or with doff < 5, is not a legitimate RST: it cannot have originated from any RFC 793-conforming implementation. Letting it reach the RST fast-path would:
- Silently evict conntrack state for an active connection — a trivially exploitable denial-of-service: an attacker sends a single spoofed RST+SYN to tear down any tracked session without completing the SYN handshake.
- Forward a structurally invalid packet to the kernel — the kernel may discard it, but the conntrack slot is already gone.
Running the structural check first ensures that only RFC 793-conforming packets reach RST handling. The cost is one additional inline function call per TCP packet, which the BPF verifier eliminates entirely via __always_inline.
- IPv4 stateful path:
- If the inbound flow key exists in
udp_conntrack→ PASS - If destination port is not in
udp_whitelist→ DROP - If source IPv4 address/prefix matches
trusted_ipv4or a UDP ACL rule → PASS (rate limits and port handler bypassed; whitelist already matched) - If destination port is in
udp_whitelistand rate limits pass → PASS - Otherwise → DROP
- If the inbound flow key exists in
- IPv6 stateful path:
- If the inbound flow key exists in
udp_conntrack→ PASS - If destination port is not in
udp_whitelist→ DROP - If source IPv6 address/prefix matches
trusted_ipv6or a UDP ACL rule → PASS (rate limits and port handler bypassed; whitelist already matched) - If destination port is in
udp_whitelistand rate limits pass → PASS - Otherwise → DROP
- If the inbound flow key exists in
- Trusted source priority: in XDP mode, trusted TCP sources are an emergency/admin bypass for pure SYN admission and skip the auto-discovered TCP whitelist and SYN limits. For UDP, trusted sources do not open closed ports; they only bypass UDP rate limits and port handlers after the destination port is already whitelisted. Fragment drops and malformed-packet checks still apply.
- Userspace assist: trusted IPv4/IPv6 source addresses and CIDR ranges are synced into
trusted_ipv4/trusted_ipv6LPM trie maps by the daemon. Thenftablesfallback maintains equivalenttrusted_v4/trusted_v6sets and accepts trusted sources before port checks.
Traverses IPv6 extension headers up to 6 levels deep to locate the transport protocol and prevent crafted-header bypass attacks. This logic now exists on both the XDP ingress path and the tc egress tracker, so IPv6 reply-state tracking is not limited to the simplest nexthdr cases. Non-initial IPv6 fragments are explicitly counted and dropped before the transport parser, so they cannot slip through on a failed bounds check.
sudo axdp uninstallThe command stops and disables both services, detaches XDP from every configured interface, removes only Auto XDP's fixed tc filter (pref 49152 handle 1) without deleting the shared clsact qdisc, deletes the nftables fallback table, clears the live/candidate/rollback BPF pin generations, and removes all installed runtime files, service definitions, configuration, and state. If network cleanup cannot be verified, it keeps the runtime files so the uninstall can be retried safely.
If /etc/auto_xdp/auto_xdp.env has already been removed, provide the protected interfaces explicitly:
sudo axdp uninstall eth0 eth1This benchmark simulates a volumetric UDP flood attack. We used a high-performance AMD EPYC™ 7Y43 server as the "Attacker" to stress-test a 1 vCPU AMD Ryzen 9 3900X instance protected by Auto XDP.
- 🇭🇰 Attacker: AMD EPYC™ 7Y43 @ 2.55GHz (Generating ~367k PPS / 188 Mbps)
- 🇺🇸 Target (Receiver): AMD Ryzen 9 3900X @ 2.0GHz (1 vCPU, 1GB RAM)
- Tool:
pktgen(Linux Kernel Packet Generator) - Attacker and target connected over public internet
| Metric | Auto XDP OFF | Auto XDP ON | Improvement |
|---|---|---|---|
| Softirq (si) CPU Usage | 85.9% | 3.0% | ~28x Reduction |
| System Responsiveness | Extremely Laggy | Smooth | Significant |
| Packet Handling | Processed by Kernel Stack | Dropped at Driver Level | - |
When XDP is off, the kernel networking stack processes every incoming packet, consuming nearly all CPU via soft interrupts. With XDP on, packets are dropped at the NIC driver level before reaching the stack — the same 367k PPS flood only uses 3% CPU, and the machine stays fully responsive.
XDP OFF — softirq at 85.9% under flood:
XDP ON — same flood, CPU drops to 3.0%:
XDP ON — before attack:
XDP ON — after attack:
# Load the kernel module
modprobe pktgen
# Configure the device (replace enp3s0 with your interface name)
PGDEV=/proc/net/pktgen/INTERFACE
echo "rem_device_all" > /proc/net/pktgen/kpktgend_0
echo "add_device INTERFACE" > /proc/net/pktgen/kpktgend_0
# Set attack parameters
echo "count 10000000" > $PGDEV # Send 10 million packets
echo "pkt_size 64" > $PGDEV # Small packets put more stress on the CPU
echo "dst TARGET_IP" > $PGDEV # Target IP
echo "dst_mac TARGET_MAC" > $PGDEV # Target MAC
echo "clone_skb 100" > $PGDEV # Speed up packet generationContributions are welcome! Please read our Contributing Guide for details on our process for submitting pull requests and how to set up your development environment.
If you have a bug fix, performance improvement, or new feature in mind:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-improvement) - Commit your changes
- Open a pull request
For bugs or questions, please open an issue.
Previously, TCP SYN-rate / SYN-aggregate-rate / per-source ESTABLISHED-cap
controls only applied to ports listed in [rate_limits.syn_by_proc] /
[rate_limits.syn_by_service] (and the parallel tcp_conn_by_* tables);
all other ports ran unprotected.
Now every auto-discovered TCP port receives baseline protection from five layers:
| Layer | Key | Normal default | Strict default (sensitive procs) |
|---|---|---|---|
| L1 SYN rate (per-source) | (src/prefix) | 100 SYN/s | 5 SYN/s |
| L2 SYN aggregate rate (per-prefix) | (prefix, port) | 1000 SYN/s | 50 SYN/s |
| L3 Per-source ESTABLISHED cap | (src/32, port) | 50 | 5 |
| L4 Per-prefix ESTABLISHED cap | (prefix, port) | 200 | 20 |
| L5 Per-port total ESTABLISHED cap | (port) | 5000 | 200 |
"Sensitive" = process or service is in [rate_limits.syn_by_proc] /
syn_by_service with rate ≤ sensitive_port_threshold (default 5).
This covers SSH, databases, RDP, telnet.
The shipped [rate_limits].source_cidr_v4 defaults to /24 so per-prefix
counters (L2 and L4) cover /24-scale aggregation out of the box.
Structure Design:
Rules & Features:
- https://github.com/danger-dream/ebpf-firewall
- https://github.com/R00tS3c/XDP-eBPF-Anti-DDoS-Firewall
- https://github.com/Outfluencer/Minecraft-XDP-eBPF
Development workflow: Auto XDP is developed with AI-assisted implementation. All architecture decisions, specs, and code review are done by me; every change is verified against the test suite and CI across supported distros before merging.
MPL 2.0 © 2026 Yunheng Liu



