A Go implementation of a game master server - as in: QuakeWorld, Quake 2, Heretic 2, Doom 3, Quake 3 Arena. A MCP server for the agentic era is included. It uses a client to ask the game servers for their server infos. So, you can use this project as a Go library to query the game servers as well.
  • Go 99.4%
  • Makefile 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Paul Klumpp 1f42c18a34 Move module path to tangled.org and rename import paths
The repository moved from Gogs (gogs.netdome.biz) to a knot on
Tangled (knot.netdome.biz). The Gogs host could not serve SHA-256
repositories anyway: gogs/git-module parses object IDs as fixed
40-character SHA-1 hex strings.

The module path is now tangled.org/paulklumpp.tngl.sh/go-masterserver-client-mcp,
which is the vanity path that carries a go-import meta tag, so
'go get' resolves it.

Also ignore the stray build artifact at the repository root.
2026-11-11 00:15:11 +01:00
client Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00
cmd/quake-masterserver Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00
internal Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00
.gitignore Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00
AGENTS.md Initial commit: Go reimplementation of the lasange-system Quake family 2026-11-06 08:47:20 +01:00
env.example Initial commit: Go reimplementation of the lasange-system Quake family 2026-11-06 08:47:20 +01:00
go.mod Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00
go.sum Initial commit: Go reimplementation of the lasange-system Quake family 2026-11-06 08:47:20 +01:00
LICENSE Initial commit: Go reimplementation of the lasange-system Quake family 2026-11-06 08:47:20 +01:00
Makefile Initial commit: Go reimplementation of the lasange-system Quake family 2026-11-06 08:47:20 +01:00
README.md Move module path to tangled.org and rename import paths 2026-11-11 00:15:11 +01:00

quake-masterserver

Master servers for the classic Quake family — Quake II, QuakeWorld, Heretic II, Doom 3 and Quake 3 Arena — reimplemented in Go from the C plugins of the lasange-system masterserver. No Docker, one static binary.

In addition to the master server itself, this module ships:

  • a public Go client library (./client) so other projects can query master and game servers over UDP, and
  • an MCP server (streamable HTTP only) that exposes the in-memory server lists of every enabled game as tools.

Features

  • One binary, all five game masters, each independently switchable via environment (.env is supported).
  • Official master ports by default: Q2 27900, QW 27000, H2 28900, D3 27650, Q3 27950 + MOTD 27951.
  • The master probes the registered game servers itself over UDP (status/print, n-replies, the native Q3/D3 getstatus/getInfo heartbeat cycle) and caches the results (hostname, map, players, cvars) in memory.
  • MCP tools per enabled game:
    • list_<game>_servers — plain ip:port list,
    • <game>_server_info — JSON with server name, map, players and all collected cvars (optionally limited to one addr),
    • remove_server — admin tool, only registered when MS_ENABLE_ADMIN=true.
  • DoS-hardened UDP engine: per-source-IP rate limiting, a bounded rate-limiter table, a per-game server cap, and a 1400-byte packet limit.
  • HTTP-only MCP endpoint with optional bearer auth; serving on a non-loopback address without a token is refused.

Supported games

Game UDP port Heartbeat Server list query Info source
Quake II 27900 \xff..heartbeat + cvars query\n\0 heartbeat cvars, on demand status→print
QuakeWorld 27000 a\n<seq>\n<players>\n c\n\0 on demand status→n
Heretic II 28900 \xff..heartbeat + cvars query\n\0 heartbeat cvars, on demand status→print
Doom 3 27650 \xff\xffheartbeat\0 + getInfo getServers + LE32 protocol native getInfo cycle (infoResponse pairs)
Quake 3 27950, MOTD 27951 \xff..heartbeat + getstatus <challenge> getservers <proto> [filters] native getstatus cycle (statusResponse cvars)

Q3 also serves the MOTD on 27951 (text configurable via Q3M_MOTD).

Build

make build       # bin/quake-masterserver (version from git describe)
make test        # go test ./...
make test-race   # go test -race ./...
make vet         # go vet ./...
make fmt         # gofmt -w .
make check       # vet + race tests
make run         # build and start

The binary is a single static Go executable with no runtime dependencies. UDP traffic needs outbound and inbound access to the configured master ports (for info probing, the master also dials out to the registered game servers).

Configuration

Everything is configured via the environment; a .env file in the working directory is loaded automatically (ENV_FILE / --env-file to override). env.example lists every variable. All games are enabled by default, mirroring the original image.

Variable Default Notes
Q2M_ENABLED / Q2M_PORT true / 27900 Quake II master
QWM_ENABLED / QWM_PORT true / 27000 QuakeWorld master
H2M_ENABLED / H2M_PORT true / 28900 Heretic II master
D3M_ENABLED / D3M_PORT true / 27650 Doom 3 master
Q3M_ENABLED / Q3M_PORT true / 27950 Quake 3 master
Q3M_MOTD_PORT / Q3M_MOTD 27951 / welcome text Q3 MOTD port and text
MS_BIND_ADDR 0.0.0.0 UDP bind address (IPv4)
MS_HEARTBEAT_TIMEOUT 5m drop servers without a heartbeat after
MS_SWEEP_INTERVAL 15s reap dead registrations
MS_MAX_SERVERS 4096 server cap per game
MS_INFO_TTL 60s how often game servers are re-probed
MS_INFO_PROBE_TIMEOUT 2s per game server probe timeout
MS_MAX_INFO_PROBES 64 max servers probed per refresh round
MS_PACKET_RATE / MS_PACKET_WINDOW 100 / 10s per source IP rate limit
MS_ENABLE_ADMIN false expose the remove_server tool
MCP_ADDR 127.0.0.1:8080 HTTP MCP listen address
MCP_PATH /mcp must start with /, must not be /healthz
MCP_STATELESS false disable MCP sessions
MCP_AUTH_TOKEN – bearer token; required for non-loopback MCP_ADDR
LOG_LEVEL info debug, info, warn, error
ENV_FILE .env alternative env file

Flags (--addr, --path, --stateless, --log-level, --env-file, --version) override the corresponding transport settings.

MCP tools

For every enabled game the server registers two read-only tools:

  • list_<game>_servers → {"game": "q3", "servers": ["10.0.0.1:27960", ...], "count": n}
  • <game>_server_info → {"game": "q3", "servers": [{"addr", "name", "map", "players", "max_players", "protocol", "info": {...}}], "count": n} (optional input addr limits the result to one server)

and, when MS_ENABLE_ADMIN=true:

  • remove_server → {"game", "addr", "removed"}

Example HTTP setup for a local MCP client:

url: http://127.0.0.1:8080/mcp

/healthz returns {"status":"ok"} without auth and is usable for probes.

Client library

The client packages at the module root are importable:

import (
    "context"
    "tangled.org/paulklumpp.tngl.sh/go-masterserver-client-mcp/client/q3"
)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Server list from the master (protocol 68 = Q3 1.32).
servers, err := q3.GetServers(ctx, "master.example.com:27950", 68)
for _, s := range servers {
    fmt.Println(s.Addr()) // "10.0.0.1:27960"
}

// Info for one game server.
info, err := q3.Status(ctx, "10.0.0.1:27960")
fmt.Println(info.Name, info.Map, info.Clients)

Available packages and one-shot functions:

Package Functions
client Dial, RoundTrip, Read, ReadUntilIdle, ParseCvars, DecodeAddrBlocks, ParseBackslashPairs
client/q2 GetServers, Status, ParseServerList, ParsePrint
client/q3 GetServers, Status, ParseServerList, ParseStatusResponse
client/qw GetServers, Status, ParseServerList, ParseReply
client/h2 GetServers, Status, ParseServerList, ParsePrint
client/d3 GetServers, Status, ParseServerList, ParseInfoResponse, Protocol

The master server itself reuses these packages for its probing, so the library is exercised by the main program.

Security

Memory safety against malicious clients

The UDP side is hardening-first:

  • Per source IP rate limiting (not per ip:port): MS_PACKET_RATE packets per MS_PACKET_WINDOW per IP. Every incoming packet is checked before any processing, so floods neither grow state nor trigger replies (no reflection/amplification of inbound traffic).
  • Bounded tracking table: the rate limiter tracks at most 65536 source IPs. Once the table is full, unknown sources are dropped instead of allocated — an attacker cannot push memory usage up with spoofed addresses. Known sources keep working.
  • Server caps: MS_MAX_SERVERS per game (default 4096); registration attempts beyond the cap are ignored.
  • Packet size cap: payloads above 1400 bytes are dropped at the engine.
  • Bounded outbound probing: info refresh probes at most MS_MAX_INFO_PROBES servers per round with a per-probe timeout, so a hostile list of registrations cannot turn the master into an open prober.
  • Q3 requires the server to echo the master's challenge in its statusResponse; mismatched info is ignored.
  • A second Q3 heartbeat within 2 seconds is a shutdown signal, exactly like the original.

Sender address spoofing over UDP

Short answer: no — over plain UDP the source address cannot be verified, so a client can always claim a spoofed ip:port origin. This is a property of UDP/IP, not of this implementation.

What this server does instead (mitigation, not prevention):

  • Treats every packet as untrusted and validates it before acting: protocol header, packet size, rate limit, challenge echo (Q3).
  • Limits every amplification path: the only replies to inbound packets are the small list responses to query/c/getservers, which are rate-limited per source IP like everything else.
  • Bounds every table keyed by (potentially spoofed) address, so a spoofing flood cannot grow memory (see above).
  • Consequences for spoofing specifically: a client can register itself from a fake address (the master cannot tell), but it cannot register more servers than the per-game cap, cannot keep a fake registration alive without continuing to send from that address (heartbeats are required, and each one is rate-limited), and cannot use the master to amplify traffic toward third parties.

If you need real sender authentication, put the master behind a firewall that restricts the master ports to the subnets of the game servers, or run the games on a network where source IP spoofing is prevented at the switch/router level (e.g. anti-spoofing ACLs, or a single-subnet broadcast domain where the L2 address is pinned).

Architecture

                       ┌──────────────────────────────────────────┐
  game servers         │                engine (per port)         │
  (heartbeat, list     │  UDP socket ──▶ rate limit ──▶ Protocol  │
   requests)           │      ▲                            │      │
        ┌──────────────┤      │ (getstatus, getInfo, ...)  ▼      │
        │              │      └──────────────────────────────────┘
        ▼              │  game masters:  internal/games/{q2,qw,h2,d3,q3}
   ┌─────────┐         │  in-memory registrations + cached server info
   │  UDP    │         │      ▲                                    │
   └─────────┘         │      │ RefreshInfo (probe via client pkgs)│
        │              │      │                                    │
        ▼              │      ▼                                    │
   list/info requests │  client/{q2,qw,h2,d3,q3}  (public)        │
                       │      │                                    │
                       │      ▼                                    │
                       │  internal/mcp  (tools per enabled game)   │
                       │      │                                    │
                       │      ▼                                    │
                       │  internal/transport  (streamable HTTP)    │
                       └──────────────────────────────────────────┘
  • cmd/quake-masterserver — flags, env wiring, logger, signals.
  • internal/config — environment and .env parsing, validation.
  • internal/engine — one UDP socket per port, rate limiting, sweep loop, info refresh loop.
  • internal/games/* — one package per game master protocol; each implements master.Protocol.
  • internal/master — the Protocol interface, server records, rate limiter, the UDP prober used by RefreshInfo.
  • internal/mcp — MCP server and the per-game tool handlers.
  • internal/transport — streamable HTTP, bearer auth, /healthz.
  • client/* — the public query library (also used by the master).

License

GNU Affero General Public License v3 — Copyright (C) 2026 Paul Klumpp.