Align UDP code with udp_encryption.md spec

- Bump ClientVersion from 1.3.0 to 1.5.0 so the server sends
  protobuf-format MumbleUDP.Audio (type 0x00) instead of legacy
  format (type 0x80). HandleUDPPacket15 already decodes both.

- Remove dead WriteAudioUDP and handleUDPPacket from udp.go.
  These used the old cryptOut/cryptIn state with counter-based
  OCB nonces, which was superseded by the 1.5 cryptState15
  state in udp15.go. The active paths are WriteAudioUDP15
  (outbound) and HandleUDPPacket15 (inbound).

- Remove unused udpPacketTypeOpus and udpPingInterval constants.

- Clean up stale comment in udp15.go.
This commit is contained in:
Brandon McGinty (deepseek)
2026-08-09 02:57:52 -04:00
committed by Brandon McGinty
parent cd32abaaef
commit e61ebdae38
3 changed files with 1 additions and 193 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ const (
)
// ClientVersion is the protocol version that Client implements.
const ClientVersion = 1<<16 | 3<<8 | 0
const ClientVersion = 1<<16 | 5<<8 | 0
// Client is the type used to create a connection to a server.
type Client struct {
-189
View File
@@ -2,10 +2,7 @@ package gumble
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"math"
"net"
"time"
@@ -14,12 +11,6 @@ import (
)
const (
// udpPacketTypeOpus is the audio type byte for Opus over UDP.
udpPacketTypeOpus = 4
// udpPingInterval is how often UDP ping packets are sent.
udpPingInterval = 5 * time.Second
// maxUDPPacketSize is the maximum UDP packet size we'll process.
maxUDPPacketSize = 1024
)
@@ -110,183 +101,3 @@ func (c *Client) sendUDPPing() {
c.udpConn.Write(encrypted)
}
// WriteAudioUDP writes an encrypted audio packet over UDP.
// Returns true if the packet was sent over UDP, false if TCP should be used.
var firstUDPSendLogged bool
var udpSendCount uint64
func (c *Client) WriteAudioUDP(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) (bool, error) {
if c.udpConn == nil || !c.cryptOut.initialized {
return false, nil
}
udpSendCount++
// Build the unencrypted header
var header [1 + varint.MaxVarintLen*2]byte
header[0] = (format << 5) | target
n := varint.Encode(header[1:], int64(c.Self.Session))
if n == 0 {
return false, errors.New("gumble: varint out of range")
}
m := varint.Encode(header[1+n:], sequence)
if m == 0 {
return false, errors.New("gumble: varint out of range")
}
headerLen := 1 + n + m
// Build the unencrypted payload (length varint + opus data + positional)
l := int64(len(data))
if final {
l |= 0x2000
}
var payload [varint.MaxVarintLen]byte
p := varint.Encode(payload[:], l)
if p == 0 {
return false, errors.New("gumble: varint out of range")
}
positionalLen := 0
if X != nil {
positionalLen = 3 * 4
}
// Combine payload for encryption: length varint + opus data + positional
plainPayload := make([]byte, p+len(data)+positionalLen)
copy(plainPayload, payload[:p])
copy(plainPayload[p:], data)
if positionalLen > 0 {
binary.LittleEndian.PutUint32(plainPayload[p+len(data):], math.Float32bits(*X))
binary.LittleEndian.PutUint32(plainPayload[p+len(data)+4:], math.Float32bits(*Y))
binary.LittleEndian.PutUint32(plainPayload[p+len(data)+8:], math.Float32bits(*Z))
}
// Encrypt the payload
c.cryptOut.mu.Lock()
counter := c.cryptOut.counter
c.cryptOut.counter++
nonce := c.cryptOut.nonceForPacket(counter)
c.cryptOut.mu.Unlock()
log.Info("UDP send #%d: seq=%d counter=%d nonce=%s plain_len=%d opus_len=%d final=%v",
udpSendCount, sequence, counter, hex.EncodeToString(nonce[:]),
len(plainPayload), len(data), final)
encrypted, err := c.cryptOut.encrypt(counter, plainPayload)
if err != nil {
log.Error("UDP send #%d: encrypt FAILED: %v", udpSendCount, err)
return false, err
}
// Send: header (plain) + encrypted payload
packet := make([]byte, headerLen+len(encrypted))
copy(packet, header[:headerLen])
copy(packet[headerLen:], encrypted)
log.Info("UDP send #%d: header_len=%d enc_len=%d total=%d hex=%s",
udpSendCount, headerLen, len(encrypted), len(packet),
hex.EncodeToString(packet))
_, err = c.udpConn.Write(packet)
if err != nil {
log.Error("UDP send #%d: write FAILED: %v", udpSendCount, err)
return false, err
}
return true, nil
}
// handleUDPPacket processes an incoming UDP audio packet.
func (c *Client) handleUDPPacket(packet []byte, pktNum uint64) {
if len(packet) < 1 {
log.Warn("UDP #%d: empty packet, ignoring", pktNum)
return
}
typeByte := packet[0]
audioType := (typeByte >> 5) & 0x7
target := typeByte & 0x1F
log.Info("UDP #%d: type_byte=0x%02x audio_type=%d target=%d pkt_len=%d",
pktNum, typeByte, audioType, target, len(packet))
// Skip ping packets (type 0, 1, or 2 in bits 5-7 = type 0)
if audioType != udpPacketTypeOpus {
log.Info("UDP #%d: skipping non-Opus packet (audio_type=%d)", pktNum, audioType)
return
}
// Find the user by session (in plaintext header)
buf := packet[1:]
session, n := varint.Decode(buf)
if n <= 0 {
log.Warn("UDP #%d: failed to decode session varint (buf_len=%d first_byte=0x%02x)",
pktNum, len(buf), buf[0])
return
}
buf = buf[n:]
// Parse sequence for decryption nonce
seq, m := varint.Decode(buf)
if m <= 0 {
log.Warn("UDP #%d: failed to decode seq varint (buf_len=%d)", pktNum, len(buf))
return
}
headerLen := 1 + n + m
encrypted := packet[headerLen:]
log.Info("UDP #%d: session=%d seq=%d header_len=%d encrypted_len=%d",
pktNum, session, seq, headerLen, len(encrypted))
user := c.Users[uint32(session)]
if user == nil {
log.Warn("UDP #%d: unknown session %d (known sessions: %d)",
pktNum, session, len(c.Users))
return
}
log.Info("UDP #%d: user=%s", pktNum, user.Name)
// Decrypt the payload
if !c.cryptIn.initialized {
log.Warn("UDP #%d: cryptIn not initialized, dropping", pktNum)
return
}
c.cryptIn.mu.Lock()
counter := c.cryptIn.counter
c.cryptIn.counter++
nonce := c.cryptIn.nonceForPacket(counter)
c.cryptIn.mu.Unlock()
log.Info("UDP #%d: decrypting with counter=%d nonce=%s encrypted_hex=%s",
pktNum, counter, hex.EncodeToString(nonce[:]), hex.EncodeToString(encrypted))
plaintext, err := c.cryptIn.decrypt(counter, encrypted)
if err != nil {
log.Warn("UDP #%d: decrypt FAILED for %s (counter=%d): %v",
pktNum, user.Name, counter, err)
return
}
log.Info("UDP #%d: decrypt OK, plaintext_len=%d hex=%s",
pktNum, len(plaintext), hex.EncodeToString(plaintext))
// Now feed the decrypted payload through the existing TCP audio handler.
// We reconstruct the full UDPTunnel format: type byte + session + seq + payload.
// The handleUDPTunnel expects: [type/target byte] [session varint] [seq varint] [length varint] [data] [pos]
// Our decrypted payload is: [length varint] [data] [pos]
// We need to prepend the type byte, session, and seq.
fullPacket := make([]byte, 1+n+m+len(plaintext))
fullPacket[0] = packet[0]
varint.Encode(fullPacket[1:], session)
varint.Encode(fullPacket[1+n:], seq)
copy(fullPacket[1+n+m:], plaintext)
log.Info("UDP #%d: dispatching to handleUDPTunnel (fullPacket_len=%d)",
pktNum, len(fullPacket))
c.handleUDPTunnel(fullPacket)
}
-3
View File
@@ -478,9 +478,6 @@ func nextFrameNumber() uint32 {
}
// ---------------------------------------------------------------------------
// Replace the WriteAudioUDP path for 1.5 native format.
// ---------------------------------------------------------------------------
// WriteAudioUDP15 writes an encrypted audio packet using Mumble 1.5 native UDP.
// Returns true if sent, false if TCP should be used.
func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) {