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
|
var udpFallbackLogged bool
|
||||||
|
|
||||||
func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error {
|
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 !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 {
|
if err != nil {
|
||||||
log.Error("UDP send error: %v", err)
|
log.Error("UDP15 send error: %v", err)
|
||||||
}
|
}
|
||||||
return 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")
|
log.Info("UDP disabled, audio using TCP tunnel")
|
||||||
} else if c.udpConn == nil {
|
} else if c.udpConn == nil {
|
||||||
log.Info("no UDP socket, audio using TCP tunnel")
|
log.Info("no UDP socket, audio using TCP tunnel")
|
||||||
} else if !c.cryptOut.initialized {
|
} else if udp15Client == nil || !udp15Client.initialized {
|
||||||
log.Info("waiting for CryptSetup, audio using TCP tunnel")
|
log.Info("1.5 crypto not ready, audio using TCP tunnel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
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
|
wasInit := c.cryptOut.initialized
|
||||||
c.cryptOut.setup(packet.Key, packet.ClientNonce)
|
c.cryptOut.setup(packet.Key, packet.ClientNonce)
|
||||||
c.cryptIn.setup(packet.Key, packet.ServerNonce)
|
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 {
|
if wasInit {
|
||||||
log.Debug("CryptSetup updated (key rotation)")
|
log.Debug("CryptSetup updated (key rotation)")
|
||||||
} else {
|
} else {
|
||||||
@@ -313,10 +317,15 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive {
|
if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive {
|
||||||
c.udpActive = true
|
// Only start UDP reader if 1.5 crypto is also ready.
|
||||||
log.Info("UDP crypto ready, starting UDP reader and pinger")
|
if udp15Client != nil && udp15Client.initialized {
|
||||||
go c.udpReadRoutine()
|
c.udpActive = true
|
||||||
go c.udpPingRoutine()
|
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 {
|
} else if c.cryptOut.initialized && c.udpConn == nil {
|
||||||
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
|
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-16
@@ -1,6 +1,7 @@
|
|||||||
package gumble
|
package gumble
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -48,8 +49,9 @@ func (c *Client) startUDP() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// udpReadRoutine reads encrypted UDP audio packets from the server.
|
// udpReadRoutine reads encrypted UDP audio packets from the server.
|
||||||
|
// Uses Mumble 1.5 native UDP format.
|
||||||
func (c *Client) udpReadRoutine() {
|
func (c *Client) udpReadRoutine() {
|
||||||
log.Info("UDP reader started")
|
log.Info("UDP reader started (1.5 native format)")
|
||||||
buf := make([]byte, maxUDPPacketSize)
|
buf := make([]byte, maxUDPPacketSize)
|
||||||
var packetCount uint64
|
var packetCount uint64
|
||||||
for {
|
for {
|
||||||
@@ -63,39 +65,49 @@ func (c *Client) udpReadRoutine() {
|
|||||||
packetCount, n, addr, hex.EncodeToString(buf[:n]))
|
packetCount, n, addr, hex.EncodeToString(buf[:n]))
|
||||||
packet := make([]byte, n)
|
packet := make([]byte, n)
|
||||||
copy(packet, buf[:n])
|
copy(packet, buf[:n])
|
||||||
c.handleUDPPacket(packet, packetCount)
|
c.HandleUDPPacket15(packet, packetCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpPingRoutine sends periodic ping packets over UDP to keep the
|
// 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() {
|
func (c *Client) udpPingRoutine() {
|
||||||
ticker := time.NewTicker(udpPingInterval)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
var counter uint32
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.end:
|
case <-c.end:
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
c.sendUDPPing(counter)
|
c.sendUDPPing()
|
||||||
counter++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendUDPPing sends a UDP ping packet. The Mumble UDP ping is a type
|
// sendUDPPing sends a Mumble 1.5 native UDP ping.
|
||||||
// byte (0x20) followed by a varint-encoded timestamp.
|
func (c *Client) sendUDPPing() {
|
||||||
func (c *Client) sendUDPPing(counter uint32) {
|
cs := udp15Client
|
||||||
if c.udpConn == nil {
|
if cs == nil || !cs.initialized || c.udpConn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Type byte 0x20 (ping) + varint timestamp
|
// Type byte 0x01 = UDPPing, field 1 = timestamp (varint, milliseconds).
|
||||||
var ping [1 + varint.MaxVarintLen]byte
|
var tmp [varint.MaxVarintLen]byte
|
||||||
ping[0] = 0x20
|
var buf bytes.Buffer
|
||||||
n := varint.Encode(ping[1:], int64(time.Now().UnixNano()))
|
buf.WriteByte(0x01) // type = UDPPing
|
||||||
c.udpConn.Write(ping[:1+n])
|
|
||||||
|
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.
|
// 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