initial: svc-proxy — UDP valve for Simple Voice Chat
Standalone Go service that routes SVC client traffic to per-server
backend voice endpoints, configured via pg LISTEN/NOTIFY (same channel
mc-router subscribes to). Each pg `servers` row with both
`voice_address` and `voice_proxy_port` set spawns a Valve: a public
UDP listener that maintains per-client ephemeral bridges to the
backend's SVC port.
Pieces:
cmd/svc-proxy/main.go entry; wires config, log fan-out,
bridge.Manager, pgsync, httpsrv
internal/config/ DATABASE_URL + BIND_HOST +
BRIDGE_IDLE_TTL (default 1m) +
HTTP_ADDR (default :8081)
internal/pgsync/ LISTEN automc_routes_changed; diff
desired/actual routes; emit Apply()
internal/bridge/ Valve per public port; per-client
bridge with atomic up/down byte counters;
idle eviction every 15s against TTL
internal/httpsrv/ operator UI — embedded single-page HTML
with active-connections table polled
every 1s + SSE log stream
(last 500 lines backlog on connect)
Reverse-proxied behind server-manager at /infra/svc-proxy/* — bind
internal-only addresses for production; auth is the dashboard's
Basic gate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# svc-proxy
|
||||
|
||||
Standalone UDP "valve" 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 bridges 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 valve)
|
||||
│
|
||||
└──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. Source-address bridges (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 — `<host>:<port>` 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 `valve 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 `valve close: :24455 (gtnh)`. In-flight bridges 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 wrapper's `BRIDGE_IDLE_TTL` to bound per-source sockets).
|
||||
- Bridge ephemeral upstream sockets aren't pooled — one syscall per concurrent client. Fine up to a few thousand concurrent voice users 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.
|
||||
Reference in New Issue
Block a user