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:
2026-06-17 21:38:27 +02:00
parent 118ca5129f
commit 2185d9d15c
3 changed files with 140 additions and 1 deletions

View File

@@ -102,8 +102,17 @@ void MediaRelay::on_udp_frame(const uint8_t* data, size_t len,
crypto_aead_chacha20poly1305_ietf_ABYTES);
}
// Copy header (re-use sender's header verbatim — ssrc, seq, ts pass through).
// Copy header (ssrc, ts, flags pass through for demux/playout), then rewrite the
// seq field to THIS recipient's next send counter. The media AEAD nonce is an
// implicit per-direction monotonic counter; open() reconstructs it from the seq in
// the header (the AAD). Since we re-seal with the recipient's send_crypto (its own
// counter), the verbatim sender seq would no longer match the nonce seal() uses and
// every relayed frame would fail auth. Set seq = peek_send_counter() BEFORE sealing
// so the header (which is the authenticated AAD) carries the matching counter.
std::memcpy(seal_buf_.data(), data, voicecat::net::kVoiceHeaderSize);
const uint64_t send_ctr = send_crypto->peek_send_counter();
seal_buf_[8] = static_cast<uint8_t>((send_ctr >> 8) & 0xFF);
seal_buf_[9] = static_cast<uint8_t>(send_ctr & 0xFF);
uint8_t* out_payload = seal_buf_.data() + voicecat::net::kVoiceHeaderSize;
long sealed_out = send_crypto->seal(