Skip to content

Architecture Overview ​

GameFilter XDP is two programs and a shared-types crate, all Rust:

  • eBPF kernel program (gamefilter-ebpf/) — a single #[xdp] entry point written with aya-ebpf (no_std, no_main). Parses IPv4/IPv6 + TCP/UDP, runs destination IP checks (TENANTS_MAP in dedicated mode), whitelist/blacklist, port ownership, admission, and validators. Non-IP or unparseable traffic is passed — fail-open by design.
  • Userspace loader + CLI (gamefilter/, aya) — loads the object, populates the maps, attaches native-first with generic fallback, pins the link and every map, and exits. All CLI commands (status, whitelist, …) reopen the pinned maps, so they work without any resident process.
  • API daemon (gamefilter daemon, axum) — the management plane: token-authenticated, per-IP rate-limited REST API, 1s rate sampler, live tenant enrollment, plus the OpenShield sync task.
  • OpenShield sync (sync.rs) — polls OpenShield's lists (API, JSON file, or direct pinned-map read) and diffs them into the kernel maps; local CLI/API entries are marker-tagged and never touched.
  • Shared types (gamefilter-common/) — repr(C) structs that are the kernel ABI (FilterRule, AdmitKey, GlobalConfig, RuleStats, …), compiled into both sides.

Map layout ​

All maps are pinned under /sys/fs/bpf/gamefilter/maps/ at load; the attach itself is pinned as /sys/fs/bpf/gamefilter/link (an fd link — removing the pin detaches the program).

MapTypeKey → ValueNotes
TENANTS_MAPHASH (65536)dst_ip[16] → u64In mode: dedicated, destination IPs not in this map bypass filtering with XDP_PASS
FILTER_MAPARRAY (64)slot → FilterRuleThe rule table; position = slot used in stats
PORT_MAPARRAY (131072)proto_idx << 16 | port → u16proto_idx 0 = TCP, 1 = UDP. Stores rule_idx + 1 — 0 (zero-init) means "port not owned"
ADMIT_MAPLRU_HASH (65536)AdmitKey{src[16], dst[16], rule} → {admit_until_ns}Sliding admission deadline (monotonic ns). Also written with rule = 0xFFFF as an any-rule marker for fragments
FAIL_MAPLRU_HASH (65536)AdmitKey → {window_start_ns, count}Validation failures inside a 60-second window
WHITELISTHASH (65536)addr[16] → u64Checked first — full bypass. Value 1 = local, 2 = synced
BLACKLISTHASH (65536)addr[16] → u64Value 0 = permanent; otherwise monotonic-ns expiry (temp-bans)
STATSPERCPU_ARRAY (65)slot → RuleStatsOne slot per rule; slot 64 is the global entry. Userspace sums across CPUs
CONFIGARRAY (1)0 → {default_drop, enabled, mode}Global switches, rewritten on every reload

Key conventions:

  • IP keys are [u8; 16] — IPv4 in the first 4 bytes (rest zero), full IPv6 otherwise.
  • Admission is per (source, destination, rule) — being admitted for one tenant VPS does not grant access to another tenant VPS.
  • LRU for ADMIT_MAP/FAIL_MAP means flood churn evicts entries instead of failing inserts; the whitelist/blacklist/tenants are plain HASH so entries are never evicted.
  • The validator reads payload at fixed offsets with direct packet access; the dynamic-offset read (MC Java next-state byte) goes through bpf_xdp_load_bytes, which validates at runtime.

Packet pipeline ​

Admission and Ban State Machine ​

Hot Reload Mechanics ​

gamefilter reload and POST /api/v1/config (or /api/v1/reload) apply updates incrementally without a traffic blip:

  1. CONFIG map — updated in place with default_drop, enabled, and mode.
  2. FILTER_MAP — slots updated in place; trailing slots zeroed.
  3. PORT_MAP — computed diff between old rules and new rules: new port bindings are added, and only removed port bindings are reset to 0. Unaffected ports are never swept.
  4. TENANTS_MAP — synced with cfg.tenants diff.
  5. ADMIT_MAP — stale admissions are purged only for rules whose definition actually changed (ports, validator, or sizes). Unmodified rules preserve their active player admissions.