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, and country name; 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. - 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 metricsand/guard players, replying only to the requesting user. - 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)
AA Guard can react to INVALID_COMMAND ladderlog lines emitted by the server when players type commands such as:
/guard metrics
/guard players
Currently supported remote command:
/guard metrics— emits the current guard metrics only to the requesting user/guard players— emits currently tracked online players only to the requesting user
Authorization rules:
- users listed in
admins - moderators/admins with
player_level >= 2(player_levelis the ladderlog-reported server permission level)
Unauthorized or unknown remote commands are silently ignored for now.
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 |
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) |
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
Example:
"{{player_id}} is connecting from {{country_name}} ({{country_code}}), network: {{network_name}}."
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{{admin}}– Admin username (only in admin-targeted templates)
Behaviour:
- Templates with
{{admin}}are emitted once per admin in theadminsarray. - Templates without
{{admin}}are emitted once per join. - If no admin-targeted template is present, the guard automatically sends:
PLAYER_MESSAGE {{admin}} "{{msg}}"
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.
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_network
If no players are tracked, guard replies with No tracked players online.
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}}.",
"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,
"ipInfoTimeoutSeconds": 2,
"ipInfoRateLimitPerMinute": 30,
"metricsIntervalSeconds": 300,
"debug": false
}<player_id> is connecting from <country_name> (<country_code>), network: <network_name>.
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
- The configuration loader exits with code
2if a required field is missing or invalid. - The guard exits with code
1ifstream_selectfails (critical I/O error). - 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 metricsand/guard playersrequests are authorized for configured admins and users with level2or higher, and replies are sent only to the requester. - Online players are tracked by join/rename/leave events and exposed through
/guard playerswith fields:player_id,player_name,player_country,player_network. - 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.
The repository includes a reproducible synthetic benchmark at benchmarks/perf_footprint.php.
Run:
php benchmarks/perf_footprint.phpThe benchmark focuses on the two hottest maintenance paths under load:
guard_housekeeping: repeated cleanup of large in-memory caches.ipinfo_rate_limit_prune: repeated pruning of rate-limit timestamps.
| Scenario | Before (s) | After (s) | Improvement | Peak memory before | Peak memory after |
|---|---|---|---|---|---|
guard_housekeeping |
9.3785 | 0.0059 | 99.94% faster | 18,874,368 B | 18,874,368 B |
ipinfo_rate_limit_prune |
5.0232 | 0.0015 | 99.97% faster | 18,874,368 B | 16,777,216 B |
Raw benchmark snapshots are stored in:
benchmarks/before.jsonbenchmarks/after.json
These numbers are workload- and machine-specific; use the script above to validate on your host.
Keep the server tidy, keep the grid fair, keep the logs forthright.


