# svc-proxy Standalone UDP tunnel for [Simple Voice Chat](https://github.com/henkelmax/simple-voice-chat). Per-server public UDP port → backend voice address. Routes read from Postgres via `LISTEN`/`NOTIFY`, same pattern as `mc-router`. ## What it does Each MC server in the automc fleet runs SVC on its own UDP port inside its container (default 24454). svc-proxy exposes a **public** UDP port per server and tunnels client traffic to the backend. SVC's own `SecretPacket` is configured per backend to advertise the public proxy hostname + the assigned proxy port, so the client connects directly to the proxy — no MITM, no plugin-channel sniffing. ``` SVC client ──UDP──► svc-proxy.timemachine.center:24455 │ ├── (per-server tunnel) │ └──UDP──► mc-gtnh:24454 (backend SVC) ``` The proxy is **opaque** to the SVC payload — it can read the cleartext outer header (magic byte + player UUID) but the AES-GCM body stays end-to-end. Per-client tunnels (one ephemeral upstream socket per client `SocketAddress`) survive NAT rebinds within the idle TTL. ## pg schema Two new columns on `servers`: | Column | Type | Meaning | |---|---|---| | `voice_address` | text | Backend SVC address — `:` reachable from svc-proxy | | `voice_proxy_port` | int | Public UDP port svc-proxy binds for this server | Rows with both NULL are ignored. Owner of allocation: **server-manager** (assigns the next free port from a configured pool when the server is provisioned; clears on delete). ## NOTIFY channel Reuses `automc_routes_changed` from `mc-router`. The trigger on `servers` already fires on UPDATE, so adding/clearing the voice columns refreshes svc-proxy's bindings without restart. ## Environment | Env | Default | Effect | |---|---|---| | `DATABASE_URL` | (required) | pgx DSN | | `BIND_HOST` | `0.0.0.0` | host for the per-server UDP listeners | | `BRIDGE_IDLE_TTL` | `5m` | tear down per-client upstream sockets after this much silence | | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` | ## Operator UX ```bash # Allocate voice ports for an existing server (server-manager does this normally) UPDATE servers SET voice_address = 'mc-gtnh:24454', voice_proxy_port = 24455 WHERE name = 'gtnh'; NOTIFY automc_routes_changed; ``` svc-proxy logs `listener open: :24455 → mc-gtnh:24454 (gtnh)` and is ready. To retire a server's voice routing: ```bash UPDATE servers SET voice_address = NULL, voice_proxy_port = NULL WHERE name = 'gtnh'; NOTIFY automc_routes_changed; ``` svc-proxy logs `listener close: :24455 (gtnh)`. In-flight tunnels are torn down. ## Backend-side configuration The SVC plugin on the backend must advertise the **public** proxy address to clients (not the backend's own LAN address). Set in the backend's SVC config (`config/voicechat-server.properties`): ```properties voice_host=svc-proxy.timemachine.center:24455 ``` …or via env if mc-wrapper templates it. SVC bakes this into `SecretPacket.voiceHost`, the client uses it verbatim. ## Why not the SVC bundled proxy SVC ships proxy support for BungeeCord/Velocity (`common-proxy` module). It sniffs the MC `voicechat:secret` plugin message and rewrites the host on the fly, then NAT-bridges UDP. That requires the SVC proxy to live inside the MC proxy process. We run mc-router (Go) instead of a Java MC proxy on the edge, so the bundled approach doesn't apply. svc-proxy is the equivalent for the mc-router shape: pure UDP data plane, pg-driven config, no plugin hooks. ## Limitations - No replay protection at the proxy layer (SVC's AES-GCM is the only freshness guarantee — same as upstream). - No client rate-limiting (SVC's plugin-channel rate limit covers TCP setup; UDP audio relies on Opus payload caps + the `BRIDGE_IDLE_TTL` env to bound per-tunnel sockets). - Per-tunnel ephemeral upstream sockets aren't pooled — one syscall per concurrent client. Fine up to a few thousand concurrent voice tunnels on a single proxy host. ## Related - [mc-router (Timemachine fork)](https://git.timemachine.center/Timemachine/mc-router) — same NOTIFY channel, same pg-driven route source. - [Simple Voice Chat](https://github.com/henkelmax/simple-voice-chat) — upstream mod whose wire protocol we pass through.