feat(voice-limit-guard): enforce the limit at connect time too (#25) #37

Merged
jared merged 1 commits from feat/voice-limit-connect-time into main 2026-10-19 23:34:04 -04:00
Owner

Fixes #25 (the remaining part; #33 closed the simultaneous-join race at token issue).

Problem: the limit was only checked when a token was issued, and lk-jwt-service tokens last an hour. A token fetched while the channel had space (by a custom client, a second tab, or anyone holding on to one) could be used to connect after the channel filled up.

Now: the guard's existing reconcile loop (every 3 s) also enforces the limit at connect time:

  • It compares each limited room's distinct Matrix users (matrix_user) with max_users.
  • When people who arrived since the previous sweep take the room over its limit, it calls LiveKit RemoveParticipant on them, newest first (joined_at_ms, else joined_at), removing all of that person's devices.
  • People already in the call are never removed, so lowering a limit mid-call kicks nobody. A person's extra devices never count twice.
  • The first sweep of a room only records who is there. Unlimited, unrestricted rooms aren't even listed (fast path kept).
  • Over-limit can only come from a late connect, since admission is serialised at token issue.

Verified

  • python3 -m unittest livekit/test_voice_limit_guard.py: 56 OK, 9 new. They cover the decision cases above and reconcile_room with LiveKit mocked: the late connect is removed, and an unlimited room isn't listed.
  • Local stack (real guard, LiveKit, Synapse), with the limit at 1:
    • Bob fetched a token while the channel was empty (200) and held it. Audit Main then joined normally, and Bob connected straight to LiveKit with the old token. Guard log: removed @aud_bob:localhost:EARLYTOKEN from !cwFd…: over the voice limit of 1. Audit Main stayed in the call.
    • Control (no limit, same steps): no removal.
  • Limit 2, both users joining through the UI: both stay in the call through several sweeps, with no removals.
  • The README voice-limit section is updated.

Deploy: merging redeploys the guard on LXC 151 through the existing hook. I'll check production afterwards.

🤖 Generated with Claude Code

https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5

Fixes #25 (the remaining part; #33 closed the simultaneous-join race at token issue). **Problem:** the limit was only checked when a token was issued, and lk-jwt-service tokens last an hour. A token fetched while the channel had space (by a custom client, a second tab, or anyone holding on to one) could be used to connect after the channel filled up. **Now:** the guard's existing reconcile loop (every 3 s) also enforces the limit at connect time: - It compares each limited room's **distinct Matrix users** (`matrix_user`) with `max_users`. - When people who **arrived since the previous sweep** take the room over its limit, it calls LiveKit `RemoveParticipant` on them, newest first (`joined_at_ms`, else `joined_at`), removing all of that person's devices. - People already in the call are **never removed**, so lowering a limit mid-call kicks nobody. A person's extra devices never count twice. - The first sweep of a room only records who is there. Unlimited, unrestricted rooms aren't even listed (fast path kept). - Over-limit can only come from a late connect, since admission is serialised at token issue. **Verified** - `python3 -m unittest livekit/test_voice_limit_guard.py`: 56 OK, 9 new. They cover the decision cases above and `reconcile_room` with LiveKit mocked: the late connect is removed, and an unlimited room isn't listed. - **Local stack** (real guard, LiveKit, Synapse), with the limit at 1: - Bob fetched a token while the channel was empty (200) and held it. Audit Main then joined normally, and Bob connected straight to LiveKit with the old token. Guard log: `removed @aud_bob:localhost:EARLYTOKEN from !cwFd…: over the voice limit of 1`. Audit Main stayed in the call. - Control (no limit, same steps): no removal. - Limit 2, both users joining through the UI: both stay in the call through several sweeps, with no removals. - The README voice-limit section is updated. **Deploy:** merging redeploys the guard on LXC 151 through the existing hook. I'll check production afterwards. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared added 1 commit 2026-10-19 23:30:32 -04:00
feat(voice-limit-guard): enforce the limit at connect time too (#25)
Lint / Shell (shellcheck) (push) Successful in 39s
Lint / JS (eslint) (push) Successful in 6s
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 43s
Lint / Secret scan (gitleaks) (push) Successful in 9s
Lint / Shell (shellcheck) (pull_request) Successful in 13s
Lint / JS (eslint) (pull_request) Successful in 11s
Lint / No secrets in webhook configs (pull_request) Successful in 10s
Lint / Landing page is rendered (matrix (pull_request) Successful in 11s
Lint / Python (ruff) (pull_request) Successful in 10s
Lint / Python deps (pip-audit) (pull_request) Successful in 50s
Lint / Secret scan (gitleaks) (pull_request) Successful in 5s
d3f389cab1
The limit was only checked at token issue, and lk-jwt tokens last an hour,
so a token fetched while there was space could be used to connect once the
room was full. The reconcile loop now compares each limited room's
distinct Matrix users with max_users and RemoveParticipants the people who
arrived since the previous sweep, newest first, until it is back within the
limit. People already in the call are never removed (lowering a limit
mid-call kicks nobody); extra devices of one person never count twice.

Fixes #25

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