Retain TCP audio until UDP is authenticated

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 14:45:41 -04:00
committed by Brandon McGinty
parent 1fa96d4f15
commit 25467405b8
5 changed files with 34 additions and 3 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ Priority 0: security and crashers
Priority 1: transport, lifecycle, and correctness
---------------------------------------------------
8. TCP audio is discarded before UDP is proven usable
[x] 8. TCP audio is discarded before UDP is proven usable
Files: gumble/gumble/crypt.go, gumble/gumble/client.go, gumble/gumble/udp.go
udpActive is set immediately after CryptSetup. The TCP read routine then
discards UDPTunnel packets even if inbound UDP is blocked or NAT setup has
+1
View File
@@ -74,6 +74,7 @@ type Client struct {
udpMu sync.RWMutex
udpWriteMu sync.Mutex
udpConn *net.UDPConn
udpStarted bool
udpActive bool
udpCryptoOut *cryptState15
udpCryptoIn *cryptState15
+4 -2
View File
@@ -317,9 +317,11 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
c.udpMu.Lock()
udpReady := c.udpCryptoOut != nil
startUDP := c.cryptOut.initialized && c.udpConn != nil && !c.udpActive && udpReady
startUDP := c.cryptOut.initialized && c.udpConn != nil && !c.udpStarted && udpReady
if startUDP {
c.udpActive = true
// Keep TCP tunnelling enabled until an authenticated UDP packet proves
// that the inbound path works.
c.udpStarted = true
}
noUDPConn := c.udpConn == nil
c.udpMu.Unlock()
+9
View File
@@ -662,6 +662,7 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
}
log.Debug("UDP15 #%d: decrypt OK, plaintext_len=%d", pktNum, len(plaintext))
c.markUDPActive()
// Check type byte (0x00 = Audio, 0x01 = Ping)
if len(plaintext) < 1 {
@@ -692,6 +693,14 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
log.Warn("UDP15 #%d: unknown message type 0x%02x", pktNum, msgType)
}
// markUDPActive switches outgoing audio to UDP only after authentication has
// proved that packets can return through the network path.
func (c *Client) markUDPActive() {
c.udpMu.Lock()
c.udpActive = true
c.udpMu.Unlock()
}
// dispatchOpus15 processes a decoded MumbleUDP.Audio frame and dispatches
// the decoded PCM to audio listeners.
func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, opusData []byte, terminator bool, context uint32, position *[3]float32, volumeAdjustment float32) {
@@ -0,0 +1,19 @@
package gumble
import "testing"
// Regression: CryptSetup selected UDP before any authenticated packet had
// returned, causing TCP audio to be discarded on blocked inbound UDP paths.
func TestUDPOnlyBecomesActiveAfterAuthenticatedResponse(t *testing.T) {
c := &Client{}
if c.udpActive {
t.Fatal("new transport is unexpectedly active")
}
c.markUDPActive()
c.udpMu.RLock()
active := c.udpActive
c.udpMu.RUnlock()
if !active {
t.Fatal("authenticated UDP response did not activate UDP")
}
}