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"
"time"
"git.stormux.org/storm/barnard/gumble/gumble/varint"
"git.stormux.org/storm/barnard/log"
)
@@ -78,20 +77,21 @@ func (c *Client) udpPingRoutine() {
}
// sendUDPPing sends a Mumble 1.5 native UDP ping.
// Uses standard protobuf varint encoding (not Mumble's custom varint).
func (c *Client) sendUDPPing() {
cs := udp15Client
if cs == nil || !cs.initialized || c.udpConn == nil {
return
}
// Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds).
var tmp [varint.MaxVarintLen]byte
// Type byte 0x01 = UDPPing, field 1 = timestamp (protobuf varint, milliseconds).
var tmp [10]byte // max protobuf varint size
var buf bytes.Buffer
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
n = varint.Encode(tmp[:], int64(time.Now().UnixMilli()))
n = pbEncodeVarint(tmp[:], uint64(time.Now().UnixMilli()))
buf.Write(tmp[:n]) // timestamp value
encrypted, err := cs.encrypt15(buf.Bytes())
+58 -13
View File
@@ -10,6 +10,42 @@ import (
"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.
//
@@ -22,31 +58,39 @@ import (
// encodeUDPAudio builds a MumbleUDP.Audio protobuf message.
// 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 {
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 {
n := varint.Encode(tmp[:], int64((3<<3)|0))
n := pbEncodeVarint(tmp[:], uint64((3<<3)|0))
buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(session))
n = pbEncodeVarint(tmp[:], uint64(session))
buf.Write(tmp[:n])
}
n := varint.Encode(tmp[:], int64((4<<3)|0))
n = pbEncodeVarint(tmp[:], uint64((4<<3)|0))
buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(frameNumber))
n = pbEncodeVarint(tmp[:], uint64(frameNumber))
buf.Write(tmp[:n])
if len(opusData) > 0 {
n := varint.Encode(tmp[:], int64((5<<3)|2))
n := pbEncodeVarint(tmp[:], uint64((5<<3)|2))
buf.Write(tmp[:n])
n = varint.Encode(tmp[:], int64(len(opusData)))
n = pbEncodeVarint(tmp[:], uint64(len(opusData)))
buf.Write(tmp[:n])
buf.Write(opusData)
}
if terminator {
n := varint.Encode(tmp[:], int64((16<<3)|0))
n := pbEncodeVarint(tmp[:], uint64((16<<3)|0))
buf.Write(tmp[:n])
n = varint.Encode(tmp[:], 1)
n = pbEncodeVarint(tmp[:], 1)
buf.Write(tmp[:n])
}
@@ -54,10 +98,11 @@ func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, termina
}
// 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) {
pos := 0
for pos < len(data) {
key, n := varint.Decode(data[pos:])
key, n := pbDecodeVarint(data[pos:])
if n <= 0 {
break
}
@@ -67,7 +112,7 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
switch wireType {
case 0: // varint
val, n := varint.Decode(data[pos:])
val, n := pbDecodeVarint(data[pos:])
if n <= 0 {
return
}
@@ -81,7 +126,7 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
terminator = val != 0
}
case 2: // length-delimited
length, n := varint.Decode(data[pos:])
length, n := pbDecodeVarint(data[pos:])
if n <= 0 {
return
}
@@ -495,7 +540,7 @@ func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) {
frameNum := nextFrameNumber()
// 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)...)
// Encrypt with 1.5 format.