Switch UDP to Mumble 1.5 native protocol (matching wumble)
The old gumble library uses the legacy UDPVoice format: [plaintext header: type|target, session, seq] [OCB2 encrypted payload] Modern Murmur 1.5+ servers speak the native UDP protocol: [iv_byte(1)] [tag(3)] [OCB encrypted MumbleUDP.Audio protobuf] This is why zero incoming UDP packets were seen — the server silently dropped all legacy-format datagrams. Changes: - New udp15.go: MumbleUDP.Audio protobuf encode/decode, OCB encrypt/decrypt matching wumble's implementation, cryptState15 with IV-increment nonce and replay-protection history - udp.go: ping uses 1.5 native format (type 0x01, encrypted protobuf), reader delegates to HandleUDPPacket15 - client.go: WriteAudio tries WriteAudioUDP15 first - crypt.go: SetUDP15Crypto called from handleCryptSetup OCB variant matches wumble's crypt_state.cr exactly: full 16-byte nonce (incremented IV), no associated data, final block padding with byte[15] = remaining*8.
This commit is contained in:
committed by
Brandon McGinty
parent
e1468cd204
commit
50987d412c
@@ -334,11 +334,11 @@ func (c *Client) EnableStereoEncoder() {
|
||||
var udpFallbackLogged bool
|
||||
|
||||
func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error {
|
||||
// Try UDP first (unless disabled)
|
||||
// Try Mumble 1.5 native UDP first (unless disabled)
|
||||
if !c.Config.DisableUDP {
|
||||
if sent, err := c.WriteAudioUDP(format, target, sequence, final, data, X, Y, Z); sent {
|
||||
if sent, err := c.WriteAudioUDP15(data, final); sent {
|
||||
if err != nil {
|
||||
log.Error("UDP send error: %v", err)
|
||||
log.Error("UDP15 send error: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -350,8 +350,8 @@ func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, dat
|
||||
log.Info("UDP disabled, audio using TCP tunnel")
|
||||
} else if c.udpConn == nil {
|
||||
log.Info("no UDP socket, audio using TCP tunnel")
|
||||
} else if !c.cryptOut.initialized {
|
||||
log.Info("waiting for CryptSetup, audio using TCP tunnel")
|
||||
} else if udp15Client == nil || !udp15Client.initialized {
|
||||
log.Info("1.5 crypto not ready, audio using TCP tunnel")
|
||||
}
|
||||
}
|
||||
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
||||
|
||||
+13
-4
@@ -301,6 +301,10 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
||||
wasInit := c.cryptOut.initialized
|
||||
c.cryptOut.setup(packet.Key, packet.ClientNonce)
|
||||
c.cryptIn.setup(packet.Key, packet.ServerNonce)
|
||||
|
||||
// Also set up Mumble 1.5 native UDP crypto.
|
||||
SetUDP15Crypto(packet.Key, packet.ClientNonce, packet.ServerNonce)
|
||||
|
||||
if wasInit {
|
||||
log.Debug("CryptSetup updated (key rotation)")
|
||||
} else {
|
||||
@@ -313,10 +317,15 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
||||
}
|
||||
|
||||
if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive {
|
||||
c.udpActive = true
|
||||
log.Info("UDP crypto ready, starting UDP reader and pinger")
|
||||
go c.udpReadRoutine()
|
||||
go c.udpPingRoutine()
|
||||
// Only start UDP reader if 1.5 crypto is also ready.
|
||||
if udp15Client != nil && udp15Client.initialized {
|
||||
c.udpActive = true
|
||||
log.Info("UDP crypto ready (1.5 native), starting UDP reader and pinger")
|
||||
go c.udpReadRoutine()
|
||||
go c.udpPingRoutine()
|
||||
} else {
|
||||
log.Info("legacy crypto ready, waiting for 1.5 crypto before starting UDP")
|
||||
}
|
||||
} else if c.cryptOut.initialized && c.udpConn == nil {
|
||||
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
|
||||
}
|
||||
|
||||
+28
-16
@@ -1,6 +1,7 @@
|
||||
package gumble
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -48,8 +49,9 @@ func (c *Client) startUDP() error {
|
||||
}
|
||||
|
||||
// udpReadRoutine reads encrypted UDP audio packets from the server.
|
||||
// Uses Mumble 1.5 native UDP format.
|
||||
func (c *Client) udpReadRoutine() {
|
||||
log.Info("UDP reader started")
|
||||
log.Info("UDP reader started (1.5 native format)")
|
||||
buf := make([]byte, maxUDPPacketSize)
|
||||
var packetCount uint64
|
||||
for {
|
||||
@@ -63,39 +65,49 @@ func (c *Client) udpReadRoutine() {
|
||||
packetCount, n, addr, hex.EncodeToString(buf[:n]))
|
||||
packet := make([]byte, n)
|
||||
copy(packet, buf[:n])
|
||||
c.handleUDPPacket(packet, packetCount)
|
||||
c.HandleUDPPacket15(packet, packetCount)
|
||||
}
|
||||
}
|
||||
|
||||
// udpPingRoutine sends periodic ping packets over UDP to keep the
|
||||
// connection alive and maintain NAT bindings.
|
||||
// connection alive and maintain NAT bindings. Uses Mumble 1.5 native
|
||||
// UDP ping format: type byte 0x01, protobuf field 1 = timestamp.
|
||||
func (c *Client) udpPingRoutine() {
|
||||
ticker := time.NewTicker(udpPingInterval)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var counter uint32
|
||||
for {
|
||||
select {
|
||||
case <-c.end:
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.sendUDPPing(counter)
|
||||
counter++
|
||||
c.sendUDPPing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendUDPPing sends a UDP ping packet. The Mumble UDP ping is a type
|
||||
// byte (0x20) followed by a varint-encoded timestamp.
|
||||
func (c *Client) sendUDPPing(counter uint32) {
|
||||
if c.udpConn == nil {
|
||||
// sendUDPPing sends a Mumble 1.5 native UDP ping.
|
||||
func (c *Client) sendUDPPing() {
|
||||
cs := udp15Client
|
||||
if cs == nil || !cs.initialized || c.udpConn == nil {
|
||||
return
|
||||
}
|
||||
// Type byte 0x20 (ping) + varint timestamp
|
||||
var ping [1 + varint.MaxVarintLen]byte
|
||||
ping[0] = 0x20
|
||||
n := varint.Encode(ping[1:], int64(time.Now().UnixNano()))
|
||||
c.udpConn.Write(ping[:1+n])
|
||||
// Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds).
|
||||
var tmp [varint.MaxVarintLen]byte
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte(0x01) // type = UDPPing
|
||||
|
||||
n := varint.Encode(tmp[:], int64((1<<3)|0))
|
||||
buf.Write(tmp[:n]) // field 1 tag
|
||||
|
||||
n = varint.Encode(tmp[:], int64(time.Now().UnixMilli()))
|
||||
buf.Write(tmp[:n]) // timestamp value
|
||||
|
||||
encrypted, err := cs.encrypt15(buf.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.udpConn.Write(encrypted)
|
||||
}
|
||||
|
||||
// WriteAudioUDP writes an encrypted audio packet over UDP.
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
package gumble
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
||||
"git.stormux.org/storm/barnard/log"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mumble 1.5 native UDP — MumbleUDP.Audio protobuf helpers.
|
||||
//
|
||||
// Message MumbleUDP.Audio:
|
||||
// field 3: sender_session (varint, wire 0)
|
||||
// field 4: frame_number (varint, wire 0) — 10 ms units
|
||||
// field 5: opus_data (bytes, wire 2)
|
||||
// field 16: is_terminator (varint, wire 0)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// encodeUDPAudio builds a MumbleUDP.Audio protobuf message.
|
||||
// If session == 0, sender_session is omitted (used for outbound).
|
||||
func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, terminator bool) []byte {
|
||||
var buf bytes.Buffer
|
||||
var tmp [varint.MaxVarintLen]byte
|
||||
|
||||
if session != 0 {
|
||||
n := varint.Encode(tmp[:], int64((3<<3)|0))
|
||||
buf.Write(tmp[:n])
|
||||
n = varint.Encode(tmp[:], int64(session))
|
||||
buf.Write(tmp[:n])
|
||||
}
|
||||
n := varint.Encode(tmp[:], int64((4<<3)|0))
|
||||
buf.Write(tmp[:n])
|
||||
n = varint.Encode(tmp[:], int64(frameNumber))
|
||||
buf.Write(tmp[:n])
|
||||
if len(opusData) > 0 {
|
||||
n := varint.Encode(tmp[:], int64((5<<3)|2))
|
||||
buf.Write(tmp[:n])
|
||||
n = varint.Encode(tmp[:], int64(len(opusData)))
|
||||
buf.Write(tmp[:n])
|
||||
buf.Write(opusData)
|
||||
}
|
||||
if terminator {
|
||||
n := varint.Encode(tmp[:], int64((16<<3)|0))
|
||||
buf.Write(tmp[:n])
|
||||
n = varint.Encode(tmp[:], 1)
|
||||
buf.Write(tmp[:n])
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// decodeUDPAudio parses a MumbleUDP.Audio protobuf message.
|
||||
func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData []byte, terminator bool) {
|
||||
pos := 0
|
||||
for pos < len(data) {
|
||||
key, n := varint.Decode(data[pos:])
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
pos += n
|
||||
fieldNum := int(key >> 3)
|
||||
wireType := int(key & 0x7)
|
||||
|
||||
switch wireType {
|
||||
case 0: // varint
|
||||
val, n := varint.Decode(data[pos:])
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
pos += n
|
||||
switch fieldNum {
|
||||
case 3:
|
||||
session = uint32(val)
|
||||
case 4:
|
||||
frameNumber = uint32(val)
|
||||
case 16:
|
||||
terminator = val != 0
|
||||
}
|
||||
case 2: // length-delimited
|
||||
length, n := varint.Decode(data[pos:])
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
pos += n
|
||||
if int(length) > len(data)-pos {
|
||||
return
|
||||
}
|
||||
if fieldNum == 5 {
|
||||
opusData = make([]byte, length)
|
||||
copy(opusData, data[pos:pos+int(length)])
|
||||
}
|
||||
pos += int(length)
|
||||
default:
|
||||
return // unknown wire type, bail
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mumble 1.5 native UDP crypto (AES-128-OCB with IV-prefix header).
|
||||
//
|
||||
// Wire format: [iv_byte(1)][tag(3)][ciphertext]
|
||||
// Nonce is the full 16-byte IV, incremented per-packet.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
udp15BlockSize = 16
|
||||
udp15HeaderSize = 4 // 1 byte IV + 3 bytes tag
|
||||
)
|
||||
|
||||
// cryptState15 implements Mumble 1.5 native UDP encryption.
|
||||
type cryptState15 struct {
|
||||
key [16]byte
|
||||
encryptIV [16]byte
|
||||
decryptIV [16]byte
|
||||
history [256]byte // replay: history[iv_byte] == expected next byte
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// setup15 initializes 1.5-style crypto from CryptSetup.
|
||||
// clientNonce → encryptIV, serverNonce → decryptIV.
|
||||
func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error {
|
||||
if len(key) != 16 || len(clientNonce) != 16 || len(serverNonce) != 16 {
|
||||
return errors.New("gumble: invalid crypto key/nonce")
|
||||
}
|
||||
copy(cs.key[:], key)
|
||||
copy(cs.encryptIV[:], clientNonce)
|
||||
copy(cs.decryptIV[:], serverNonce)
|
||||
cs.initialized = true
|
||||
|
||||
log.Info("cryptState15 setup: key=%s encryptIV=%s decryptIV=%s",
|
||||
hex.EncodeToString(cs.key[:]),
|
||||
hex.EncodeToString(cs.encryptIV[:]),
|
||||
hex.EncodeToString(cs.decryptIV[:]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encrypt15 encrypts plaintext for Mumble 1.5 native UDP.
|
||||
// Returns [iv_byte(1)][tag(3)][ciphertext].
|
||||
func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) {
|
||||
if !cs.initialized {
|
||||
return nil, errors.New("gumble: crypto not initialized")
|
||||
}
|
||||
|
||||
// Increment IV (big-endian).
|
||||
incrementBE(cs.encryptIV[:])
|
||||
|
||||
ciphertext, tag := ocb15Encrypt(cs.key[:], cs.encryptIV[:], plaintext)
|
||||
|
||||
out := make([]byte, udp15HeaderSize+len(ciphertext))
|
||||
out[0] = cs.encryptIV[0]
|
||||
out[1] = tag[0]
|
||||
out[2] = tag[1]
|
||||
out[3] = tag[2]
|
||||
copy(out[4:], ciphertext)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// decrypt15 decrypts a Mumble 1.5 native UDP packet.
|
||||
// Returns plaintext or nil on failure.
|
||||
func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) {
|
||||
if !cs.initialized {
|
||||
return nil, errors.New("gumble: crypto not initialized")
|
||||
}
|
||||
if len(packet) < udp15HeaderSize {
|
||||
return nil, errors.New("gumble: packet too short")
|
||||
}
|
||||
|
||||
ivByte := packet[0]
|
||||
expectedTag := packet[1:4]
|
||||
encrypted := packet[4:]
|
||||
|
||||
savedIV := cs.decryptIV
|
||||
|
||||
// Determine how the received iv_byte relates to our expected decryptIV[0].
|
||||
if ivByte == cs.decryptIV[0] {
|
||||
// Perfect match — packet arrived in order.
|
||||
} else {
|
||||
diff := int(ivByte) - int(cs.decryptIV[0])
|
||||
if diff < -128 {
|
||||
diff += 256
|
||||
} else if diff > 128 {
|
||||
diff -= 256
|
||||
}
|
||||
|
||||
if diff >= -30 && diff <= 30 {
|
||||
cs.decryptIV[0] = ivByte
|
||||
if diff < 0 {
|
||||
// Late packet — we're ahead. Restore after decrypt.
|
||||
defer func() { cs.decryptIV = savedIV }()
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("gumble: OCB IV too far off")
|
||||
}
|
||||
|
||||
// Replay check
|
||||
if cs.history[ivByte] != 0 && cs.history[ivByte] == cs.decryptIV[1] {
|
||||
cs.decryptIV = savedIV
|
||||
return nil, errors.New("gumble: OCB replay detected")
|
||||
}
|
||||
}
|
||||
|
||||
plaintext, tag, err := ocb15Decrypt(cs.key[:], cs.decryptIV[:], encrypted)
|
||||
if err != nil {
|
||||
cs.decryptIV = savedIV
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify first 3 bytes of tag.
|
||||
if tag[0] != expectedTag[0] || tag[1] != expectedTag[1] || tag[2] != expectedTag[2] {
|
||||
cs.decryptIV = savedIV
|
||||
return nil, errors.New("gumble: OCB authentication failed")
|
||||
}
|
||||
|
||||
// Update replay history.
|
||||
cs.history[ivByte] = cs.decryptIV[1]
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// incrementBE increments a 16-byte slice as a big-endian integer.
|
||||
func incrementBE(iv []byte) {
|
||||
for i := len(iv) - 1; i >= 0; i-- {
|
||||
iv[i]++
|
||||
if iv[i] != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OCB variant for Mumble 1.5 native UDP.
|
||||
// Matches the implementation in Wumble's crypt_state.cr.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ocb15Encrypt encrypts with AES-128-OCB (no associated data).
|
||||
// Returns ciphertext and 16-byte tag.
|
||||
func ocb15Encrypt(key, nonce, plaintext []byte) (ciphertext, tag []byte) {
|
||||
block, _ := aes.NewCipher(key)
|
||||
|
||||
// delta = AES_K(nonce)
|
||||
delta := make([]byte, 16)
|
||||
block.Encrypt(delta, nonce)
|
||||
|
||||
checksum := make([]byte, 16)
|
||||
|
||||
pos := 0
|
||||
remaining := len(plaintext)
|
||||
|
||||
// Full blocks.
|
||||
for remaining > 16 {
|
||||
shift2inplace(delta)
|
||||
|
||||
xor16(checksum, checksum, plaintext[pos:pos+16])
|
||||
|
||||
// C = delta XOR AES_K(delta XOR plaintext)
|
||||
tmp := make([]byte, 16)
|
||||
xorBytes(tmp, plaintext[pos:pos+16], delta)
|
||||
block.Encrypt(tmp, tmp)
|
||||
xorBytes(tmp, tmp, delta)
|
||||
|
||||
ciphertext = append(ciphertext, tmp...)
|
||||
pos += 16
|
||||
remaining -= 16
|
||||
}
|
||||
|
||||
// Final partial block.
|
||||
shift2inplace(delta)
|
||||
|
||||
// pad = AES_K(temporary XOR delta) where temporary[15] = remaining*8
|
||||
tmp := make([]byte, 16)
|
||||
tmp[15] = byte(remaining * 8)
|
||||
xor16(tmp, tmp, delta)
|
||||
pad := make([]byte, 16)
|
||||
block.Encrypt(pad, tmp)
|
||||
|
||||
// Cpartial = plaintext XOR pad (truncated)
|
||||
cpart := make([]byte, remaining)
|
||||
xorBytes(cpart, plaintext[pos:pos+remaining], pad[:remaining])
|
||||
ciphertext = append(ciphertext, cpart...)
|
||||
|
||||
// checksum ^= (cpart || 0*) XOR pad
|
||||
csTemp := make([]byte, 16)
|
||||
copy(csTemp, cpart)
|
||||
xor16(csTemp, csTemp, pad)
|
||||
xor16(checksum, checksum, csTemp)
|
||||
|
||||
// Tag = AES_K(3*delta XOR checksum)
|
||||
shift3inplace(delta)
|
||||
xor16(delta, delta, checksum)
|
||||
tag = make([]byte, 16)
|
||||
block.Encrypt(tag, delta)
|
||||
|
||||
return ciphertext, tag
|
||||
}
|
||||
|
||||
// ocb15Decrypt decrypts with AES-128-OCB (no associated data).
|
||||
// Returns plaintext and 16-byte tag.
|
||||
func ocb15Decrypt(key, nonce, ciphertext []byte) (plaintext, tag []byte, err error) {
|
||||
block, _ := aes.NewCipher(key)
|
||||
|
||||
delta := make([]byte, 16)
|
||||
block.Encrypt(delta, nonce)
|
||||
|
||||
checksum := make([]byte, 16)
|
||||
|
||||
pos := 0
|
||||
remaining := len(ciphertext)
|
||||
|
||||
// Full blocks.
|
||||
for remaining > 16 {
|
||||
shift2inplace(delta)
|
||||
|
||||
// P = delta XOR AES_D_K(delta XOR ciphertext)
|
||||
tmp := make([]byte, 16)
|
||||
xorBytes(tmp, ciphertext[pos:pos+16], delta)
|
||||
block.Decrypt(tmp, tmp)
|
||||
xorBytes(tmp, tmp, delta)
|
||||
plaintext = append(plaintext, tmp...)
|
||||
|
||||
xor16(checksum, checksum, tmp)
|
||||
pos += 16
|
||||
remaining -= 16
|
||||
}
|
||||
|
||||
// Final partial block.
|
||||
shift2inplace(delta)
|
||||
|
||||
tmp := make([]byte, 16)
|
||||
tmp[15] = byte(remaining * 8)
|
||||
xor16(tmp, tmp, delta)
|
||||
pad := make([]byte, 16)
|
||||
block.Encrypt(pad, tmp)
|
||||
|
||||
// Ppartial = ciphertext XOR pad (truncated)
|
||||
ppart := make([]byte, remaining)
|
||||
xorBytes(ppart, ciphertext[pos:pos+remaining], pad[:remaining])
|
||||
plaintext = append(plaintext, ppart...)
|
||||
|
||||
// checksum ^= (ppart || 0*) XOR pad
|
||||
csTemp := make([]byte, 16)
|
||||
copy(csTemp, ppart)
|
||||
xor16(csTemp, csTemp, pad)
|
||||
xor16(checksum, checksum, csTemp)
|
||||
|
||||
// Tag = AES_K(3*delta XOR checksum)
|
||||
shift3inplace(delta)
|
||||
xor16(delta, delta, checksum)
|
||||
tag = make([]byte, 16)
|
||||
block.Encrypt(tag, delta)
|
||||
|
||||
return plaintext, tag, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GF(2^128) helpers (same as crypt.go's doubleBlock, but in-place).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func shift2inplace(block []byte) {
|
||||
carry := (block[0] >> 7) & 1
|
||||
for i := 0; i < 15; i++ {
|
||||
block[i] = (block[i] << 1) | (block[i+1] >> 7)
|
||||
}
|
||||
block[15] = (block[15] << 1) ^ (carry * 0x87)
|
||||
}
|
||||
|
||||
func shift3inplace(block []byte) {
|
||||
orig := make([]byte, 16)
|
||||
copy(orig, block)
|
||||
shift2inplace(block)
|
||||
xor16(block, block, orig)
|
||||
}
|
||||
|
||||
func xor16(dst, a, b []byte) {
|
||||
dst[0] = a[0] ^ b[0]
|
||||
dst[1] = a[1] ^ b[1]
|
||||
dst[2] = a[2] ^ b[2]
|
||||
dst[3] = a[3] ^ b[3]
|
||||
dst[4] = a[4] ^ b[4]
|
||||
dst[5] = a[5] ^ b[5]
|
||||
dst[6] = a[6] ^ b[6]
|
||||
dst[7] = a[7] ^ b[7]
|
||||
dst[8] = a[8] ^ b[8]
|
||||
dst[9] = a[9] ^ b[9]
|
||||
dst[10] = a[10] ^ b[10]
|
||||
dst[11] = a[11] ^ b[11]
|
||||
dst[12] = a[12] ^ b[12]
|
||||
dst[13] = a[13] ^ b[13]
|
||||
dst[14] = a[14] ^ b[14]
|
||||
dst[15] = a[15] ^ b[15]
|
||||
}
|
||||
|
||||
func xorBytes(dst, a, b []byte) {
|
||||
for i := 0; i < len(dst); i++ {
|
||||
dst[i] = a[i] ^ b[i]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global 1.5 crypto state.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var udp15Client *cryptState15
|
||||
var udp15Server *cryptState15
|
||||
|
||||
// SetUDP15Crypto installs the 1.5 native UDP crypto state from CryptSetup.
|
||||
func SetUDP15Crypto(key, clientNonce, serverNonce []byte) {
|
||||
csClient := &cryptState15{}
|
||||
if err := csClient.setup15(key, clientNonce, serverNonce); err != nil {
|
||||
log.Error("SetUDP15Crypto client: %v", err)
|
||||
return
|
||||
}
|
||||
udp15Client = csClient
|
||||
|
||||
csServer := &cryptState15{}
|
||||
if err := csServer.setup15(key, serverNonce, clientNonce); err != nil {
|
||||
log.Error("SetUDP15Crypto server: %v", err)
|
||||
return
|
||||
}
|
||||
udp15Server = csServer
|
||||
|
||||
log.Info("Mumble 1.5 native UDP crypto initialized")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame number tracking.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var udp15FrameNumber uint32
|
||||
|
||||
// nextFrameNumber returns the next frame number for outbound 1.5 UDP audio.
|
||||
func nextFrameNumber() uint32 {
|
||||
n := udp15FrameNumber
|
||||
udp15FrameNumber++
|
||||
return n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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) {
|
||||
cs := udp15Client
|
||||
if cs == nil || !cs.initialized {
|
||||
return false, nil
|
||||
}
|
||||
if c.udpConn == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
frameNum := nextFrameNumber()
|
||||
|
||||
// Build MumbleUDP.Audio protobuf.
|
||||
payload := encodeUDPAudio(0, frameNum, data, final)
|
||||
|
||||
log.Info("UDP15 send: frame=%d opus_len=%d proto_len=%d final=%v",
|
||||
frameNum, len(data), len(payload), final)
|
||||
|
||||
// Encrypt with 1.5 format.
|
||||
encrypted, err := cs.encrypt15(payload)
|
||||
if err != nil {
|
||||
log.Error("UDP15 encrypt failed: %v", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Info("UDP15 send: encrypted_len=%d hex=%s",
|
||||
len(encrypted), hex.EncodeToString(encrypted))
|
||||
|
||||
_, err = c.udpConn.Write(encrypted)
|
||||
if err != nil {
|
||||
log.Error("UDP15 send write failed: %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// HandleUDPPacket15 processes an incoming Mumble 1.5 native UDP packet.
|
||||
func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
||||
if len(packet) < udp15HeaderSize {
|
||||
log.Warn("UDP15 #%d: packet too short (%d bytes)", pktNum, len(packet))
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("UDP15 #%d: raw hex=%s", pktNum, hex.EncodeToString(packet))
|
||||
|
||||
cs := udp15Server
|
||||
if cs == nil || !cs.initialized {
|
||||
log.Warn("UDP15 #%d: crypto not initialized", pktNum)
|
||||
return
|
||||
}
|
||||
|
||||
plaintext, err := cs.decrypt15(packet)
|
||||
if err != nil {
|
||||
log.Warn("UDP15 #%d: decrypt failed: %v", pktNum, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("UDP15 #%d: decrypt OK, plaintext hex=%s", pktNum, hex.EncodeToString(plaintext))
|
||||
|
||||
// Parse MumbleUDP.Audio protobuf.
|
||||
session, frameNum, opusData, terminator := decodeUDPAudio(plaintext)
|
||||
|
||||
log.Info("UDP15 #%d: session=%d frame=%d opus_len=%d term=%v",
|
||||
pktNum, session, frameNum, len(opusData), terminator)
|
||||
|
||||
if len(opusData) == 0 && !terminator {
|
||||
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
|
||||
if codec == nil {
|
||||
log.Warn("UDP15 #%d: no audio codec", pktNum)
|
||||
return
|
||||
}
|
||||
decoder = codec.NewDecoder()
|
||||
user.decoder = decoder
|
||||
log.Info("UDP15 #%d: new decoder for %s", pktNum, user.Name)
|
||||
}
|
||||
|
||||
if terminator && len(opusData) == 0 {
|
||||
// Decoder reset on terminator.
|
||||
decoder.Reset()
|
||||
log.Info("UDP15 #%d: terminator for %s, decoder reset", pktNum, user.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if len(opusData) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Detect sequence gaps via frame_number.
|
||||
if user.audioSequenceValid {
|
||||
gap := int64(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)
|
||||
for i := int64(1); i < gap; i++ {
|
||||
c.dispatchPLC15(user, decoder)
|
||||
}
|
||||
} else if gap < 0 && gap > -100 {
|
||||
log.Info("UDP15 #%d: seq reorder for %s: %d -> %d, resetting decoder",
|
||||
pktNum, user.Name, user.audioSequence, frameNum)
|
||||
decoder.Reset()
|
||||
} else if gap == 0 {
|
||||
log.Info("UDP15 #%d: duplicate seq=%d for %s", pktNum, frameNum, user.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
user.audioSequence = int64(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)
|
||||
decoder.Reset()
|
||||
return
|
||||
}
|
||||
|
||||
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),
|
||||
AudioBuffer: AudioBuffer(pcm),
|
||||
}
|
||||
c.dispatchAudio(user, &event)
|
||||
}
|
||||
|
||||
// dispatchPLC15 generates a Packet Loss Concealment frame for 1.5 UDP.
|
||||
func (c *Client) dispatchPLC15(user *User, decoder AudioDecoder) {
|
||||
pcm, err := decoder.Decode(nil, AudioMaximumFrameSize)
|
||||
if err != nil {
|
||||
decoder.Reset()
|
||||
return
|
||||
}
|
||||
seq := user.audioSequence + 1
|
||||
user.audioSequence = seq
|
||||
event := AudioPacket{
|
||||
Client: c,
|
||||
Sender: user,
|
||||
Target: &VoiceTarget{ID: 0},
|
||||
Sequence: seq,
|
||||
AudioBuffer: AudioBuffer(pcm),
|
||||
}
|
||||
c.dispatchAudio(user, &event)
|
||||
}
|
||||
Reference in New Issue
Block a user