fix(voice-limit-guard): two simultaneous joins can no longer both pass a full room (#25) #33

Merged
jared merged 1 commits from fix/voice-limit-race into main 2026-10-18 19:46:01 -04:00
Owner

Refs #25. This closes the simultaneous-join race. The issue's other gap, a token fetched earlier and used up to an hour later, would need connect-time enforcement (LiveKit participant_joined webhooks) and stays open. Production change: the guard runs on LXC 151 and needs reinstalling and restarting after merge.

voice-limit-guard checked the limit against the users LiveKit already had connected. Tokens issued but not yet connected (the 1–2 s before the LiveKit connect) weren't counted, so two users pressing Join at the same moment could both get into a 1-person channel.

Change

  • SlotReservations: each admitted user gets a 30 s reservation, counted like a present user until LiveKit lists them, or it expires.
  • The check for a room runs under a per-room lock, so two requests can't both read "0/1".
  • Fail-open on errors is unchanged.

Verified

  • Unit tests: the second simultaneous join is blocked; the same user on another device is still allowed; reservations expire and clear once present; locks are per room. 47/47 pass, ruff clean.
  • Real guard against the local LiveKit + Synapse. Voice Lounge max_users: 1, two users' /sfu/get fired simultaneously:
    • currently running guard (old code): [200, 200], both admitted;
    • this branch, started on :8072 with the same env: [200, 403], logged blocked @aud_bob…: 1/1.

To deploy (LXC 151): reinstall per the README (install -D -m644 …/voice-limit-guard.py /opt/voice-limit-guard/), then systemctl restart voice-limit-guard.

🤖 Generated with Claude Code

https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5

Refs #25. This closes the simultaneous-join race. The issue's other gap, a token fetched earlier and used up to an hour later, would need connect-time enforcement (LiveKit `participant_joined` webhooks) and stays open. **Production change:** the guard runs on LXC 151 and needs reinstalling and restarting after merge. `voice-limit-guard` checked the limit against the users LiveKit **already had connected**. Tokens issued but not yet connected (the 1–2 s before the LiveKit connect) weren't counted, so two users pressing Join at the same moment could both get into a 1-person channel. **Change** - `SlotReservations`: each admitted user gets a **30 s reservation**, counted like a present user until LiveKit lists them, or it expires. - The check for a room runs under a **per-room lock**, so two requests can't both read "0/1". - Fail-open on errors is unchanged. **Verified** - Unit tests: the second simultaneous join is blocked; the same user on another device is still allowed; reservations expire and clear once present; locks are per room. 47/47 pass, ruff clean. - **Real guard against the local LiveKit + Synapse.** Voice Lounge `max_users: 1`, two users' `/sfu/get` fired simultaneously: - currently running guard (old code): **[200, 200]**, both admitted; - this branch, started on :8072 with the same env: **[200, 403]**, logged `blocked @aud_bob…: 1/1`. **To deploy** (LXC 151): reinstall per the README (`install -D -m644 …/voice-limit-guard.py /opt/voice-limit-guard/`), then `systemctl restart voice-limit-guard`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared added 1 commit 2026-10-18 15:24:28 -04:00
fix(voice-limit-guard): two simultaneous joins can no longer both pass a full room (#25)
Lint / Shell (shellcheck) (pull_request) Successful in 16s
Lint / JS (eslint) (pull_request) Successful in 13s
Lint / No secrets in webhook configs (pull_request) Successful in 8s
Lint / Landing page is rendered (matrix (pull_request) Successful in 10s
Lint / Python (ruff) (pull_request) Successful in 8s
Lint / Python deps (pip-audit) (pull_request) Successful in 1m25s
Lint / Secret scan (gitleaks) (pull_request) Successful in 13s
Lint / Shell (shellcheck) (push) Successful in 14s
Lint / JS (eslint) (push) Successful in 7s
Lint / No secrets in webhook configs (push) Successful in 4s
Lint / Landing page is rendered (matrix (push) Successful in 4s
Lint / Python (ruff) (push) Successful in 5s
Lint / Python deps (pip-audit) (push) Successful in 40s
Lint / Secret scan (gitleaks) (push) Successful in 6s
fa0351084b
The limit was checked against participants LiveKit already had, so
tokens issued but not yet connected were not counted: two users
pressing Join together could both get into a 1-person channel.
Serialise the check per room and give each admitted user a 30 s
reservation, counted like a present user until LiveKit lists them or
it expires.

Refs #25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared merged commit 96d1778bb4 into main 2026-10-18 19:46:01 -04:00
Sign in to join this conversation.