From e93437a2121d8f403aa141d19710df61ffe9b9c8 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (deepseek)" Date: Sun, 9 Aug 2026 02:40:48 -0400 Subject: [PATCH] Handle legacy UDPVoiceOpus format (type 0x80) inside 1.5 crypto The server sends audio with type byte 0x80 (legacy UDPVoiceOpus) inside the Mumble 1.5 native UDP crypto envelope. This is the legacy format: [session varint][seq varint][length varint][opus data]. Added handleLegacyUDPVoice to parse this format. Refactored dispatch into dispatchOpus15 + decodeAndDispatch for reuse by both 0x00 protobuf and 0x80 legacy paths. --- gumble/gumble/udp15.go | 83 +++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/gumble/gumble/udp15.go b/gumble/gumble/udp15.go index 6919f35..fe8907c 100644 --- a/gumble/gumble/udp15.go +++ b/gumble/gumble/udp15.go @@ -559,27 +559,37 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { log.Info("UDP15 #%d: ping response, ignoring", pktNum) return } - if msgType != 0x00 { - log.Warn("UDP15 #%d: unknown message type 0x%02x", pktNum, msgType) + + // MumbleUDP.Audio protobuf format (1.5 native). + if msgType == 0x00 { + session, frameNum, opusData, terminator := decodeUDPAudio(plaintext) + c.dispatchOpus15(pktNum, session, int64(frameNum), opusData, terminator) return } - // Parse MumbleUDP.Audio protobuf. - session, frameNum, opusData, terminator := decodeUDPAudio(plaintext) + // Legacy UDPVoiceOpus format: type byte has bits 5-7 = 4. + if (msgType >> 5) == 4 { + c.handleLegacyUDPVoice(pktNum, plaintext) + return + } + log.Warn("UDP15 #%d: unknown message type 0x%02x", pktNum, msgType) +} + +// 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) { if len(opusData) == 0 && !terminator { log.Info("UDP15 #%d: no opus data (session=%d frame=%d), skipping", pktNum, session, frameNum) return } - // Find the user. user := c.Users[session] if user == nil { log.Warn("UDP15 #%d: unknown session %d", pktNum, session) return } - // Get or create decoder. decoder := user.decoder if decoder == nil { codec := c.audioCodec @@ -593,7 +603,6 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { } if terminator && len(opusData) == 0 { - // Decoder reset on terminator. decoder.Reset() log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name) return @@ -603,9 +612,59 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { return } - // Detect sequence gaps via frame_number. + c.decodeAndDispatch(pktNum, user, decoder, frameNum, opusData) +} + +// handleLegacyUDPVoice parses the legacy UDPVoice format (type byte 0x80) +// inside a 1.5-decrypted payload. +func (c *Client) handleLegacyUDPVoice(pktNum uint64, data []byte) { + pos := 0 + + // Session varint. + session, n := varint.Decode(data[pos:]) + if n <= 0 { + log.Warn("UDP15 #%d: legacy session varint decode failed", pktNum) + return + } + pos += n + + // Sequence varint. + seq, n := varint.Decode(data[pos:]) + if n <= 0 { + log.Warn("UDP15 #%d: legacy seq varint decode failed", pktNum) + return + } + pos += n + + // Length varint (bit 13 = terminator). + length, n := varint.Decode(data[pos:]) + if n <= 0 { + log.Warn("UDP15 #%d: legacy length varint decode failed", pktNum) + return + } + pos += n + + terminator := (length & 0x2000) != 0 + audioLen := int(length &^ 0x2000) + if audioLen > len(data)-pos { + log.Warn("UDP15 #%d: legacy audio length %d > remaining %d", + pktNum, audioLen, len(data)-pos) + return + } + + opusData := data[pos : pos+audioLen] + + log.Info("UDP15 #%d: legacy voice session=%d seq=%d opus_len=%d term=%v", + pktNum, session, seq, len(opusData), terminator) + + c.dispatchOpus15(pktNum, uint32(session), seq, opusData, terminator) +} + +// decodeAndDispatch decodes an Opus frame and dispatches PCM to audio listeners. +func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecoder, frameNum int64, opusData []byte) { + // Detect sequence gaps. if user.audioSequenceValid { - gap := int64(frameNum) - user.audioSequence + gap := frameNum - user.audioSequence if gap > 1 && gap < 100 { log.Info("UDP15 #%d: seq gap for %s: %d -> %d (loss=%d), generating PLC", pktNum, user.Name, user.audioSequence, frameNum, gap-1) @@ -621,10 +680,9 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { return } } - user.audioSequence = int64(frameNum) + user.audioSequence = frameNum user.audioSequenceValid = true - // Decode Opus. pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize) if err != nil { log.Warn("UDP15 #%d: Opus decode failed for %s: %v", pktNum, user.Name, err) @@ -634,12 +692,11 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) { log.Info("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm)) - // Dispatch. event := AudioPacket{ Client: c, Sender: user, Target: &VoiceTarget{ID: 0}, - Sequence: int64(frameNum), + Sequence: frameNum, AudioBuffer: AudioBuffer(pcm), } c.dispatchAudio(user, &event)