AA Guard monitors ladderlog input and dispatches server commands with precision. A lightweight, self-contained solution, no external PHP packages, designed for continuous pipe operation.
Player joins → Guard reads event → Guard queries IP at ipinfo.io → Guard resolves network and country → Guard emits join notification (optional) → Guard matches network against regex rules → Guard executes action templates if matched.
- Non-blocking I/O: Reads
STDINline-by-line usingstream_selectfor efficient event handling. - Clean output: Writes commands to
STDOUTand flushes immediately; logs toSTDERRwith severity (DEBUG,WARN,ERROR). - Event handling: Processes
PLAYER_ENTERED_GRID,PLAYER_ENTERED_SPECTATOR,PLAYER_RENAMED,PLAYER_LEFT, andINVALID_COMMANDladderlog events, including arguments containing spaces. - IP validation: Validates IPv4 format before processing; rejects private and reserved ranges.
- GeoIP lookup: Queries ipinfo.io for network name, country code, country name, and IANA timezone; supports Bearer token authentication.
- Intelligent caching: In-memory IP cache (
cacheTtlSeconds) minimises redundant API calls. - Rate limiting: Local per-minute rate limiting for API calls; respects ipinfo.io backoff signals.
- Retry mechanism: Configurable retry queue with custom delays (
retry.delaysMs) for resilience under load. - Action deduplication: Reduces repeated enforcement using configurable dedupe window (
dedupeWindowSeconds). - Regex matching: Compares network names against user-defined rules (
rules[].pattern) with validation at startup. - Action templates: Supports lifecycle hooks:
- Startup actions (
actions.onStartup) - Join notifications (
actions.onConnect, custom message viaactions.onConnectMessage) - Rule match responses (
actions.onMatch) - Periodic metrics reports (
actions.onMetrics) - Debug forwarding (
actions.onDebug, whendebug=true)
- Startup actions (
- Admin notifications: Guarantees per-admin join messages even if the configuration lacks
{{admin}}template. Only sent to admins currently present (online). - Comprehensive metrics: Tracks bans, lookups, cache hits, live cache size, API errors, rate-limited events, invalid IPs, and runtime.
- Metrics on demand: Periodic reports (
metricsIntervalSeconds,0to disable) or manual trigger viaSIGUSR1. - Remote control: Supports admin/mod remote commands such as
/guard metrics,/guard players, and/guard reload, replying only to the requesting user. - Live config reload: Reloads
rules,actions,admins, retry, cache/dedupe, metrics interval, and debug settings from disk without restarting, via/guard reloadorSIGHUP. - Online player register: Records
player_id,player_name,player_country, andplayer_networkon join; updates tracked identity onPLAYER_RENAMED; removes player from list onPLAYER_LEFT. - Graceful lifecycle: Responds to
SIGTERMandSIGINT; stops cleanly whenSTDINcloses.
- PHP 8.1 or later
- Outbound HTTPS access to ipinfo.io
- Armagetron server with
ladderlog.txtsupport (version at least 0.2.8-sty or 0.4)
Typical server pipeline:
tail -fn0 -s0.01 /path/to/commands_file | $armagetron-dedicated | tee -a /path/to/console_logWith default configuration:
tail -fn0 -s0.01 /path/to/server_ladderlog | php /path/to/aa-guard/bin/aa_guard.php | tee -a /path/to/commands_fileWith custom configuration directory:
tail -fn0 -s0.01 /path/to/server_ladderlog | php /path/to/aa-guard/bin/aa_guard.php /path/to/config_dir | tee -a /path/to/commands_fileDetached process with authentication token:
IPINFO_TOKEN="your_token" screen -dmS aa-guard sh -c 'tail -fn0 -s0.01 /path/to/server_ladderlog | php /path/to/aa-guard/bin/aa_guard.php /path/to/config_dir | tee -a /path/to/commands_file'SIGTERM: Terminate gracefullySIGINT: Interrupt gracefullySIGUSR1: Emit metrics immediately (requirespcntlextension)SIGHUP: Reload configuration from disk immediately (requirespcntlextension); notifies every currently present admin of success or failure
AA Guard can react to INVALID_COMMAND ladderlog lines emitted by the server when players type commands such as:
/guard metrics
/guard players
/guard reload
Currently supported remote commands:
/guard metrics— emits the current guard metrics only to the requesting user/guard players— emits currently tracked online players only to the requesting user/guard reload— reloads configuration from disk and replies to the requester with the outcome (see Reloading Configuration)
Authorization rules:
- users listed in
admins - moderators/admins with
player_level >= 2(player_levelis the ladderlog-reported server permission level)
Unauthorized commands, unknown subcommands, and a bare /guard all trigger a private reply built from actions.onErrorMessage + actions.onError (see Templates and Placeholders). If either is left empty, these cases are silently ignored, matching the previous behaviour.
Configuration files reside in config/ by default:
config/general.json: Core settingsconfig/rules.json: Matching rulesconfig/actions.json: Action templates
Legacy single-file configuration remains supported if a JSON file path is provided explicitly.
| Key | Type | Required | Default | Purpose |
|---|---|---|---|---|
admins |
array | Yes | – | Administrator usernames for notifications |
retry.maxAttempts |
int | Yes | – | Maximum number of retry attempts for failed lookups |
retry.delaysMs |
int | Yes | – | Delay (ms) between each retry attempt |
cacheTtlSeconds |
int | No | 1800 |
IP cache lifetime in seconds |
dedupeWindowSeconds |
int | No | 15 |
Deduplication window for repeated matches |
ipInfoBaseUrl |
string | No | https://ipinfo.io |
Base URL for the GeoIP lookup API (ipinfo.io or a compatible endpoint) |
ipInfoTimeoutSeconds |
int | No | 2 |
API request timeout in seconds |
ipInfoRateLimitPerMinute |
int | No | 30 |
Maximum API calls per minute |
metricsIntervalSeconds |
int | No | 0 |
Periodic metrics report interval (0 = disabled) |
debug |
bool | No | false |
Enable debug logging and forwarding |
| Key | Type | Required | Purpose |
|---|---|---|---|
onStartup |
array | Yes | Commands executed once at startup, including ladderlog subscriptions such as LADDERLOG_WRITE_PLAYER_ENTERED_GRID 1, LADDERLOG_WRITE_PLAYER_ENTERED_SPECTATOR 1, LADDERLOG_WRITE_PLAYER_RENAMED 1, LADDERLOG_WRITE_PLAYER_LEFT 1, and LADDERLOG_WRITE_INVALID_COMMAND 1 |
onDebug |
array | Yes | Debug message templates (emitted when debug=true; once per admin per event) |
onConnectMessage |
string | Yes | Template for join message ({{msg}} placeholder; uses player_id, country_name, country_code, network_name) |
onConnect |
array | Yes | Action templates for each player join (may reference {{msg}}) |
onMatch |
array | Yes | Action templates when a network matches a rule |
onMetrics |
array | No | Metrics report templates (emitted periodically or on SIGUSR1) |
onErrorMessage |
array | No | Pool of flavor-quote strings; one is picked at random per unhandled /guard invocation |
onError |
array | No | Action templates delivering the picked onErrorMessage quote (may reference {{msg}}, {{player_id}}) |
An array of rule objects; each item contains:
| Field | Type | Purpose |
|---|---|---|
name |
string | Human-readable rule identifier |
pattern |
string | PCRE-compatible regex for network name matching |
Rendered before onConnect templates; generates the {{msg}} placeholder.
Available placeholders:
{{player_id}}– Player identifier{{country_name}}– Full country name{{country_code}}– ISO 3166-1 alpha-2 code{{network_name}}– ASN name from ipinfo.io{{time}}– Player's local time (HH:MM), derived from the IANA timezone reported by ipinfo.io for their IP;unknownif the timezone is unavailable or invalid
Example:
"{{player_id}} is connecting from {{country_name}} ({{country_code}}), network: {{network_name}}, time={{time}}."
Executed for each player join.
Available placeholders:
{{msg}}– Pre-rendered join message (fromonConnectMessage){{player_id}}– Player identifier{{country_name}}– Country name (full){{country_code}}– Country code (ISO 3166-1 alpha-2){{network_name}}– Network/ASN name{{time}}– Player's local time (HH:MM), derived from the IANA timezone reported by ipinfo.io for their IP;unknownif the timezone is unavailable or invalid{{admin}}– Admin username (only in admin-targeted templates)
Behaviour:
- Templates with
{{admin}}are emitted once per admin currently present in the tracked online player list. Absent admins do not receiveonConnectoutput. - Templates without
{{admin}}are emitted once per join. - If no admin-targeted template is present, the guard automatically sends:
PLAYER_MESSAGE {{admin}} "{{msg}}"to each present admin.
Executed when a network matches a rule.
Available placeholders:
{{player_id}}– Player identifier{{display_name}}– Player display name{{ip}}– IPv4 address{{country}}– Country code{{network_name}}– Network/ASN name{{rule_name}}– Matched rule name
Emitted periodically, on demand (SIGUSR1), or in response to /guard metrics.
For periodic and SIGUSR1 reports, templates are emitted only to admins currently present in the tracked online player list. Absent admins do not receive onMetrics output.
Available placeholders:
{{admin}}– Admin username{{bans}}– Total enforcement actions{{lookups}}– Total IP lookups{{cache_hits}}– Cache hits{{cache_size}}– Current in-memory cache entries{{api_errors}}– API errors{{rate_limited}}– Rate limit events{{invalid_ips}}– Invalid IP addresses rejected{{last_action_who}}– Last enforced player identifier{{last_action_why}}– Last enforcement reason (matched rule name){{runtime}}– Formatted runtime duration
last_action_who and last_action_why are available before runtime in the default metrics template.
Sent privately to the requester when a /guard invocation is unhandled: no subcommand given, an unknown subcommand, or the requester is not authorized.
onErrorMessage is a pool of quote strings; one is chosen at random per event and rendered into the {{msg}} placeholder for onError templates.
Available placeholders:
{{msg}}– Randomly chosen quote (fromonErrorMessage){{player_id}}– Identifier of the player who issued the command
Behaviour:
- Always targets only the requester (never broadcast to admins).
- No-op if
onErrorMessageoronErroris empty (matches pre-existing silent behaviour).
Each tracked online player is sent as a dedicated PLAYER_MESSAGE line to the requester.
PLAYER_RENAMED events move tracked entry from old player_id to new player_id and update player_name from ladderlog screen name field.
Fields per line:
player_idplayer_nameplayer_countryplayer_networktime– Player's local time (HH:MM), derived from the IANA timezone reported by ipinfo.io for their IP at join time;unknownif the timezone is unavailable or invalid
If no players are tracked, guard replies with No tracked players online.
A single PLAYER_MESSAGE reply is sent to the requester (or to every present admin for SIGHUP):
- Success:
Configuration reloaded (<N> rules). - Failure:
Config reload failed: <error message>(e.g. missing field, invalid JSON, invalid regex)
See Reloading Configuration for what is and isn't reloaded.
Debug logging templates (emitted only when debug=true).
Placeholders are optional; typically static commands.
{
"actions": {
"onStartup": [
"CONSOLE_MESSAGE 0xff0000>> 0x888888[GUARD] 0xffffff AA guard started",
"LADDERLOG_WRITE_PLAYER_ENTERED_GRID 1",
"LADDERLOG_WRITE_PLAYER_LEFT 1",
"LADDERLOG_WRITE_INVALID_COMMAND 1"
],
"onDebug": [
"PLAYER_MESSAGE {{admin}} \"0xff0000>> 0x888888[GUARD] 0xffffff [{{level}}] {{msg}}\""
],
"onConnectMessage": "{{player_id}} is connecting from {{country_name}} ({{country_code}}), network: {{network_name}}, time={{time}}.",
"onConnect": [
"# CONSOLE_MESSAGE 0xff0000>> 0x888888[GUARD] 0xffffff{{player_id}} is connecting from {{country_name}} ({{country_code}}).",
"PLAYER_MESSAGE {{admin}} \"0xff0000>> 0x888888[GUARD] 0xffffff {{msg}}\""
],
"onMatch": [
"KICK {{player_id}} Your network is banned.",
"CONSOLE_MESSAGE 0xff0000>> 0x888888[GUARD] 0xffffff {{player_id}} was kicked because {{rule_name}} networks are banned."
],
"onMetrics": [
"PLAYER_MESSAGE {{admin}} \"0xff0000>> 0x888888[GUARD] 0xffffff bans={{bans}} lookups={{lookups}} cacheHits={{cache_hits}} cacheSize={{cache_size}} apiErrors={{api_errors}} rateLimited={{rate_limited}} invalidIps={{invalid_ips}} lastActionWho={{last_action_who}} lastActionWhy={{last_action_why}} runtime={{runtime}}\""
]
},
"admins": [
"admin_1",
"admin_2"
],
"rules": [
{
"name": "vpn",
"pattern": "/vpn/i"
}
],
"retry": {
"maxAttempts": 3,
"delaysMs": [250, 750, 1500]
},
"cacheTtlSeconds": 604800,
"dedupeWindowSeconds": 15,
"ipInfoBaseUrl": "https://ipinfo.io",
"ipInfoTimeoutSeconds": 2,
"ipInfoRateLimitPerMinute": 30,
"metricsIntervalSeconds": 300,
"debug": false
}<player_id> is connecting from <country_name> (<country_code>), network: <network_name>, time=<HH:MM>.
<HH:MM> reflects the player's own local time (from their IP's IANA timezone via ipinfo.io). If the timezone can't be determined, <HH:MM> is unknown — the guard never substitutes its own server clock.
PLAYER_ENTERED_GRID player_1 192.168.51.42 Player 1
PLAYER_ENTERED_SPECTATOR player_2 192.168.51.43 Player 2
PLAYER_RENAMED player_1 player_1@clan 192.168.51.42 1 Player 1
PLAYER_LEFT player_1 192.168.51.42 Player 1
INVALID_COMMAND guard admin_1 192.168.51.42 2 metrics
INVALID_COMMAND guard admin_1 192.168.51.42 2 players
INVALID_COMMAND guard admin_1 192.168.51.42 2 reload
Configuration can be reloaded from disk without restarting the process, in two ways:
/guard reload— an authorized player (configured admin, orplayer_level >= 2) triggers a reload; only the requester is notified of the outcome.SIGHUP— e.g.kill -HUP <pid>; reloads and notifies every currently present admin.
Both paths re-run the same config validation used at startup (missing fields, invalid JSON, invalid regex, etc.). If validation fails, the guard logs the error, replies with Config reload failed: <error>, and keeps running on the last-known-good configuration — it never exits or drops the currently loaded rules/actions because of a bad reload.
What reloads: rules, all actions templates, admins, retry.maxAttempts, retry.delaysMs, cacheTtlSeconds, dedupeWindowSeconds, metricsIntervalSeconds, debug.
Known limitation: ipInfoTimeoutSeconds and ipInfoRateLimitPerMinute are consumed once when the IP lookup client is constructed at startup and are not picked up by reload; changing either value requires a process restart.
- The configuration loader exits with code
2if a required field is missing or invalid at startup; the same validation errors during a/guard reloadorSIGHUPreload are reported without exiting (see Reloading Configuration). - Delivered signals (
SIGTERM,SIGINT,SIGUSR1,SIGHUP) interrupt the blockingstream_selectcall (EINTR); the guard treats this as expected and simply retries the select on the next loop iteration instead of exiting. - IP lookups reject private and reserved ranges (
private_iperror); no internal addresses are queried. - Invalid IPv4 addresses are rejected early and counted in the
invalid_ipsmetric. - Deduplication is keyed on
playerId|ruleNameto prevent repeated enforcement. - Startup actions enable
LADDERLOG_WRITE_PLAYER_ENTERED_GRID 1,LADDERLOG_WRITE_PLAYER_ENTERED_SPECTATOR 1,LADDERLOG_WRITE_PLAYER_RENAMED 1,LADDERLOG_WRITE_PLAYER_LEFT 1, andLADDERLOG_WRITE_INVALID_COMMAND 1in the default configuration. - Remote
/guard metrics,/guard players, and/guard reloadrequests are authorized for configured admins and users with level2or higher, and replies are sent only to the requester (SIGHUPreload instead notifies every present admin). - Online players are tracked by join/rename/leave events and exposed through
/guard playerswith fields:player_id,player_name,player_country,player_network,time(player's own local timeHH:MM, from ipinfo.io timezone data, orunknown). {{time}}reflects each player's own local time, computed from the IANA timezone (e.g.Europe/Warsaw) that ipinfo.io reports for their IP. If the timezone is missing or invalid,{{time}}isunknown— it never falls back to the guard process's own server clock. Available inonConnectMessage,onConnect, and the/guard playersreport.- Metrics include last enforcement details via
last_action_whoandlast_action_whybeforeruntime. - No external package managers or dependencies are required, PHP standard library only.
- Retry mechanism: Triggered by lookup failures or rate limits. Configured delays (
retry.delaysMs) apply to lookup retries; rate limiting respects ipinfo.io's backoff signals instead. - Value sanitisation: Template values like
{{player_id}}are sanitised (alphanumeric, spaces, slashes, dashes, dots, underscores, @);{{msg}}in quoted contexts uses proper shell escaping (\"and\\). - Admin-targeted actions: Templates containing
{{admin}}are emitted once per administrator; useful for debug logs, metrics, and personalised notifications.
Keep the server tidy, keep the grid fair, keep the logs forthright.


