8a94ffd58f
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
6.8 KiB
Markdown
129 lines
6.8 KiB
Markdown
# 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.
|