Fix protobuf varint encoding in 1.5 UDP protocol

The 1.5 UDP protocol uses standard Google protobuf varint encoding,
but encodeUDPAudio, decodeUDPAudio, and sendUDPPing were using
Mumble's custom varint (from the legacy UDP format).  These are
different encodings for values >= 128, which includes:
- The field-16 (is_terminator) tag: (16<<3)|0 = 128
- Opus data lengths > 127
- Ping timestamps

The server's protobuf parser (ParseFromArray) would see garbage
lengths (e.g. 622 bytes of opus data would be decoded as a 14082-byte
field, causing parse failure).  Packets were silently dropped.

Also add the target field (field 1, value 0 = normal speech) to
outbound audio, matching the reference client's encodeAudioPacket_protobuf.

Also bump ClientVersion from 1.3.0 to 1.5.0 so the server uses the
protobuf decoder for our audio packets instead of the legacy decoder.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-09 03:09:40 -04:00
committed by Brandon McGinty
parent e61ebdae38
commit 0f2eb4408f
2 changed files with 63 additions and 18 deletions
+5 -5
View File
@@ -6,7 +6,6 @@ import (
"net" "net"
"time" "time"
"git.stormux.org/storm/barnard/gumble/gumble/varint"
"git.stormux.org/storm/barnard/log" "git.stormux.org/storm/barnard/log"
) )
@@ -78,20 +77,21 @@ func (c *Client) udpPingRoutine() {
} }
// sendUDPPing sends a Mumble 1.5 native UDP ping. // sendUDPPing sends a Mumble 1.5 native UDP ping.
// Uses standard protobuf varint encoding (not Mumble's custom varint).
func (c *Client) sendUDPPing() { func (c *Client) sendUDPPing() {
cs := udp15Client cs := udp15Client
if cs == nil || !cs.initialized || c.udpConn == nil { if cs == nil || !cs.initialized || c.udpConn == nil {
return return
} }
// Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds). // Type byte 0x01 = UDPPing, field 1 = timestamp (protobuf varint, milliseconds).
var tmp [varint.MaxVarintLen]byte var tmp [10]byte // max protobuf varint size
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteByte(0x01) // type = UDPPing buf.WriteByte(0x01) // type = UDPPing
n := varint.Encode(tmp[:], int64((1<<3)|0)) n := pbEncodeVarint(tmp[:], uint64((1<<3)|0))
buf.Write(tmp[:n]) // field 1 tag buf.Write(tmp[:n]) // field 1 tag
n = varint.Encode(tmp[:], int64(time.Now().UnixMilli())) n = pbEncodeVarint(tmp[:], uint64(time.Now().UnixMilli()))
buf.Write(tmp[:n]) // timestamp value buf.Write(tmp[:n]) // timestamp value
encrypted, err := cs.encrypt15(buf.Bytes()) encrypted, err := cs.encrypt15(buf.Bytes())
+58 -13
View File
@@ -10,6 +10,42 @@ import (
"git.stormux.org/storm/barnard/log" "git.stormux.org/storm/barnard/log"
) )
// ---------------------------------------------------------------------------
// Protobuf varint helpers.
// The 1.5 UDP protocol uses standard Google protobuf varint encoding,
// NOT Mumble's custom varint (which is used in the legacy UDP format).
// The Mumble custom varint is in ../varint/; protobuf varint is below.
// ---------------------------------------------------------------------------
// pbEncodeVarint writes v as a protobuf varint into buf and returns the
// number of bytes written. buf must have sufficient space (10 bytes for
// a full uint64).
func pbEncodeVarint(buf []byte, v uint64) int {
i := 0
for v >= 0x80 {
buf[i] = byte(v) | 0x80
v >>= 7
i++
}
buf[i] = byte(v)
return i + 1
}
// pbDecodeVarint reads a protobuf varint from buf and returns the value
// and the number of bytes consumed (0 on error).
func pbDecodeVarint(buf []byte) (uint64, int) {
var v uint64
var s uint
for i, b := range buf {
v |= uint64(b&0x7F) << s
if b < 0x80 {
return v, i + 1
}
s += 7
}
return 0, 0 // truncated
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mumble 1.5 native UDP — MumbleUDP.Audio protobuf helpers. // Mumble 1.5 native UDP — MumbleUDP.Audio protobuf helpers.
// //
@@ -22,31 +58,39 @@ import (
// encodeUDPAudio builds a MumbleUDP.Audio protobuf message. // encodeUDPAudio builds a MumbleUDP.Audio protobuf message.
// If session == 0, sender_session is omitted (used for outbound). // If session == 0, sender_session is omitted (used for outbound).
// Uses standard protobuf varint encoding, not Mumble's custom varint.
func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, terminator bool) []byte { func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, terminator bool) []byte {
var buf bytes.Buffer var buf bytes.Buffer
var tmp [varint.MaxVarintLen]byte var tmp [10]byte // max protobuf varint size
// Field 1: target = 0 (normal speech). Always encoded so the server
// sees the oneof Header choice, matching the reference client.
n := pbEncodeVarint(tmp[:], uint64((1<<3)|0))
buf.Write(tmp[:n])
n = pbEncodeVarint(tmp[:], 0)
buf.Write(tmp[:n])
if session != 0 { if session != 0 {
n := varint.Encode(tmp[:], int64((3<<3)|0)) n := pbEncodeVarint(tmp[:], uint64((3<<3)|0))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(session)) n = pbEncodeVarint(tmp[:], uint64(session))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
} }
n := varint.Encode(tmp[:], int64((4<<3)|0)) n = pbEncodeVarint(tmp[:], uint64((4<<3)|0))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(frameNumber)) n = pbEncodeVarint(tmp[:], uint64(frameNumber))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
if len(opusData) > 0 { if len(opusData) > 0 {
n := varint.Encode(tmp[:], int64((5<<3)|2)) n := pbEncodeVarint(tmp[:], uint64((5<<3)|2))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(len(opusData))) n = pbEncodeVarint(tmp[:], uint64(len(opusData)))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
buf.Write(opusData) buf.Write(opusData)
} }
if terminator { if terminator {
n := varint.Encode(tmp[:], int64((16<<3)|0)) n := pbEncodeVarint(tmp[:], uint64((16<<3)|0))
buf.Write(tmp[:n]) buf.Write(tmp[:n])
n = varint.Encode(tmp[:], 1) n = pbEncodeVarint(tmp[:], 1)
buf.Write(tmp[:n]) buf.Write(tmp[:n])
} }
@@ -54,10 +98,11 @@ func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, termina
} }
// decodeUDPAudio parses a MumbleUDP.Audio protobuf message. // decodeUDPAudio parses a MumbleUDP.Audio protobuf message.
// Uses standard protobuf varint decoding, not Mumble's custom varint.
func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData []byte, terminator bool) { func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData []byte, terminator bool) {
pos := 0 pos := 0
for pos < len(data) { for pos < len(data) {
key, n := varint.Decode(data[pos:]) key, n := pbDecodeVarint(data[pos:])
if n <= 0 { if n <= 0 {
break break
} }
@@ -67,7 +112,7 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
switch wireType { switch wireType {
case 0: // varint case 0: // varint
val, n := varint.Decode(data[pos:]) val, n := pbDecodeVarint(data[pos:])
if n <= 0 { if n <= 0 {
return return
} }
@@ -81,7 +126,7 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
terminator = val != 0 terminator = val != 0
} }
case 2: // length-delimited case 2: // length-delimited
length, n := varint.Decode(data[pos:]) length, n := pbDecodeVarint(data[pos:])
if n <= 0 { if n <= 0 {
return return
} }
@@ -495,7 +540,7 @@ func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) {
frameNum := nextFrameNumber() frameNum := nextFrameNumber()
// Build MumbleUDP.Audio: type byte 0x00 + protobuf. // Build MumbleUDP.Audio: type byte 0x00 + protobuf.
// Matches wumble: Bytes[0_u8] + Protobuf.field(4, ...) + Protobuf.bytes(5, ...) // Matches reference client: 0x00 + target=0 + frame_number + opus_data [+ is_terminator]
payload := append([]byte{0x00}, encodeUDPAudio(0, frameNum, data, final)...) payload := append([]byte{0x00}, encodeUDPAudio(0, frameNum, data, final)...)
// Encrypt with 1.5 format. // Encrypt with 1.5 format.