fix(media): rewrite relay re-seal seq so multi-user audio decrypts
The media AEAD nonce is an implicit per-direction monotonic counter; open() reconstructs it from the 14-byte header seq field (the AAD), so the contract is header.seq == the counter seal() used. The SFU relay decrypted inbound frames with the sender key, re-sealed with the recipient send_crypto (its own counter), but forwarded the sender header verbatim -- so seq carried the wrong counter and the recipient rebuilt the wrong nonce, silently dropping every relayed frame. It only worked for a single first-ever sender into a fresh recipient, which is why reverse/3rd-party audio failed. Rewrite the outgoing header seq to the recipient peek_send_counter() before re-sealing so each server->client direction is one contiguous monotonic counter and the nonce always matches. Safe: the jitter buffer orders by timestamp, not seq. No wire-format/proto/ABI change. Adds test_relay_interleaved_reseal regression coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
26
PROGRESS.md
26
PROGRESS.md
@@ -24,6 +24,32 @@ up instantly. Newest status at the top.
|
||||
channel CRUD with full per-channel Opus audio config, user moderation (kick/ban/move/server
|
||||
mute/server deafen/set permissions), and server account management. `dotnet test` of the
|
||||
Windows solution passes. Still to do: DRED/audio-quality polish.
|
||||
- **Done:** **Fixed multi-user voice — relayed frames failed AEAD decryption (nonce desync)**
|
||||
(2026-06-17, reported live: with 2+ people in a channel, audio was one-directional — "I can
|
||||
hear them but they can't hear me" — and a 3rd joiner heard nobody). Root cause was in the SFU
|
||||
relay (`server/src/media_relay.cpp`). The media AEAD nonce is an *implicit per-direction
|
||||
monotonic counter*; `open()` reconstructs it from the 14-byte header's `seq` field (the AAD),
|
||||
so the wire contract is `header.seq == the counter seal() used` (the client honors this at
|
||||
`client.cpp:907`). The relay decrypted each inbound frame with the sender's key, then re-sealed
|
||||
with the **recipient's** `send_crypto` (its own counter) but **forwarded the sender's header
|
||||
verbatim** — so `header.seq` carried the sender's counter, not the recipient's. The recipient's
|
||||
`open()` rebuilt the wrong nonce → every relayed frame failed auth and was silently dropped. It
|
||||
only "worked" while the sender's counter coincidentally equalled the server→recipient counter
|
||||
(a single first-ever sender into a fresh recipient), which is exactly why the first/sole talker
|
||||
was heard but reverse/3rd-party audio was not. **Fix:** before re-sealing, the relay rewrites the
|
||||
outgoing header's `seq` (bytes [8..9]) to the recipient's `peek_send_counter()`, so each
|
||||
server→client direction is one contiguous monotonic counter and the nonce always matches (the
|
||||
anti-replay window also stops seeing false replays from interleaved senders). Safe because the
|
||||
jitter buffer orders by `timestamp`, not `seq` (`audio_engine.h`); `seq` exists only to carry the
|
||||
AEAD counter. No wire-format/proto/ABI change. Regression test added in `tests/test_media_aead.cpp`
|
||||
(`test_relay_interleaved_reseal`): two senders interleaved into one recipient all decrypt with the
|
||||
fix, and the verbatim-seq path is asserted to fail. `ctest --preset m1-dev` — **18/18 green** (run
|
||||
via PowerShell; Git Bash can't resolve the runtime DLLs. `vad_ptt_devices` is timing-flaky over
|
||||
loopback — passes on re-run — unrelated to this fix). **Latent, separate:** the secondary
|
||||
"3rd joiner sometimes can't see other users" report is a control-plane (TCP snapshot/UserEvent)
|
||||
issue, not this AEAD bug — re-verify after live testing before investigating. Also still latent:
|
||||
the 16-bit `seq` wraps after 65536 frames per direction (faster on a busy relay) with no ROC, so
|
||||
the implicit counter desyncs on long continuous sessions (`crypto.cpp` open() TODO).
|
||||
- **Done:** **Fixed "randomly bumped to Lobby" in the Windows client — actors were excluded
|
||||
from their own state-change broadcasts** (2026-06-17, reported live: a connected client would
|
||||
intermittently snap from its joined channel back to Lobby in the UI). Root cause was a design
|
||||
|
||||
Reference in New Issue
Block a user