feat: hard cross-client voice channel limits via voice-limit-guard
Lint / Shell (shellcheck) (push) Successful in 7s
Lint / JS (eslint) (push) Successful in 5s
Lint / Python (ruff) (push) Successful in 4s
Lint / Python deps (pip-audit) (push) Successful in 1m1s
Lint / Secret scan (gitleaks) (push) Successful in 4s

Add a fail-open Python sidecar (livekit/voice-limit-guard.py) that fronts
lk-jwt-service to enforce per-room voice participant caps for ALL Matrix
clients, not just Lotus Chat:
- lk-jwt-service moved to :8071 (systemd drop-in), guard owns :8070 so NPM's
  existing /sfu/get + /get_token proxy targets are unchanged
- guard reads io.lotus.voice_limit.max_users (Synapse admin API, cached),
  forwards to lk-jwt-service, and on an issued token decodes the LiveKit alias
  + requester, counts distinct Matrix users via LiveKit ListParticipants, and
  returns 403 when the room is full (rejoins/extra devices allowed)
- any error fails open (returns upstream response) so calls never break
- systemd/voice-limit-guard.service; README documents ports, setup, revert

Also update landing page: voice limit is now server-enforced for all clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 23:45:41 -04:00
parent 295a072dc9
commit 68a6acfa24
4 changed files with 380 additions and 4 deletions
+47 -2
View File
@@ -67,7 +67,8 @@ matrix/
- coturn config: `/etc/turnserver.conf`
- LiveKit config: `/etc/livekit/config.yaml`
- LiveKit service: `livekit-server.service`
- lk-jwt-service: `lk-jwt-service.service` (binds `:8070`, serves JWT tokens for MatrixRTC at `/sfu/get` and legacy `/get_token`)
- lk-jwt-service: `lk-jwt-service.service` (now binds `:8071` via drop-in `/etc/systemd/system/lk-jwt-service.service.d/override.conf`; serves JWT tokens for MatrixRTC at `/sfu/get` and legacy `/get_token`)
- voice-limit-guard: `voice-limit-guard.service` (binds `:8070`, fronts lk-jwt-service — enforces hard per-room voice participant limits for ALL clients; script `/opt/voice-limit-guard/voice-limit-guard.py`) — see [Voice Channel Limits](#voice-channel-limits)
- Hookshot: `/opt/hookshot/`, service: `matrix-hookshot.service`
- Hookshot config: `/opt/hookshot/config.yml`
- Hookshot registration: `/etc/matrix-synapse/hookshot-registration.yaml`
@@ -187,6 +188,49 @@ Killing livekit-server while a call is active drops everyone. Instead:
---
## Voice Channel Limits
Per-room voice participant caps are enforced **server-side for every client** (Element, FluffyChat, Lotus Chat, …), not just our own web client.
**How it works**
Every Matrix client must fetch a LiveKit JWT from lk-jwt-service before it can join a call. `voice-limit-guard` (a small fail-open Python sidecar, `livekit/voice-limit-guard.py` in this repo) sits in front of that service:
- lk-jwt-service was moved off `:8070` to `:8071` (systemd drop-in). The guard now owns `:8070`, so NPM's existing `/sfu/get` + `/get_token` proxy targets are unchanged.
- On each token request the guard reads `io.lotus.voice_limit``max_users` for the room (Synapse admin API, cached 10 s). `0` / absent = no limit.
- It forwards the request to lk-jwt-service, and if a token is issued it decodes the JWT to get the LiveKit alias (`video.room`) + requester identity (`sub`), then asks LiveKit `ListParticipants` how many **distinct Matrix users** are in the room.
- requester already present (rejoin / extra device) → allow
- distinct users ≥ limit → **403** (the client cannot get a token, so it cannot join)
- otherwise → allow
- **Fail-open:** any error (admin API down, bad token, LiveKit unreachable) returns the upstream response unchanged, so calls keep working even if enforcement is degraded.
**Setting a limit:** room admins set it from Lotus Chat → Room Settings → General → **Voice** (writes the `io.lotus.voice_limit` state event). Any tool that can send room state works too:
```bash
# max 5 participants in <roomId>; send {} to remove the limit
curl -X PUT -H "Authorization: Bearer <admin_token>" -H "Content-Type: application/json" \
"https://matrix.lotusguild.org/_matrix/client/v3/rooms/<roomId>/state/io.lotus.voice_limit/" \
-d '{"max_users": 5}'
```
**Config:** the guard reads `MATRIX_TOKEN` (server-admin) from `/etc/matrix-deploy.env`; LiveKit key/secret + ports are set in `systemd/voice-limit-guard.service`.
**Manual (re)deploy** (the file-specific auto-deploy pipeline does not cover this service):
```bash
# On LXC 151
install -D -m644 /opt/matrix-config/livekit/voice-limit-guard.py /opt/voice-limit-guard/voice-limit-guard.py
install -m644 /opt/matrix-config/systemd/voice-limit-guard.service /etc/systemd/system/voice-limit-guard.service
# one-time: rebind lk-jwt-service to :8071
mkdir -p /etc/systemd/system/lk-jwt-service.service.d
printf '[Service]\nEnvironment=LIVEKIT_JWT_BIND=:8071\n' > /etc/systemd/system/lk-jwt-service.service.d/override.conf
systemctl daemon-reload && systemctl restart lk-jwt-service && systemctl enable --now voice-limit-guard
```
**To fully revert** (back to lk-jwt-service directly on `:8070`): `systemctl disable --now voice-limit-guard`, remove the drop-in, `daemon-reload`, `systemctl restart lk-jwt-service`.
---
## Access Token Rotation
The `MATRIX_TOKEN` in `/etc/matrix-deploy.env` on LXC 151 is a Jared user token used to push hookshot transforms to Matrix room state (requires power level ≥ 50 in Spam and Stuff).
@@ -230,7 +274,8 @@ The token in `draupnir/production.yaml` in this repo is **intentionally redacted
| 6789 | LiveKit metrics | 0.0.0.0 |
| 7880 | LiveKit HTTP | 0.0.0.0 |
| 7881 | LiveKit RTC TCP | 0.0.0.0 |
| 8070 | lk-jwt-service | 0.0.0.0 |
| 8070 | voice-limit-guard (fronts lk-jwt-service) | 0.0.0.0 |
| 8071 | lk-jwt-service (behind guard) | 0.0.0.0 |
| 8080 | synapse-admin (nginx) | 0.0.0.0 |
| 3478 | coturn STUN/TURN | 0.0.0.0 |
| 5349 | coturn TURNS/TLS | 0.0.0.0 |