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.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-09 02:40:48 -04:00
committed by Brandon McGinty
parent e6806a7521
commit e93437a212
+70 -13
View File
@@ -559,27 +559,37 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
log.Info("UDP15 #%d: ping response, ignoring", pktNum) log.Info("UDP15 #%d: ping response, ignoring", pktNum)
return 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 return
} }
// Parse MumbleUDP.Audio protobuf. // Legacy UDPVoiceOpus format: type byte has bits 5-7 = 4.
session, frameNum, opusData, terminator := decodeUDPAudio(plaintext) 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 { if len(opusData) == 0 && !terminator {
log.Info("UDP15 #%d: no opus data (session=%d frame=%d), skipping", pktNum, session, frameNum) log.Info("UDP15 #%d: no opus data (session=%d frame=%d), skipping", pktNum, session, frameNum)
return return
} }
// Find the user.
user := c.Users[session] user := c.Users[session]
if user == nil { if user == nil {
log.Warn("UDP15 #%d: unknown session %d", pktNum, session) log.Warn("UDP15 #%d: unknown session %d", pktNum, session)
return return
} }
// Get or create decoder.
decoder := user.decoder decoder := user.decoder
if decoder == nil { if decoder == nil {
codec := c.audioCodec codec := c.audioCodec
@@ -593,7 +603,6 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
} }
if terminator && len(opusData) == 0 { if terminator && len(opusData) == 0 {
// Decoder reset on terminator.
decoder.Reset() decoder.Reset()
log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name) log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name)
return return
@@ -603,9 +612,59 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
return 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 { if user.audioSequenceValid {
gap := int64(frameNum) - user.audioSequence gap := frameNum - user.audioSequence
if gap > 1 && gap < 100 { if gap > 1 && gap < 100 {
log.Info("UDP15 #%d: seq gap for %s: %d -> %d (loss=%d), generating PLC", log.Info("UDP15 #%d: seq gap for %s: %d -> %d (loss=%d), generating PLC",
pktNum, user.Name, user.audioSequence, frameNum, gap-1) pktNum, user.Name, user.audioSequence, frameNum, gap-1)
@@ -621,10 +680,9 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
return return
} }
} }
user.audioSequence = int64(frameNum) user.audioSequence = frameNum
user.audioSequenceValid = true user.audioSequenceValid = true
// Decode Opus.
pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize) pcm, err := decoder.Decode(opusData, AudioMaximumFrameSize)
if err != nil { if err != nil {
log.Warn("UDP15 #%d: Opus decode failed for %s: %v", pktNum, user.Name, err) 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)) log.Info("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm))
// Dispatch.
event := AudioPacket{ event := AudioPacket{
Client: c, Client: c,
Sender: user, Sender: user,
Target: &VoiceTarget{ID: 0}, Target: &VoiceTarget{ID: 0},
Sequence: int64(frameNum), Sequence: frameNum,
AudioBuffer: AudioBuffer(pcm), AudioBuffer: AudioBuffer(pcm),
} }
c.dispatchAudio(user, &event) c.dispatchAudio(user, &event)