Skip to content

Fix voice receive: wrong port in SelectProtocol + missing DAVE decrypt layer on early streams#3241

Merged
quinchs merged 2 commits intodiscord-net:voice/big-dave-supportfrom
brunoamancio:fix/voice-port-and-dave-streams
Feb 21, 2026
Merged

Fix voice receive: wrong port in SelectProtocol + missing DAVE decrypt layer on early streams#3241
quinchs merged 2 commits intodiscord-net:voice/big-dave-supportfrom
brunoamancio:fix/voice-port-and-dave-streams

Conversation

@brunoamancio
Copy link
Copy Markdown
Contributor

@brunoamancio brunoamancio commented Feb 21, 2026

Ran into two issues getting audio receive to work on this branch:

1. SelectProtocol sends local port instead of external port

UDP discovery gives back the external IP and port as seen by the voice server, but SendSelectProtocol was passing UdpPort (local socket port) instead of the external port from the discovery bytes. Works fine without NAT since they're the same, but behind NAT the voice server ends up sending audio to the wrong port.

Fix: extract external port from bytes 72-73 of the discovery response and pass it through.

2. Input streams created before DAVE is ready

RepopulateAudioStreamsAsync fires StreamCreated before OnConnectingAsync sets up the DaveSessionManager, so the initial input streams get built without DaveDecryptStream in the pipeline. Once MLS finishes and audio starts flowing, the still-encrypted frames hit OpusDecodeStream directly → InvalidPacket.

Fix: added RebuildInputStreamsForDaveAsync on AudioClient — tears down existing streams and recreates them with the DAVE layer once the initial key exchange completes (PrepareProtocolTransitionAsync with InitTransitionId).

Tested with:

  • Voice connection behind NAT
  • Full receive pipeline working: sodium → RTP → DAVE decrypt → opus → PCM (322 frames decrypted, 1 failed on the very first unencrypted frame)
  • Streams correctly go from dave=False → rebuild → dave=True

The UDP discovery response from Discord contains the external IP and
port as seen by the voice server. SendSelectProtocol was passing
UdpPort (the local socket port) instead of the external port from
the discovery packet. Behind NAT, these differ, causing the voice
server to send audio to the wrong port.

Extract the external port from bytes 72-73 of the discovery response
and pass it to SendSelectProtocol alongside the external IP.
When a user joins a voice channel, SocketGuild.RepopulateAudioStreamsAsync
fires StreamCreated before AudioClient.OnConnectingAsync initializes
the DaveSessionManager. This means input streams are created without
the DaveDecryptStream layer in the pipeline.

Once the MLS key exchange completes and the initial transition fires,
the encrypted audio reaches OpusDecodeStream directly — causing
InvalidPacket errors since the data is still DAVE-encrypted.

Add RebuildInputStreamsForDaveAsync to AudioClient which tears down
existing streams and recreates them with the DaveDecryptStream layer.
Call it from DaveSessionManager.PrepareProtocolTransitionAsync when
the initial transition (transitionId == InitTransitionId) completes.
@brunoamancio brunoamancio changed the title Fix voice connection: external port in SelectProtocol + rebuild streams after DAVE init Fix voice receive: wrong port in SelectProtocol + missing DAVE decrypt layer on early streams Feb 21, 2026
@quinchs quinchs merged commit 9f832ba into discord-net:voice/big-dave-support Feb 21, 2026
2 checks passed
quinchs added a commit that referenced this pull request Feb 22, 2026
* create bindings project

* adjust structure

* spite misha

* integrate dave into audio client

* working send/receive audio with dave

* add doc section about libdave

* make voice api model internal

* add xml docs to new close codes

* document new voice op codes

* add xml docs to the libdave binding

* fix docs samples

* Update tools/Discord.Net.Dave/Utils.cs

Co-authored-by: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com>

* cap. snake case -> pascal case for constants

* switch to hard cast of JToken

* add PATH to libdave docs

* ensure log sink isn't gc collected

* improve thread and memory safety

* rename constant to pascal case

* remove 'CChar' alias

* use ext. and add project imports

* add dave to gh workflow

* fix incorrect opcode for dave

* add logs for MLS failure

* add docs to libdave binding

* update to newer libdave api

* update to latest libdave

* update to latest libdave spec

* (mostly) working

* ok its fixed

* update libdave docs

* remove comments

* ensure previous dave sessions are cleaned

* readd set speaking call

* update log to be synchronous

* grammar

* return rented array to pool

* doc new streams and fix warning message

* Fix voice receive: wrong port in SelectProtocol + missing DAVE decrypt layer on early streams (#3241)

* Fix SelectProtocol sending local port instead of external port

The UDP discovery response from Discord contains the external IP and
port as seen by the voice server. SendSelectProtocol was passing
UdpPort (the local socket port) instead of the external port from
the discovery packet. Behind NAT, these differ, causing the voice
server to send audio to the wrong port.

Extract the external port from bytes 72-73 of the discovery response
and pass it to SendSelectProtocol alongside the external IP.

* Rebuild input streams after initial DAVE key exchange

When a user joins a voice channel, SocketGuild.RepopulateAudioStreamsAsync
fires StreamCreated before AudioClient.OnConnectingAsync initializes
the DaveSessionManager. This means input streams are created without
the DaveDecryptStream layer in the pipeline.

Once the MLS key exchange completes and the initial transition fires,
the encrypted audio reaches OpusDecodeStream directly — causing
InvalidPacket errors since the data is still DAVE-encrypted.

Add RebuildInputStreamsForDaveAsync to AudioClient which tears down
existing streams and recreates them with the DaveDecryptStream layer.
Call it from DaveSessionManager.PrepareProtocolTransitionAsync when
the initial transition (transitionId == InitTransitionId) completes.

* clean up some memory related things

* more memory related improvements

---------

Co-authored-by: Mihail Gribkov <61027276+Misha-133@users.noreply.github.com>
Co-authored-by: Th3B0Y <brunoamancio.ti@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants