Lobby coordination server for Antistatic, the uncompromising platform fighter by bluehexagons.
Built on bluehexagons/gomoose
- IPv4 and IPv6 support
- Keyed lobby and random matchmaking endpoints
- Structured JSON logging with
log/slog - Configurable HTTP timeouts
- Automatic TLS with Let's Encrypt or custom certificates
- Rate limiting to prevent abuse
- Bounded in-memory lobby, matchmaking, and rate-limit state
- Docker support
- Health endpoint with lobby and matchmaking statistics
By default, running antistatic-server will run on port 80 without enabling HTTPS.
Run with antistatic-server -help to view all command line options.
By default, HTTPS support looks for cert.key and cert.crt in the working directory.
Use -cert path and -key path to specify custom locations.
Specifying a port using -tlsport will implicitly enable TLS.
| Flag | Default | Description |
|---|---|---|
-host |
"" | HTTP host to listen on |
-port |
80 | HTTP port to listen on |
-tls |
false | Enables TLS (sets tlsport to 443 if unspecified) |
-tlshost |
"" | TLS host to listen on |
-tlsport |
0 | TLS port to listen on |
-cert |
cert.crt | File to use as TLS cert |
-key |
cert.key | File to use as TLS key |
-autocert |
"" | Domain for automatic TLS (Let's Encrypt) |
-autocert-cache |
certs | Cache directory for autocert certificates |
-nohttp |
false | Disables HTTP server |
-read-timeout |
15s | HTTP read timeout |
-write-timeout |
15s | HTTP write timeout |
-idle-timeout |
60s | HTTP idle timeout |
-trust-proxy |
false | Trust X-Forwarded-For and X-Real-IP headers |
-trusted-proxy-cidrs |
"" | Comma-separated CIDR allowlist for trusted reverse proxies |
-stun-host |
"" | Bind address for the built-in STUN responder (default: dual-stack any-address) |
-stun-port |
0 | UDP port for the built-in STUN responder (0 disables; conventional value is 3478) |
To keep memory and CPU bounded under abusive traffic, the server enforces fixed in-memory limits:
| Limit | Value |
|---|---|
| URL path length | 512 bytes |
| Request body size | 10 KiB |
| Tracked rate-limit clients | 65,536 |
| Active lobbies | 10,000 |
| Members per lobby | 128 |
| Matchmaking tickets | 20,000 |
| Active matchmaking matches | 10,000 |
When a capacity limit is reached, new state-creating requests return 503 Service Unavailable; existing tickets and lobby members can continue to refresh until they expire or are deleted.
antistatic-server -tls -cert /etc/tls/server.crt -key /etc/tls/server.key- Custom cert/key locationsantistatic-server -tls -nohttp- HTTPS only, no HTTPantistatic-server -port 8080- Custom HTTP portantistatic-server -autocert example.com- Automatic TLS with Let's Encryptantistatic-server -autocert example.com -autocert-cache /var/cache/certs- Custom cache directoryantistatic-server -read-timeout 30s -write-timeout 30s- Custom timeoutsantistatic-server -trust-proxy -trusted-proxy-cidrs 127.0.0.1/32- Trust proxy headers from a local reverse proxy
The server can answer RFC 5389 Binding Requests on a UDP port so the
matchmaking client can discover its externally-mapped UDP endpoint without
relying on a third-party STUN service. Enable it with -stun-port 3478 (and
optionally -stun-host to bind a specific address). The UDP port must be
reachable directly from the public internet; UDP traffic is not forwarded by
HTTP reverse proxies, so route 3478/udp through the host firewall to the
server process.
The responder only emits Binding Success replies with a single XOR-MAPPED-ADDRESS attribute — no auth, no relay, no TURN — and discards anything that isn't a well-formed Binding Request, so it has the same minimal attack surface as the existing HTTP listener.
-trust-proxy is still opt-in. When enabled, forwarded headers are only honored if the immediate TCP peer is in -trusted-proxy-cidrs.
For example, when nginx runs on the same host and proxies to the Go server over loopback, use -trust-proxy -trusted-proxy-cidrs 127.0.0.1/32.
If nginx connects over IPv6 loopback, include ::1/128 as well.
Quick command to generate a self-signed certificate:
openssl req -newkey rsa:2048 -nodes -keyout cert.key -x509 -days 36525 -out cert.crt| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check (returns status, live counts, startup lobby creation total, successful game estimate, error count, version) |
PUT |
/{version}/lobby/{key}/{port} |
Register/update a lobby member |
DELETE |
/{version}/lobby/{key}/{port} |
Remove a lobby member |
GET |
/lobby/{key}/{port} |
Legacy endpoint (no version) |
PUT |
/{version}/matchmaking/{queue}/{ticket}/{port} |
Register or refresh a matchmaking ticket |
GET |
/{version}/matchmaking/{queue}/{ticket}/{port} |
Poll matchmaking ticket status |
DELETE |
/{version}/matchmaking/{queue}/{ticket}/{port} |
Cancel a matchmaking ticket |
Lobby and matchmaking ownership is protected with an X-Antistatic-Token header. The first successful PUT for a lobby member or matchmaking ticket returns a token; clients must send that token in X-Antistatic-Token when refreshing, polling, or deleting the same member/ticket. Tokens are bearer credentials and should not be logged or shared.
{
"status": "ok",
"lobby_count": 3,
"ticket_count": 2,
"match_count": 1,
"lobbies_created": 12,
"successful_games_estimate": 8,
"error_count": 1,
"version": "0.6.4"
}{
"local_ips": ["192.168.1.20", "10.0.0.20"]
}local_ips is optional. When present, entries are sanitized to private-scope addresses and only reflected to lobby peers seen from the same public IP.
{
"lobby": {
"key": "ABC123",
"members": [
{
"ip": "198.51.100.10",
"port": 45860,
"local_ips": ["192.168.1.20"]
}
],
"version": "0.9.5"
},
"ip": "198.51.100.10",
"port": 45860,
"token": "member-owner-token"
}{
"character": "Carbon",
"local_ips": ["192.168.1.20", "10.0.0.20"]
}local_ips is optional for matchmaking too. Entries are sanitized to private-scope addresses and only reflected to matched peers seen from the same public IP, allowing same-NAT or same-host clients to try LAN/loopback tunnel candidates without exposing LAN addresses to unrelated WAN peers.
Waiting, matched, and canceled matchmaking responses include aggregate queue measurements for the same game version and queue:
{
"status": "waiting",
"ticket": "ticket-id",
"ip": "198.51.100.10",
"port": 45860,
"token": "ticket-owner-token",
"queue": {
"players_waiting": 1,
"own_wait_ms": 12000,
"oldest_wait_ms": 12000,
"match_count": 4,
"average_match_wait_ms": 22000
}
}The queue data is privacy-preserving aggregate state only. It does not include other players' tickets, IPs, characters, or tokens.
{
"status": "matched",
"ticket": "ticket-id",
"ip": "198.51.100.10",
"port": 45860,
"token": "ticket-owner-token",
"match": {
"id": "0.9.5|default|TicketA|TicketB",
"role": "host",
"peer": {
"ip": "198.51.100.20",
"port": 45861,
"character": "Silicon"
},
"self": {
"ip": "198.51.100.10",
"port": 45860,
"character": "Carbon"
}
}
}Antistatic checks config.server for the lobby and matchmaking server URL.
Set this using the config command; e.g. config server \"http://example.com:8080\" (quotes must be escaped until strings are better supported).
The change can be persisted by editing the asconfig JSON file (e.g. nano ~/asconfig from the in-game terminal, or sifting through the fs.json save game file)
and adding/changing the server property there. This config is loaded when the game starts.
The server uses structured JSON logging via log/slog. Example log output:
{"time":"2026-04-27T09:17:56.123Z","level":"INFO","msg":"Lobby request","requestID":"abc123","method":"PUT","ip":"198.51.100.10","port":45860,"key":"ABC123","version":"0.9.5"}Build and run with Docker:
docker build -t antistatic-server .
docker run -p 80:80 -p 443:443 antistatic-serverOr with custom flags:
docker run -p 8080:8080 antistatic-server -port 8080 -tls -autocert example.comRequires Go 1.24 or later.
go build -o antistatic-server .For static binary (recommended for production):
CGO_ENABLED=0 go build -o antistatic-server .Run tests with:
go test -v ./...