The problem
A flood lands on one VM
No scrubbing service upstream, no router to push flowspec to. The packets reach your NIC, and every one that climbs the network stack costs CPU the service needs.
XDP DDoS shield · one Linux VM
voidgate guards a single Linux VM. While traffic is normal, its XDP program only counts packets and passes them. When the NIC floods, it arms, finds the sources, and drops them in the kernel, before the network stack sees them. IPv4 and IPv6 together.
git clone https://github.com/seawallnetworks/voidgate.git
cd voidgate
make
sudo ./voidgate -c configs/voidgate.conf -i eth0
sudo ./voidgatectl status
Needs clang, libbpf, bpftool. Full requirements
A simulation of voidgate.bpf.c, not live telemetry. In idle only rx_* moves, even for flood sources. Under attack, count feeds a userspace policy that identifies attackers and puts them in the drop LPM. Keys: I idle, A active.
The problem
No scrubbing service upstream, no router to push flowspec to. The packets reach your NIC, and every one that climbs the network stack costs CPU the service needs.
The approach
XDP stays attached and only bumps a counter until the rate crosses wake_pps or wake_mbps. Then userspace counts sources and installs the worst of them into an LPM drop tree. Thresholds and decisions stay in userspace.
What it won't do
Watch first. Keep the VM reachable. Cut attacker hosts. Widen the prefix when the cluster is dense.
In idle, only rx_*, then XDP_PASS. No parse, no LPM. Arms on wake_pps / wake_mbps or voidgatectl arm.
Allow CIDRs, local-side TCP allow_ports, DHCP to this VM, IPv6 NDP with hop limit 255, fe80::/10, ff02::/16. All checked before the drop tree.
A remote over threshold_pps / threshold_mbps goes in as /32 or /128. voidgatectl drop adds to this layer and never expires.
Once aggregate_k dropped hosts share a /24 or /64, install the whole prefix. Policy and aggregate drops expire after ban_time.
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 /64$ sudo voidgatectl status state=idle armed=0 rx_pps=412 rx_bps=2800000 prefixes=0 iface=eth0 $ sudo voidgatectl drop 203.0.113.0/24 ok $ sudo voidgatectl status state=active armed=1 rx_pps=184200 rx_bps=980000000 prefixes=1 iface=eth0 $ sudo voidgatectl disarm ok
One line in, text out, over a Unix socket. There's also an OpenResty client and a localhost Prometheus endpoint.
TCP ports in allow_ports (default 22) are whitelisted on the local side, and whitelist checks run before the drop tree. Userspace also refuses to install a drop that covers local_networks or allow_networks. A remote source port of 22 is not a whitelist hit.
Per packet: bump the per-CPU rx_pkts / rx_bytes counters, read cfg.armed, return XDP_PASS. No Ethernet parse, no LPM lookup, no map writes. Userspace wakes once per idle_poll_ms to read the counters.
No. The built-in allow list is 169.254.169.254/32, 127.0.0.0/8, ::1/128, fe80::/10, ff02::/16. DHCP and DHCPv6 to this VM (exact port pairs) and ICMPv6 NDP types 133–137 with hop limit 255 also pass before the drop tree.
voidgatectl undrop <cidr> lifts one prefix, and voidgatectl disarm flushes them all. Policy and aggregate drops expire on their own after ban_time. For a permanent exemption, add the prefix to allow_networks and run voidgatectl reload. Reload also lifts any drop that now covers an allowed prefix.
No. xdp_mode = auto tries native driver XDP, then falls back to generic SKB mode. You can pin either one. The test suite runs on a veth pair in private namespaces.
One daemon, one config file, one systemd unit.