Retire legacy implementations and flatten managed layout
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-21 00:11:32 +02:00
parent dd811a0bb8
commit 08e6c5930a
422 changed files with 252 additions and 38242 deletions
+19 -213
View File
@@ -1,229 +1,35 @@
# Deployment & Self-Hosting
# Server deployment
## Managed server deployment checkpoint
The .NET server preserves protocol v2 and the native schema/credentials. Publish a
self-contained executable (no installed .NET runtime required):
Publish a locked, self-contained server from the repository root:
```powershell
# Publishes both win-x64 and linux-x64 by default.
./dotnet/publish-server.ps1
# Publish just one target when needed.
./dotnet/publish-server.ps1 -Runtime linux-x64
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --help
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe account add Operator --admin --data-dir ./voicecat-data
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --data-dir ./voicecat-data --allow-guests false
./scripts/publish-server.ps1
./scripts/publish-server.ps1 -Runtime linux-x64
./artifacts/server/win-x64/VoiceCat.Server.exe --help
```
Account add/reset use a hidden password prompt, redirected standard input, or
`VOICECAT_ADMIN_PASSWORD`. Passwords are never accepted as command arguments or logged.
Account delete/list work against the same database, including while the server runs.
Provisioning grants administrator access only through the local command's `--admin`;
in-band account creation remains non-admin. Restrict access to the data directory.
Defaults are `0.0.0.0:8384` TCP+UDP, guest access enabled, 64 connections, 15-second TLS
handshakes, 45-second idle expiry and 15-second sweeps. Override with flags or environment:
| Flag | Environment variable |
|---|---|
| `--data-dir` | `VOICECAT_DATA_DIR` |
| `--bind` | `VOICECAT_BIND_ADDRESS` |
| `--port` | `VOICECAT_BIND_PORT` |
| `--name` | `VOICECAT_SERVER_NAME` |
| `--allow-guests` | `VOICECAT_ALLOW_GUESTS` |
| `--max-connections` | `VOICECAT_MAX_CONNECTIONS` |
| `--handshake-seconds` | `VOICECAT_HANDSHAKE_TIMEOUT_SECONDS` |
| `--idle-seconds` | `VOICECAT_IDLE_TIMEOUT_SECONDS` |
| `--reaper-seconds` | `VOICECAT_REAPER_INTERVAL_SECONDS` |
| `--auth-burst` | `VOICECAT_AUTH_BURST` |
| `--auth-refill-seconds` | `VOICECAT_AUTH_REFILL_SECONDS` |
Command arguments override environment values. `--print-config` validates and prints JSON
without creating files; `--print-fingerprint` prints the persisted leaf-certificate SHA-256
pin. Startup emits one JSON `ready` event with both fingerprints and actual TCP/UDP ports.
Bind accepts IP literals; IPv6 listeners are IPv6-only. Open/forward both protocols.
An exclusive data-directory instance lock prevents duplicate managed server processes.
Ctrl+C and Unix SIGINT/SIGTERM stop all transport tasks; shutdown has a ten-second deadline.
Fatal listener/media/reaper failure exits the host rather than leaving a broken listener.
`--health-check HOST:PORT` performs a real protocol TLS 1.3 handshake and emits JSON.
Automation can add `--expect-fingerprint SHA256` to verify the persisted certificate.
Password authentication is limited before Argon2 by source address and username across
connections: burst 5, refill one attempt per ten seconds. Starting at three failed attempts,
backoff grows from one to thirty seconds. Success clears backoff but does not restore
tokens. State is bounded to 4096 keys; idle entries retire after ten minutes when full.
Throttle and credential failures share the generic auth error. Limits are process-local.
The publish script produces Windows x64 and Linux x64 self-contained outputs in one command
by default. It uses separate per-RID lock files so deployment and development restore graphs
remain reproducible. Pass `-Runtime` to publish only one target.
The Linux smoke starts the published ELF, validates TLS health and the pin, checks mode 0700
data creation, sends SIGTERM and requires a clean exit:
```bash
sh deploy/linux/smoke.sh dotnet/artifacts/server/linux-x64/VoiceCat.Server
```
`deploy/linux/voicecat.service` runs under a systemd dynamic user with a private state
directory, no capabilities and filesystem/kernel hardening. Install a published binary and
the unit from `deploy/linux/` as root, or use the helper there. The default Dockerfile now
builds the managed server with locked packages and runs it as UID 1654 in Microsoft's
chiseled .NET runtime-dependencies image. Compose drops all capabilities, sets the root
filesystem read-only and persists only `/data`.
For operational endurance against an already running server:
The default endpoint is TCP and UDP port 8384. The server creates its TLS identity and SQLite
database in the data directory. Account passwords are accepted only through a hidden prompt,
redirected standard input, or `VOICECAT_ADMIN_PASSWORD`; they are never command arguments.
```powershell
./dotnet/soak-server.ps1 -HostName 127.0.0.1 -Port 8384 -Minutes 30 -Pairs 4
./artifacts/server/win-x64/VoiceCat.Server.exe account add Operator --admin --data-dir ./voicecat-data
./artifacts/server/win-x64/VoiceCat.Server.exe --data-dir ./voicecat-data --allow-guests false
```
Every cycle creates independent managed processes that exchange text and decoded voice.
A short 16-session published-server soak is part of this checkpoint. A release candidate
still needs the long soak on its target host. TOML/reload and signed image publication remain
before broad production rollout.
The checked-in `Dockerfile` builds the managed Linux server. `docker-compose.yml` runs it as a
non-root user with a read-only filesystem, persistent `/data`, and TCP/UDP 8384 exposed.
The product goal: someone looks at this and thinks *"oh, I (or my agent) can stand this up
in a few minutes."* Everything below is in service of that. Three install paths, all
**zero-config and encrypted by default**.
## 1. The three paths
### A. Docker (recommended)
For a host installation, `deploy/linux/install.sh` installs a published Linux binary and the
systemd unit. Validate a published binary with:
```bash
docker build -t voicecat:local .
docker run -d --name voicecat \
-p 8384:8384/tcp \ # control (TLS 1.3)
-p 8384:8384/udp \ # media (encrypted)
-v voicecat-data:/data \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=16m \
--cap-drop ALL --security-opt no-new-privileges \
voicecat:local
sh deploy/linux/smoke.sh artifacts/server/linux-x64/VoiceCat.Server
```
That's the whole thing. On first start it generates its Ed25519 identity + self-signed
cert, creates the SQLite database under `/data`, prints the **server fingerprint** (for
clients to verify), and listens. Control and media share one port number on TCP+UDP to keep
firewall rules trivial.
The server exposes `--health-check host:port` for TLS-aware probes. Before release, run the
container and the concurrent client soak:
A `docker-compose.yml` is provided for people who prefer it, but it isn't required.
### B. Single static binary
```bash
# download for your OS, then:
./voicecat-server # uses ./voicecat-data/ , prints fingerprint, runs
```powershell
./scripts/soak-server.ps1 -HostName 127.0.0.1 -Port 8384 -Minutes 30 -Pairs 4
```
The server is a **single statically linked executable** (mbedTLS, libsodium, opus, sqlite,
etc. linked in — all permissive licenses). No runtime, no shared libraries to install, no
package manager. Linux (primary), macOS, and Windows builds.
### C. From source
```bash
git clone … && cd voice-cat
cmake --preset server-release # vcpkg fetches & pins all deps; auto-triplet (Linux/macOS/Windows)
cmake --build --preset server-release
./build/server-release/bin/voicecat-server
```
One `cmake` invocation; vcpkg (manifest mode) resolves the dependency graph reproducibly.
The `server-release` preset produces an optimized, **stripped** binary (`-s` linker flag) —
smaller executables suitable for distribution. Works on Linux (primary), macOS, and Windows;
the vcpkg triplet is auto-resolved by [`cmake/voicecat-toolchain.cmake`](../cmake/voicecat-toolchain.cmake).
No system packages to chase.
## 2. Zero-config defaults
The server runs with **no config file at all**. Sensible defaults:
| Setting | Default |
|---------|---------|
| Encryption | On, always (not configurable off) |
| TLS cert / identity | Auto-generated on first run, persisted to the data dir |
| Database | Embedded SQLite in the data dir (no external DB) |
| Guests | Enabled (so the very first connect "just works"); easily disabled |
| Ports | `8384` TCP + UDP |
| A default channel | One "Lobby" voice/text channel created on first run |
| Opus policy | 48 kHz, 20 ms frames, mono/VOIP defaults; per-channel overrides allowed |
| Argon2id cost | Auto-tuned to the host on first run |
Override only what you care about, via env vars or an optional `server.toml`:
```toml
# server.toml — every key is optional
server_name = "Cats United"
allow_guests = false
bind_port = 8384
data_dir = "/data"
[tls] # only if you want a real CA cert; otherwise self-signed
cert_file = "/data/fullchain.pem"
key_file = "/data/privkey.pem"
[opus.defaults] # default Opus policy for new channels
mode = "mono"
bitrate_bps = 24000
frame_ms = 20
fec = true
dtx = true
[opus.limits] # server-enforced ceilings (bound bandwidth)
max_bitrate_bps = 128000 # channels can't be configured above this
```
Every key also has an `VOICECAT_*` env var form, which is what the Docker path uses.
## 3. Connecting (client side)
- **Pure direct-connect.** Enter `host:port` (and a nickname or account). There is no central
directory or server browser — you connect to a server you know.
- **Saved server list.** The client keeps a local list of saved servers (host:port, pinned
fingerprint, nickname/credentials per server) so you can store several and pick one to
join. This lives entirely in the client.
- On first connect the client shows the server's **fingerprint** and pins it (TOFU). No
accounts or certs needed to try it; if the operator disabled guests, the client prompts for
the username/password an admin gave you.
That's the entire flow: run the server, share `host:port` + fingerprint, friends save it and
connect.
## 3a. Provisioning accounts (admin)
Accounts are **admin-provisioned** — there is no self-serve registration. Two equivalent ways,
both writing the same SQLite store:
```bash
# CLI against the server's data dir or a running server
voicecat-admin account add <username> # prompts for / generates a password
voicecat-admin account reset <username>
voicecat-admin account del <username>
voicecat-admin account list
```
…or from the **in-app admin interface** (a user with the admin permission), which sends the
privileged `CreateAccount`/`ResetPassword`/`DeleteAccount` control messages over TLS
(protocol.md §3). Guests need no provisioning; they just pick a nickname (if guests are
enabled).
## 4. Why it stays this easy (design constraints that protect the goal)
- **No external services.** No separate database, no Redis, no TURN/STUN server, no reverse
proxy required. SQLite is embedded; media is plain UDP.
- **No certificate chore.** Self-signed + Ed25519 TOFU means encryption needs zero operator
action. A domain owner *can* drop in a Let's Encrypt cert, but never *has* to.
- **One port pair.** TCP+UDP on the same number; one firewall/port-forward rule.
- **Static linking + permissive licenses.** The binary has no install-time dependencies and
can be redistributed (including closed-source) without copyleft obligations.
- **Agent-friendly.** The run command is a single line with no interactive prompts, the
server logs its fingerprint and listen address in machine-readable form, and `--help` /
`--print-config` expose everything an automation needs. Health endpoint for liveness checks.
## 5. Operational niceties (planned, not blocking v1)
- Signed, multi-architecture image publication.
- Graceful reload of `server.toml` on `SIGHUP`.
- `voicecat-admin` (see §3a) also handles bans and channel admin, talking to the same SQLite
file or a running server.
- Prebuilt images for `linux/amd64` + `linux/arm64` (Raspberry Pi / cheap VPS friendly).