Skip to content

Configuration Reference ​

Config file: /etc/gamefilter/gamefilter.yaml (YAML, chmod 600 — it contains the API key).

Every value below is optional — missing keys fall back to the defaults shown. After editing, apply with sudo gamefilter reload (hot, no detach) or POST /api/v1/reload.

yaml
interface: eth1
xdp_mode: auto
enabled: true
mode: dedicated
default_action: pass

tenants:
  - ip: "10.210.0.2"
    label: "Minecraft VPS"

filters:
  - name: mc-java
    protocol: tcp
    ports: ["25565"]
    validator: mc_java
    min_size: 7
    max_size: 800
    admission_ttl_sec: 300
    ban_sec: 300
    max_failures: 8

whitelist: []
blacklist: []

sync:
  openshield:
    enabled: false
    mode: api
    url: "http://127.0.0.1:9100"
    api_key: ""
    file: /var/lib/openshield/lists.json
    interval_sec: 15

api:
  enabled: true
  listen: 127.0.0.1:9300
  api_key: ""
  rate_limit_per_sec: 10
  whitelist: []

Top level ​

interface ​

Typestring
Default"eth1"

The interface the XDP program attaches to. This can be a private NIC (e.g. eth1) or a VM bridge (e.g. vmbr0 / br0) facing your game servers / guest VPSes, behind OpenShield on the physical uplink.

When to change: always, at install — the installer auto-detects from your default route / bridges and asks.

Common mistakes: attaching to the public interface where OpenShield is already running (Linux only allows one native XDP program per interface).

xdp_mode ​

Typestring
Default"auto"

Reserved. The loader currently always behaves as auto: it attempts a native (driver) attach first and falls back to generic (SKB) mode if the NIC driver rejects it. The mode actually used is recorded in /var/lib/gamefilter/state.json and shown by gamefilter status.

mode ​

Typestring ("dedicated" | "vps")
Default"vps"

The operating mode for destination IP matching:

  • dedicated (Multi-Tenant / Proxmox): ONLY destination IPs listed under tenants: (or enrolled via REST API) are filtered. All other destination IPs on the bridge bypass filtering instantly with XDP_PASS (100% transparent and safe for web/DB/general VPSes).
  • vps (Single-Server / Standalone): Filters all incoming traffic on the interface regardless of destination IP.

When to change: use dedicated on hypervisors / dedicated servers hosting multiple guest VPSes with different workloads. Use vps on a dedicated game server box or single-purpose VPS.

tenants ​

Typelist of { ip: string, label?: string }
Default[]

The list of enrolled tenant destination IPs in mode: dedicated. Each entry declares an IP that should be protected by the filter rules. Can be updated live via the REST API (/api/v1/tenants) or gamefilter reload.

enabled ​

Typeboolean
Defaulttrue

Global kill switch, mirrored into the kernel CONFIG map. When false, the XDP program passes every packet immediately — the attachment stays in place but filters nothing.

When to change: emergency "stop filtering without detaching" — can be flipped live via POST /api/v1/config with {"enabled": false}.

default_action ​

Typestring: drop | pass
Default"drop"

What happens to traffic whose destination port no filter owns:

  • drop — only traffic that matches a filter (or the whitelist) passes. Recommended for a dedicated game NIC.
  • pass — filters protect their ports; everything else flows through.

Common mistakes: drop on an interface that also carries SSH/monitoring/other services without filters for them — you will lock those services out. Anything else (typos like "deny") is rejected by POST /api/v1/config and treated as unrecognized on load.


filters: — per-filter fields ​

A list of up to 64 filters (MAX_RULES). Each filter owns a set of ports for one protocol and validates first packets against it.

yaml
filters:
  - name: mc-bedrock
    protocol: udp
    ports: ["19132"]
    validator: raknet
    min_size: 25
    max_size: 1500
    admission_ttl_sec: 300
    ban_sec: 300
    max_failures: 8

filters[].name ​

Typestring
Default""

Human label used in gamefilter status, the API, and admissions listings. Not used by the kernel — position in the list determines the rule slot.

Common mistakes: reordering the list between reloads and confusing yourself — per-filter stats are keyed by list position (slot), not name.

filters[].protocol ​

Typestring: tcp | udp
Default"udp"

L4 protocol this filter owns. Port ownership is per-protocol: TCP 25565 and UDP 25565 are independent entries in the port table, so one filter never shadows the other protocol's traffic.

Common mistakes: pointing a TCP validator at protocol: udp (or vice versa) — load fails fast with unknown protocol only for values other than tcp/udp; a wrong-but-valid choice loads fine and then drops everything on that port.

filters[].ports ​

Typelist of strings
Default[]

Ports this filter owns. Three forms, freely mixed:

yaml
ports: ["25565"]                # single
ports: ["27015", "27016"]       # list
ports: ["27015-27050"]          # inclusive range

Rules: ports 1–65535, ranges must be ascending and at most 4096 ports wide.

Common mistakes: quoting is required ("25565", not 25565 — the field is a list of strings); overlapping two filters on the same port+protocol (the later filter wins the port — the earlier one silently loses it).

filters[].validator ​

Typestring (validator id)
Default"none"

Which protocol proof the first data packet from an unadmitted source must pass. One of:

ValidatorL4First-packet proofRecommended bounds
mc_javaTCPFull handshake frame: varint length that exactly consumes the payload, packet id 0, protocol varint, address string, port, next-state 1|2. Legacy FE 01 ping accepted.min_size: 7, max_size: 800
raknetUDPRakNet offline magic at the exact offset for unconnected ping (0x01) or open-connection-request 1/2 (0x05, 0x07).min_size: 25, max_size: 1500
fivemUDP0xFFFFFFFF + getinfo/getstatus/connect, or ENet CONNECT (low nibble 2, ≥52 bytes). UNVERIFIED upstream.min_size: 9, max_size: 1500
source_engineUDP0xFFFFFFFF + A2S type byte (T/U/V/W/i/q); T additionally requires Source Engine Query\0.min_size: 5, max_size: 1400
ssh_bannerTCPPayload begins with SSH-, 8–255 bytes (RFC 4253).min_size: 8, max_size: 255
tcp_genericTCPSize bounds + NULL/Xmas/FIN-scan flag rejection.min_size: 1, max_size: 1460
udp_genericUDPSize bounds only.min_size: 1, max_size: 1400
nonebothSize bounds only (same as the generic validators without flag checks).—

An unknown validator name fails the load/reload with unknown validator '<name>'.

When to change: widen to udp_generic/tcp_generic if a strict validator false-drops your traffic (the fivem ENet CONNECT check is marked UNVERIFIED upstream) — keep the size bounds tight when you do.

filters[].min_size ​

Typeinteger (bytes)
Default1

Minimum L4 payload size for the validating (first) packet. Payloads smaller than this fail validation.

When to change: set from the protocol's smallest legal first frame (e.g. 7 for a degenerate MC Java handshake, 25 for RakNet — the unconnected ping is exactly 33 bytes).

Common mistakes: setting it above real client traffic (e.g. proxies that coalesce or pad) — false drops; or leaving it at 1 for a strict validator, which lets tiny garbage reach the validator (harmless, it still fails the magic checks).

filters[].max_size ​

Typeinteger (bytes)
Default1500

Maximum L4 payload size. 0 disables the upper bound. Payloads larger than this fail validation.

When to change: raise for setups with jumbo frames, MTU probing (RakNet open-connection-request-1 pads up to ~1464 bytes of payload), or modded servers.

Common mistakes: setting it tighter than the protocol's MTU probes — the classic false-drop cause for Bedrock (1500 covers the 1464-byte probe).

filters[].admission_ttl_sec ​

Typeinteger (seconds)
Default300

Sliding admission window. A source that passes validation once is admitted for this long, and every subsequent packet refreshes the deadline — active players stay admitted forever; idle ones fall out after the TTL and must revalidate.

When to change: raise for long-lived protocols where revalidation is annoying (SSH uses 3600 in the example); lower for high-churn query-only ports.

filters[].ban_sec ​

Typeinteger (seconds)
Default0

Temp-ban length once a source hits max_failures failed validations inside a 60-second window. 0 = never ban, just drop.

When to change: set 0 while tuning a new filter so mistakes drop packets instead of banning players; 300–600 is the normal production range.

filters[].max_failures ​

Typeinteger
Default8

Failed validations inside a sliding 60-second window that trigger the temp-ban. Banning only happens when both ban_sec > 0 and max_failures > 0. With max_failures: 0, failure accounting is skipped entirely.

Common mistakes: setting it to 1 — a single fragmented/corner-case packet from a real client then earns a ban.


whitelist: / blacklist: ​

Typelist of IP strings (v4 or v6)
Default[]

Static lists seeded into the kernel maps at load time. Whitelisted sources bypass everything (checked first, before the blacklist); blacklisted sources are dropped permanently (config-seeded entries never expire).

These are the secondary list source — the primary source for a fleet is OpenShield sync below, and live entries managed via the CLI/API are never removed by sync.

Reload does not re-seed lists

gamefilter reload / POST /api/v1/reload hot-apply filters, port ownership, and globals — but not these lists. Changes to whitelist:/blacklist: in the YAML take effect on the next gamefilter load (or add them live with the CLI/API instead).


sync.openshield — OpenShield-XDP list sync ​

Polls OpenShield-XDP and mirrors its whitelist/blacklist into GameFilter's kernel maps. Adds and removals both propagate (each cycle diffs against the previous sync). Locally added entries (CLI/API) always win — sync never touches them.

yaml
sync:
  openshield:
    enabled: false
    mode: api
    url: "http://127.0.0.1:9100"
    api_key: ""
    file: /var/lib/openshield/lists.json
    interval_sec: 15

sync.openshield.enabled ​

Typeboolean
Defaultfalse

Master switch. The sync loop runs inside gamefilter daemon (the gamefilter-api.service unit), so sync requires the daemon to be running even if the HTTP API itself is disabled.

sync.openshield.mode ​

Typestring: api | file | maps
Default"api"

How lists are fetched:

  • api — polls {url}/api/v1/lists with Bearer auth. Expects {"whitelist": [...], "blacklist": [...]}.
  • file — reads a JSON export of the same shape from file.
  • maps — best-effort direct read of OpenShield's pinned ban maps (/sys/fs/bpf/openshield/pinned_bans_map or …/maps/pinned_bans_map). IPv4 blacklist only — OpenShield's whitelist is bloom/LPM and cannot be enumerated.

An unknown mode logs unknown sync mode each cycle and syncs nothing.

sync.openshield.url ​

Typestring (URL)
Default"http://127.0.0.1:9100"

OpenShield metrics endpoint base URL (api mode only). The path /api/v1/lists is appended; a trailing slash is handled.

sync.openshield.api_key ​

Typestring
Default""

OpenShield API key (osk_…, see openshield key) sent as a Bearer token in api mode. Masked as "***" in GET /api/v1/config responses.

sync.openshield.file ​

Typestring (path)
Default"/var/lib/openshield/lists.json"

JSON export path for file mode: {"whitelist": ["1.2.3.4", …], "blacklist": […]}.

sync.openshield.interval_sec ​

Typeinteger (seconds)
Default15

Poll interval. Values below 5 are clamped to 5.

Common mistakes: enabling maps mode and expecting whitelist entries (impossible by design); pointing api mode at GameFilter's own API port (9300) instead of OpenShield's (9100).


api: — management API ​

Everything is managed through this API (no TUI by design). It runs inside gamefilter daemon.

yaml
api:
  enabled: true
  listen: 127.0.0.1:9300
  api_key: ""
  rate_limit_per_sec: 10
  whitelist: []

api.enabled ​

Typeboolean
Defaulttrue

When false, gamefilter daemon skips the HTTP server entirely and only runs OpenShield sync (idling otherwise).

api.listen ​

Typestring (host:port)
Default"127.0.0.1:9300"

Bind address. Keep it on 127.0.0.1 unless you also set api.whitelist — the only other protections are the API key and the rate limit.

api.api_key ​

Typestring (min 8 chars)
Default""

Bearer token for every endpoint (including /health). The installer generates gf_ + 32 hex chars. The daemon refuses to start without a key of at least 8 characters. Manage it with sudo gamefilter key [set|regen]; masked as "***" in API config reads.

api.rate_limit_per_sec ​

Typeinteger
Default10

Per-source-IP token bucket: rate_limit_per_sec tokens per second, burst capacity 2× the rate. Over-limit requests get 429. 0 disables rate limiting. Checked after the key, so it only throttles authenticated clients.

api.whitelist ​

Typelist of IPs / CIDRs
Default[] (any source)

Source-IP allowlist for the API itself, checked before the key (403 on mismatch). Accepts single IPs and CIDRs, v4 and v6:

yaml
api:
  listen: 0.0.0.0:9300
  whitelist: ["10.0.0.5", "192.168.1.0/24"]

Common mistakes: exposing listen on a routable address with an empty whitelist; mixing v4 CIDRs with v6 clients (a v4 rule never matches a v6 source and vice versa).


Complete example ​

The shipped default — filters for Minecraft Java/Bedrock, Geyser, FiveM, Source engine, and SSH:

yaml
interface: eth1
xdp_mode: auto
enabled: true
default_action: drop

filters:
  - name: mc-java
    protocol: tcp
    ports: ["25565"]
    validator: mc_java
    min_size: 7
    max_size: 800
    admission_ttl_sec: 300
    ban_sec: 300
    max_failures: 8

  - name: mc-bedrock
    protocol: udp
    ports: ["19132"]
    validator: raknet
    min_size: 25
    max_size: 1500
    admission_ttl_sec: 300
    ban_sec: 300
    max_failures: 8

  - name: geyser
    protocol: udp
    ports: ["19133"]
    validator: raknet
    min_size: 25
    max_size: 1500
    admission_ttl_sec: 300
    ban_sec: 300
    max_failures: 8

  - name: fivem
    protocol: udp
    ports: ["30120"]
    validator: fivem
    min_size: 9
    max_size: 1500
    admission_ttl_sec: 600
    ban_sec: 300
    max_failures: 8

  - name: source-engine
    protocol: udp
    ports: ["27015-27050"]
    validator: source_engine
    min_size: 5
    max_size: 1400
    admission_ttl_sec: 600
    ban_sec: 300
    max_failures: 8

  - name: ssh
    protocol: tcp
    ports: ["22"]
    validator: ssh_banner
    min_size: 8
    max_size: 255
    admission_ttl_sec: 3600
    ban_sec: 600
    max_failures: 5

whitelist: []
blacklist: []

sync:
  openshield:
    enabled: false
    mode: api
    url: "http://127.0.0.1:9100"
    api_key: ""
    file: /var/lib/openshield/lists.json
    interval_sec: 15

api:
  enabled: true
  listen: 127.0.0.1:9300
  api_key: ""
  rate_limit_per_sec: 10
  whitelist: []

Generic fallbacks for non-game services (commented out in the shipped config):

yaml
  - name: tcp-generic
    protocol: tcp
    ports: ["80", "443"]
    validator: tcp_generic
    min_size: 1
    max_size: 1460
    admission_ttl_sec: 300
    ban_sec: 0
    max_failures: 8
  - name: udp-generic
    protocol: udp
    ports: ["53"]
    validator: udp_generic
    min_size: 1
    max_size: 1400
    admission_ttl_sec: 60
    ban_sec: 0
    max_failures: 8