Data plane
src/bpf/voidgate.bpf.c
GPL-2.0-only XDP program. Verdicts are XDP_PASS or XDP_DROP only. No AF_XDP, no redirect, no userspace packet path.
01 / architecture
A multi-layer XDP shield. Userspace owns state. The program reads cfg.armed and, when armed, enforces a prefix-tree drop list. Thresholds do not live in BPF. Default verdict is XDP_PASS.
IDLE. XDP stays attached. rx_* then XDP_PASS. No Ethernet parse, no LPM, no host or remote maps. Arm on wake_pps / wake_mbps or ctl.
Allow LPM, local-side TCP allow_ports, DHCP, IPv6 NDP 133–137 with hop limit 255, fe80::/10, ff02::/16. Userspace refuses a drop CIDR that covers local_* or allow_*.
Drop LPM. A remote over threshold_pps / threshold_mbps becomes /32 or /128. Manual voidgatectl drop is this layer and does not expire.
aggregate_k dropped hosts in a /24 or /64 install that prefix. Policy and aggregate live ban_time seconds. IPv4 and IPv6 together, always.
Data plane
src/bpf/voidgate.bpf.cGPL-2.0-only XDP program. Verdicts are XDP_PASS or XDP_DROP only. No AF_XDP, no redirect, no userspace packet path.
Control plane
src/voidgate.c + policy.cWatches coarse rx rates, arms the gate, walks remote maps, and writes CIDRs into LPM drop tries. Apache-2.0.
The cheap path is the product. On every packet the program bumps metrics.rx_pkts / rx_bytes, loads cfg.armed, and if it is zero returns XDP_PASS. It must not parse Ethernet, must not LPM, and must not touch host or remote maps.
Whitelist checks run before the drop tree. Default verdict is still XDP_PASS.
parse_err. If the source and destination addresses are still readable (a bad IPv4 IHL or version, a truncated IPv6 extension), the drop tree still applies but no port exemption does; only a header too short to hold the addresses, or an IPv6 header of another version, is passed unjudged.fe80::/10, ff02::/16, and ICMPv6 NDP types 133–137 with hop limit 255 skip the drop tree (routed traffic arrives below 255, so an off-link flood cannot use the exemption). IPv4 has no extra hard-pass here; link-local v4 is an allow-list entry.allow_v4 / allow_v6 skips the drop tree.dport and dest in local_*, or sport and src in local_*. A remote source port of 22 is not a whitelist hit.drop_v4 / drop_v6 returns XDP_DROP.host_* and remote_*; outbound from a local src updates host_*.LPM lookup keys use prefixlen 32 or 128 for a host query. LPM_TRIE maps are created with BPF_F_NO_PREALLOC.
| Map | Type | Written when | Notes |
|---|---|---|---|
| cfg | ARRAY[1] | userspace | armed, allow_ports |
| metrics | PERCPU_ARRAY[1] | always (rx); armed (dropped, …) | userspace sums CPUs |
| drop_v4 / drop_v6 | LPM_TRIE | ACTIVE | empty in IDLE after disarm |
| allow_v4 / allow_v6 | LPM_TRIE | start / reload | metadata, localhost, link-local by default |
| local_v4 / local_v6 | LPM_TRIE | start / reload | this VM’s CIDRs |
| host_v4 / host_v6 | PERCPU_HASH | armed | local IPs |
| remote_v4 / remote_v6 | LRU_PERCPU_HASH | armed | attack sources; not flushed on disarm |
Idle
cfg.armed = 0
idle_poll_mswake_pps / wake_mbps exceeded, or ctl arm / drop
clear_seconds and drop tree empty, or ctl disarm
Active
cfg.armed = 1
/32 or /128/24 or /64In ACTIVE, userspace walks remotes. A source over threshold_pps or threshold_mbps is inserted as /32 or /128 (reason=policy). If at least aggregate_k dropped hosts sit in the same /24 or /64, that prefix is inserted too (reason=aggregate). Policy and aggregate drops expire after ban_time. Manual drops do not. A timed drop (drop <cidr> ttl=<sec>, reason=4, from OpenResty) expires after its own ttl and is not counted for aggregation, so a few web bans behind one NAT never become a /24.
ACTIVE returns to IDLE when the NIC has been quiet for clear_seconds and the drop tree is empty. voidgatectl drop forces ACTIVE. voidgatectl disarm flushes drops and returns IDLE.
local_* or allow_*.