Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076340227f |
@@ -67,7 +67,7 @@ matrix/
|
||||
- coturn config: `/etc/turnserver.conf`
|
||||
- LiveKit config: `/etc/livekit/config.yaml`
|
||||
- LiveKit service: `livekit-server.service`
|
||||
- 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`)
|
||||
- lk-jwt-service: `lk-jwt-service.service` (now binds `127.0.0.1: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 **and publish permissions (screenshare/camera via JWT re-signing)** for ALL clients; script `/opt/voice-limit-guard/voice-limit-guard.py`) — see [Voice Channel Limits & Call Permissions](#voice-channel-limits--call-permissions)
|
||||
- Hookshot: `/opt/hookshot/`, service: `matrix-hookshot.service`
|
||||
- Hookshot config: `/opt/hookshot/config.yml`
|
||||
@@ -238,7 +238,9 @@ install -D -m644 /opt/matrix-config/livekit/voice-limit-guard.py /opt/voice-limi
|
||||
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
|
||||
# Loopback only (matrix #24): the guard runs on this LXC; binding all
|
||||
# interfaces let any LAN host mint unguarded tokens straight from :8071.
|
||||
printf '[Service]\nEnvironment=LIVEKIT_JWT_BIND=127.0.0.1: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
|
||||
```
|
||||
|
||||
@@ -290,7 +292,7 @@ The token in `draupnir/production.yaml` in this repo is **intentionally redacted
|
||||
| 7880 | LiveKit HTTP | 0.0.0.0 |
|
||||
| 7881 | LiveKit RTC TCP | 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 |
|
||||
| 8071 | lk-jwt-service (behind guard) | 127.0.0.1 (matrix #24; was 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 |
|
||||
|
||||
@@ -389,31 +389,3 @@ class NormalizePermissionTest(unittest.TestCase):
|
||||
guard.livekit_update_participant = lambda *a: None
|
||||
participant = {"identity": "@a:x:DEV", "permission": {"canPublish": True, "canPublishSources": ["CAMERA"]}}
|
||||
self.assertFalse(guard.reconcile_participant("!r:x", participant, {"SCREEN_SHARE"}))
|
||||
|
||||
|
||||
class SlotReservationTests(unittest.TestCase):
|
||||
"""[matrix #25] Simultaneous joins can't both pass a full room."""
|
||||
|
||||
def test_second_simultaneous_join_is_blocked(self):
|
||||
r = guard.SlotReservations(ttl=30)
|
||||
present = set()
|
||||
counted = present | r.reserved("room", present, now=0)
|
||||
self.assertFalse(guard.should_block(1, counted, "@a:x"))
|
||||
r.reserve("room", "@a:x", now=0)
|
||||
# @b asks a moment later, before @a has connected to LiveKit.
|
||||
counted = present | r.reserved("room", present, now=1)
|
||||
self.assertTrue(guard.should_block(1, counted, "@b:x"))
|
||||
# @a asking again (second device) is still allowed.
|
||||
self.assertFalse(guard.should_block(1, counted, "@a:x"))
|
||||
|
||||
def test_reservation_expires_and_clears_once_present(self):
|
||||
r = guard.SlotReservations(ttl=30)
|
||||
r.reserve("room", "@a:x", now=0)
|
||||
self.assertEqual(r.reserved("room", set(), now=31), set())
|
||||
r.reserve("room", "@a:x", now=40)
|
||||
self.assertEqual(r.reserved("room", {"@a:x"}, now=41), set())
|
||||
|
||||
def test_room_lock_is_per_room(self):
|
||||
r = guard.SlotReservations()
|
||||
self.assertIs(r.room_lock("a"), r.room_lock("a"))
|
||||
self.assertIsNot(r.room_lock("a"), r.room_lock("b"))
|
||||
|
||||
@@ -145,41 +145,6 @@ def should_block(limit: int, present_users: set, requester: str) -> bool:
|
||||
return len(present_users) >= limit
|
||||
|
||||
|
||||
# [matrix #25] Tokens issued but not yet connected weren't counted, so two
|
||||
# users pressing Join together could both pass a 1-person limit. Each admitted
|
||||
# user holds a short reservation (counted like a present user) until LiveKit
|
||||
# lists them or it expires; checks for a room are serialised with a lock.
|
||||
RESERVATION_TTL = 30.0
|
||||
|
||||
|
||||
class SlotReservations:
|
||||
"""Per-room reservations for admitted-but-not-yet-connected users."""
|
||||
|
||||
def __init__(self, ttl: float = RESERVATION_TTL):
|
||||
self.ttl = ttl
|
||||
self._by_room = {} # alias -> {user: expiry}
|
||||
self._guard = threading.Lock()
|
||||
self._room_locks = {}
|
||||
|
||||
def room_lock(self, alias: str) -> threading.Lock:
|
||||
with self._guard:
|
||||
return self._room_locks.setdefault(alias, threading.Lock())
|
||||
|
||||
def reserved(self, alias: str, present: set, now: float) -> set:
|
||||
"""Unexpired reservations; drops expired ones and users now present."""
|
||||
room = self._by_room.get(alias, {})
|
||||
for user in [u for u, exp in room.items() if exp <= now or u in present]:
|
||||
del room[user]
|
||||
return set(room)
|
||||
|
||||
def reserve(self, alias: str, user: str, now: float) -> None:
|
||||
if user:
|
||||
self._by_room.setdefault(alias, {})[user] = now + self.ttl
|
||||
|
||||
|
||||
RESERVATIONS = SlotReservations()
|
||||
|
||||
|
||||
def allowed_sources(policy: dict):
|
||||
"""Pure decision: the narrowed `canPublishSources` list, or None.
|
||||
|
||||
@@ -653,16 +618,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# LiveKit-admin outage here cannot skip the publish-source policy below.
|
||||
if limit > 0 and alias:
|
||||
try:
|
||||
with RESERVATIONS.room_lock(alias):
|
||||
present = livekit_present_users(alias)
|
||||
now = time.monotonic()
|
||||
counted = present | RESERVATIONS.reserved(alias, present, now)
|
||||
if should_block(limit, counted, requester):
|
||||
log(f"blocked {requester or '?'} from {room_id}: {len(counted)}/{limit}")
|
||||
self._send_blocked()
|
||||
return
|
||||
if requester not in present:
|
||||
RESERVATIONS.reserve(alias, requester, now)
|
||||
present = livekit_present_users(alias)
|
||||
if should_block(limit, present, requester):
|
||||
log(f"blocked {requester or '?'} from {room_id}: {len(present)}/{limit}")
|
||||
self._send_blocked()
|
||||
return
|
||||
except Exception as exc:
|
||||
# Fail open on the limit only; still apply the policy below.
|
||||
log(f"limit check error for {room_id}: {exc}")
|
||||
|
||||
Reference in New Issue
Block a user