03 / operations

One line in. Text out.

voidgatectl talks to /run/voidgate.sock. Prometheus, if metrics_port > 0, binds 127.0.0.1:9105/metrics.

voidgatectl

Command Effect
status state, armed, rx_pps, rx_bps, drop count, iface
stats XDP counters: rx_pkts, passed, dropped, non_ip, map_full, parse_err
drops installed prefixes with reason and age
arm cfg.armed = 1; re-baseline remote rate snapshots
disarm flush drop_*, cfg.armed = 0, back to IDLE
drop <cidr> manual insert; forces ACTIVE. Reason 1. Not auto-expired.
drop <cidr> ttl=<sec> timed insert for OpenResty bans; forces ACTIVE. Reason 4. Lifted after ttl (1 s to one year); a re-ban only extends it. Never upgrades a manual drop and never counts toward aggregate_k.
undrop <cidr> remove one prefix from the drop tree
reload re-read the config file; replace allow/local maps and ports

Drop reasons: 1 manual, 2 policy, 3 aggregate.

Config

One key = value per line; # starts a comment. Every key is optional, and the defaults below are also what ships in configs/voidgate.conf. The commented file explains each one in more depth.

Key Default What it does reload
Interface and files
interface eth0 RX NIC to attach XDP to. -i overrides. Never lo. restart
xdp_mode auto auto tries native driver XDP, then SKB. native / skb pin one. restart
log_file /var/log/voidgate.log Append-only log, with -d only; under the unit the log is the journal. Parent directory must exist. restart
pid_file /run/voidgate.pid Locked for the daemon's life; -s stop reads it. restart
Waking up
wake_pps 2000 Aggregate NIC packets/s that arms the gate (IDLE → ACTIVE). live
wake_mbps 25 Aggregate NIC Mbit/s that arms the gate. Either wake_* arms. live
idle_poll_ms 1000 Userspace tick in ms. <= 0 becomes 1000. live
clear_seconds 30 Quiet seconds, with an empty drop tree, before ACTIVE → IDLE. live
Dropping
threshold_pps 500 Per-remote packets/s that earns a /32 or /128 drop. Keep below wake_pps. live
threshold_mbps 5 Per-remote Mbit/s, same effect. Keep below wake_mbps. live
ban_time 1900 Seconds a policy or aggregate drop lives. Manual drops never expire. live
aggregate_k 8 Dropped hosts in one /24 or /64 before the whole prefix is dropped. live
Never drop
local_networks (auto) This VM's CIDRs. Empty takes the NIC's addresses as /32 and /128. Don't list the on-link LAN prefix. live
allow_networks metadata, loopback, link-local Never-drop CIDRs. A non-empty list replaces the built-in one; it does not merge. Repeated lines add up. live
allow_ports 22 TCP ports whitelisted on the local side. CSV, at most 8. live
Capacity and metrics
remote_map_size 262144 Cap on tracked attack sources (LRU, not preallocated). restart
drop_map_size 65536 Cap on drop-tree prefixes (not preallocated). restart
metrics_port 9105 Prometheus on 127.0.0.1. 0 disables it. restart

live: voidgatectl reload applies it. restart: fixed when the daemon starts. The built-in allow list is 169.254.169.254/32, 127.0.0.0/8, ::1/128, fe80::/10, ff02::/16.

Prometheus

When metrics_port is positive, the daemon listens on localhost and serves GET /metrics:

/metrics
# TYPE voidgate_armed gauge
voidgate_armed 0
# TYPE voidgate_rx_packets_total counter
voidgate_rx_packets_total 0
# TYPE voidgate_rx_bytes_total counter
voidgate_rx_bytes_total 0
# TYPE voidgate_dropped_packets_total counter
voidgate_dropped_packets_total 0
# TYPE voidgate_map_full_total counter
voidgate_map_full_total 0
# TYPE voidgate_drop_prefixes gauge
voidgate_drop_prefixes 0
# TYPE voidgate_rx_pps gauge
voidgate_rx_pps 0

OpenResty client

voidgate is not a WAF. Let OpenResty, or a gateway built on it such as Kong or APISIX, decide who is abusive and push that address down to XDP with a timed drop. lua/resty/voidgate.lua talks to the socket over a non-blocking cosocket. Install it with sudo make install-lua (OpenResty's site lualib, or set LUA_DIR), and add the worker user to the group voidgate, which the shipped unit gives the socket to with -g voidgate, so nginx can reach it: commonly nobody for OpenResty, kong for Kong, and nobody or apisix (Docker image) for APISIX.

nginx
lua_shared_dict voidgate_ban 1m;

location / {
    limit_req zone=api burst=20 nodelay;
    limit_req_status 429;
    log_by_lua_block {
        if ngx.status == 429 then
            require("resty.voidgate").ban(ngx.var.remote_addr, 600,  -- 10m
                                          { dict = "voidgate_ban" })
        end
    }
}

ban(ip, ttl, opt) works from every phase but init_by_lua*, log included, and sends one ban per address per window. ban_now(ip, ttl) sends at once and returns the daemon's answer, in rewrite, access, content or timer context. Every drop from Lua needs a ttl: a permanent drop is voidgatectl drop's job. The other methods (status(), stats(), drops(), drop(cidr, ttl), undrop(), arm(), disarm(), reload()) run in rewrite, access, content or timer context and return nil, err on failure. stats() returns 64-bit counters as decimal strings.

ACTIVE policy, short

Each tick, remotes over threshold become /32 or /128. Dense clusters become /24 or /64. Userspace will not install a prefix that covers the VM itself or the allow list. On disarm, remote_* stays; the next arm re-baselines snapshots so leftover counters are not a phantom flood.