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 Feb 21, 2026
Conversation
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.
Merged
quinchs
approved these changes
Feb 21, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SendSelectProtocolwas passingUdpPort(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
RepopulateAudioStreamsAsyncfiresStreamCreatedbeforeOnConnectingAsyncsets up theDaveSessionManager, so the initial input streams get built withoutDaveDecryptStreamin the pipeline. Once MLS finishes and audio starts flowing, the still-encrypted frames hitOpusDecodeStreamdirectly →InvalidPacket.Fix: added
RebuildInputStreamsForDaveAsynconAudioClient— tears down existing streams and recreates them with the DAVE layer once the initial key exchange completes (PrepareProtocolTransitionAsyncwithInitTransitionId).Tested with:
dave=False→ rebuild →dave=True