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_MAPin 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).
| Map | Type | Key → Value | Notes |
|---|---|---|---|
TENANTS_MAP | HASH (65536) | dst_ip[16] → u64 | In mode: dedicated, destination IPs not in this map bypass filtering with XDP_PASS |
FILTER_MAP | ARRAY (64) | slot → FilterRule | The rule table; position = slot used in stats |
PORT_MAP | ARRAY (131072) | proto_idx << 16 | port → u16 | proto_idx 0 = TCP, 1 = UDP. Stores rule_idx + 1 — 0 (zero-init) means "port not owned" |
ADMIT_MAP | LRU_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_MAP | LRU_HASH (65536) | AdmitKey → {window_start_ns, count} | Validation failures inside a 60-second window |
WHITELIST | HASH (65536) | addr[16] → u64 | Checked first — full bypass. Value 1 = local, 2 = synced |
BLACKLIST | HASH (65536) | addr[16] → u64 | Value 0 = permanent; otherwise monotonic-ns expiry (temp-bans) |
STATS | PERCPU_ARRAY (65) | slot → RuleStats | One slot per rule; slot 64 is the global entry. Userspace sums across CPUs |
CONFIG | ARRAY (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_MAPmeans 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:
CONFIGmap — updated in place withdefault_drop,enabled, andmode.FILTER_MAP— slots updated in place; trailing slots zeroed.PORT_MAP— computed diff between old rules and new rules: new port bindings are added, and only removed port bindings are reset to0. Unaffected ports are never swept.TENANTS_MAP— synced withcfg.tenantsdiff.ADMIT_MAP— stale admissions are purged only for rules whose definition actually changed (ports, validator, or sizes). Unmodified rules preserve their active player admissions.
