panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user