01 / architecture

Four layers. One verdict.

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.

  1. 01

    Watch

    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.

  2. 02

    Keep

    Allow LPM, local-side TCP allow_ports, DHCP, IPv6 NDP 133–137 / fe80::/10 / ff02::/16. Userspace refuses a drop CIDR that covers local_* or allow_*.

  3. 03

    Cut

    Drop LPM. A remote over threshold_pps / threshold_mbps becomes /32 or /128. Manual voidgatectl drop is this layer and does not expire.

  4. 04

    Widen

    aggregate_k dropped hosts in a /24 or /64 install that prefix. Policy and aggregate live ban_time seconds. IPv4 and IPv6 together, always.

Two planes

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.

Control plane

src/voidgate.c + policy.c

Watches coarse rx rates, arms the gate, walks remote maps, and writes CIDRs into LPM drop tries. Apache-2.0.

Idle datapath

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.

Armed packet path

Whitelist checks run before the drop tree. Default verdict is still XDP_PASS.

  1. Parse — Ethernet, up to two VLAN tags, then IPv4 or IPv6. Non-IP is counted and passed. Truncated headers increment parse_err and still pass.
  2. IPv6 hard-passfe80::/10, ff02::/16, and ICMPv6 NDP types 133–137 skip the drop tree. IPv4 has no extra hard-pass here; link-local v4 is an allow-list entry.
  3. Allow LPM — source or destination in allow_v4 / allow_v6 skips the drop tree.
  4. TCP allow_ports — only when the local side is that port: dport and dest in local_*, or sport and src in local_*. A remote source port of 22 is not a whitelist hit.
  5. DHCP — UDP 67/68 and DHCPv6 546/547.
  6. Drop LPM — source or destination in drop_v4 / drop_v6 returns XDP_DROP.
  7. Count, then pass — inbound to a local dest updates host_* and remote_*; outbound from a local src updates host_*.

Maps

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

Policy, once a second

In 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.

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.

Do not blackhole the VM