XDP DDoS shield · one Linux VM

Drop floods at the NIC. Stay out of the way until then.

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.

  • Linux 5.8+ · BTF
  • IPv4 + IPv6
  • XDP_PASS or XDP_DROP only
  • Apache-2.0 · BPF GPL-2.0
quickstart
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

What happens to each packet

cfg.armed
0
rx_pkts
0
passed
0
dropped
0
no idle: rx++ then XDP_PASS · no parse, no lookups hit allow LPM · local allow_ports · DHCP · IPv6 NDP match userspace per-source counters installs yes miss miss NIC rx++metrics.rx_* armed? parseeth · vlan · ip allowed?never-drop rules drop LPM? count smart policyidentifies attackers XDP_PASS XDP_DROP no hit match userspace yes miss miss NIC rx++metrics.rx_* armed? parseeth · vlan · ip allowed? drop LPM? count smart policy XDP_PASS XDP_DROP
  • client traffic
  • flood source
  • SSH · NDP · metadata

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.

    Why voidgate

    01 / problem → approach

    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.

    The approach

    Idle cheap, then cut in-kernel

    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

    Deliberately small

    • No AF_XDP or redirect to userspace
    • No NetFlow, sFlow, pcap, AF_PACKET
    • No BGP
    • No promiscuous mode in v1

    Four layers, one program

    02 / shield

    Watch first. Keep the VM reachable. Cut attacker hosts. Widen the prefix when the cluster is dense.

    1. 01 · Watch

      Count and pass

      In idle, only rx_*, then XDP_PASS. No parse, no LPM. Arms on wake_pps / wake_mbps or voidgatectl arm.

    2. 02 · Keep

      Never drop the VM

      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.

    3. 03 · Cut

      Drop the hosts

      A remote over threshold_pps / threshold_mbps goes in as /32 or /128. voidgatectl drop adds to this layer and never expires.

    4. 04 · Widen

      Drop the cluster

      Once aggregate_k dropped hosts share a /24 or /64, install the whole prefix. Policy and aggregate drops expire after ban_time.

    Idle ↔ Active

    03 / state machine

    Idle

    cfg.armed = 0

    • XDP attached, coarse rx counters only
    • No drop tree, no per-source maps
    • Userspace polls every idle_poll_ms
    arm wake_pps / wake_mbps exceeded, or ctl arm / drop
    disarm quiet for clear_seconds and drop tree empty, or ctl disarm

    Active

    cfg.armed = 1

    • Parse IPv4/IPv6, whitelist, drop LPM
    • Top remotes → /32 or /128
    • Dense clusters → /24 or /64

    Operate it

    04 / voidgatectl
    /run/voidgate.sock
    $ 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.

    status
    state, rates, drop count
    stats
    raw XDP counters
    drops
    prefixes, reason, age
    arm / disarm
    force a state
    drop <cidr>
    manual, never expires
    drop <cidr> ttl=<s>
    timed, for OpenResty bans
    undrop <cidr>
    lift one prefix
    reload
    re-read allow/local/ports
    :9105/metrics
    Prometheus, 127.0.0.1

    Commands and config reference →

    Is it for you?

    05 / fit & faq

    Good fit

    • A single Linux VM (5.8+, BTF) with a public NIC
    • You want automatic in-kernel drops when a flood hits, and a near-free fast path the rest of the time
    • A plain-text control socket and Prometheus are enough tooling

    Look elsewhere

    • Scrubbing for a whole network edge, or BGP / flowspec
    • Floods that saturate the uplink before packets reach the VM
    • HTTP-layer filtering, packet capture, or flow export
    Can it lock me out over SSH?

    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.

    What does it cost when nothing is happening?

    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.

    Does it break cloud metadata, DHCP, or IPv6 neighbor discovery?

    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.

    It dropped a host it shouldn't have. Now what?

    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.

    Does it need a special NIC driver?

    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.

    Put it on the NIC and forget it's there.

    One daemon, one config file, one systemd unit.