panel v0.9.2 — public release

Self-hostable game server control panel: Go controller + agent, 26 game
modules, embedded web UI. One-line install via install.sh / install.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:43:35 -07:00
commit 4ccccc6fe2
2164 changed files with 301480 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
# Architecture
How panel actually fits together. (An earlier revision of this document
described the original design — SvelteKit, SQLite, Casbin, SFTP; none of
that shipped. This is the real stack.)
## Topology
```
┌───────────────────────────────┐ ┌────────────────────────────┐
│ Controller │ gRPC/mTLS │ Agent (per host) │
│ - Embedded web dashboard │◄──────────►│ - one persistent bidi │
│ - REST /api/* + SSE events │ :8443 │ stream (Agent.Connect) │
│ - Postgres (all state) │ │ - Docker runtime │
│ - Internal CA + pairing │ │ - module loader │
│ - Scheduler (cron + CEL) │ │ - RCON adapters │
│ - Admin HTTP /admin/v1/* │ │ - state tracker │
│ - Prometheus /metrics │ │ - SteamCMD / tar sidecars │
└───────────────────────────────┘ └────────────────────────────┘
▲ HTTP :8080 │ docker.sock
browser / panelctl / curl game containers (one per instance)
```
One controller orchestrates N agents. Agents connect **outbound only**
— an agent box needs no inbound panel ports, just its game ports.
## Components
**Controller** (`controller/cmd/controller`) — a single Go binary:
- **Embedded vanilla-JS dashboard.** No framework, no build step:
`static/new.html` (shell) + `static/rcc.js` + `static/rcc.css`,
compiled in via `//go:embed` and served hash-stamped with immutable
caching. The legacy single-file `static/index.html` is a frozen
"classic" UI, opt-in via the `panel_ui=classic` cookie. Live data
reaches the browser over **SSE** (`GET /api/events`, with
`Last-Event-ID` resume), not WebSockets.
- **Postgres** for everything durable: users/sessions, agents,
instances + assigned ports, schedules, backup index, pair-token
hashes, Steam credential ciphertext. Migrations are embedded and run
at startup. (No SQLite anywhere.)
- **Internal CA** (`controller/internal/ca`, material in `data/ca/`) —
issues the gRPC server cert and, via the pairing flow, each agent's
client cert. `-tls auto` turns mTLS on whenever the CA exists.
- **Scheduler** — robfig/cron 6-field triggers **and** CEL expressions
evaluated against the live event bus with sustained-for windows
("empty for 30 minutes → stop"). Actions: rcon, instance
start/stop/restart, backup.
- **Auth** — argon2id + session cookies; roles are just
`admin`/`user`; optional Sign-in-with-Steam (OpenID). No
OIDC/WebAuthn/Casbin.
- **Admin HTTP** (`/admin/v1/*`) — out-of-band repair surface, gated by
loopback or the `X-Panel-Admin-Token` header, usable when the
dashboard or a session is broken.
**Agent** (`agent/cmd/agent`) — a single Go binary per game host:
- Dials the controller's gRPC port and holds one bidi stream; all
control traffic (create/start/stop/update/backup/RCON/file ops,
events, heartbeats) is multiplexed over `oneof` envelope messages
(`proto/panel/v1`). Reconnect + rehydrate: instance metadata survives
agent restarts via `--meta-dir` sidecar files rebuilt from the
controller's DB.
- **Docker-per-game runtime** (`agent/internal/runtime`): one container
per instance, named volumes and/or bind mounts under `--data-root`,
optional host networking. `panel-*` images are **auto-built** from
the module's in-tree Dockerfile on first use; third-party images are
pulled. Windows-only servers run under Wine + Xvfb in those images.
- **Sidecars**: `steamcmd/steamcmd` for installs/updates (Windows-depot
and beta-branch capable), alpine tar containers for backup/restore.
- **State tracker** (`agent/internal/state`): container state + RCON
polling + log-line pattern matching → typed PlayerEvent/AppState
events streamed upstream and fanned out to the browser (SSE), the
scheduler (CEL), and `panelctl watch`.
- **RCON adapters** (`agent/internal/rcon`): `source_rcon`, `telnet`,
`websocket_rcon` (Rust), `stdio`, `be_rcon` (DayZ), `docker_exec_rcon`
— all redial on EOF.
**panelctl** (`controller/cmd/panelctl`) — operator CLI. gRPC surface
for day-to-day lifecycle (agents/instances/create/start/stop/rcon/
backup/watch), plus a `panelctl admin` HTTP surface for the repair
endpoints. Uses the same mTLS certs as agents.
**Modules** (`modules/<id>/module.yaml`) — declarative manifests: image
or Dockerfile, ports, env, volumes, config files + templated values,
RCON adapter + commands, state-source polls, log event patterns, update
providers, appearance. The loader (`pkg/module`) is strict
(`KnownFields`) — unknown keys are rejected, not ignored. The README's
"Module manifest" section documents the schema by example.
## Security model
- **Control plane:** gRPC with mTLS; agent certs are issued via
one-time pairing tokens ([PAIRING.md](PAIRING.md)) and verified at the
TLS handshake (`tls.RequireAndVerifyClientCert`). Plaintext exists
only as an explicit dev opt-in (`--insecure` / `-tls off`).
- **Browser:** session cookies over your reverse proxy's TLS
([INSTALL.md](INSTALL.md)).
- **Secrets:** per-instance generated secrets in instance metadata;
Steam credentials AES-GCM-encrypted with a key HKDF-derived from the
CA key ([ADMIN.md](ADMIN.md)).
- **Blast radius:** the agent needs the Docker socket — root-equivalent
on its host by design. Run agents on hosts you trust and don't expose
the controller's gRPC port to the internet without cause.
## Data flow, end to end
1. Operator clicks Create → `POST /api/instances` → controller
allocates host ports (per-agent window, DB-reserved — see
[NETWORKING.md](NETWORKING.md)) → `InstanceCreate` envelope to the
agent.
2. Agent resolves the manifest (env filtering, `$INSTANCE_ID` /
`$DATA_PATH` substitution), auto-builds/pulls the image, creates the
container, persists sidecar metadata.
3. Update → SteamCMD sidecar into the game volume. Start → container
start; state tracker begins polling/tailing; readiness regex flips
the card to Running.
4. Events stream to the controller → Postgres aggregates + SSE fanout →
dashboard, scheduler CEL evaluation, `panelctl watch`.
## Not implemented (on purpose or yet)
- OIDC / WebAuthn / Casbin ACLs — local auth + Steam OpenID only.
- SFTP — the web file manager (docker exec/cp with bind-mount fast
path) covers the common case.
- Host-mode (non-Docker) runtime — manifests declare it, the agent
fails loudly if asked.
- Multi-tenancy/billing — this is for someone running their own boxes.