Complete UDP audio protocol handling
This commit is contained in:
committed by
Brandon McGinty
parent
4ad22db5aa
commit
95ef0be1f4
@@ -78,6 +78,7 @@ type Client struct {
|
|||||||
udpCryptoOut *cryptState15
|
udpCryptoOut *cryptState15
|
||||||
udpCryptoIn *cryptState15
|
udpCryptoIn *cryptState15
|
||||||
udpFrameNumber uint64
|
udpFrameNumber uint64
|
||||||
|
udpProtobuf bool
|
||||||
udpFallbackLogged atomic.Bool
|
udpFallbackLogged atomic.Bool
|
||||||
udpFirstRecv atomic.Bool
|
udpFirstRecv atomic.Bool
|
||||||
cryptOut cryptState // client→server encryption
|
cryptOut cryptState // client→server encryption
|
||||||
@@ -350,7 +351,7 @@ func (c *Client) EnableStereoEncoder() {
|
|||||||
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 Mumble 1.5 native 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.WriteAudioUDP15(uint32(target), data, final); sent {
|
if sent, err := c.WriteAudioUDP15(format, uint32(target), sequence, data, final, X, Y, Z); sent {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("UDP15 send error: %v", err)
|
log.Error("UDP15 send error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ func (c *Conn) WriteAudio(format, target byte, sequence int64, final bool, data
|
|||||||
header := buff[:1+n+m]
|
header := buff[:1+n+m]
|
||||||
|
|
||||||
var positionalLength int
|
var positionalLength int
|
||||||
if X != nil {
|
if X != nil || Y != nil || Z != nil {
|
||||||
|
if X == nil || Y == nil || Z == nil {
|
||||||
|
return errors.New("gumble: positional audio requires X, Y, and Z")
|
||||||
|
}
|
||||||
positionalLength = 3 * 4
|
positionalLength = 3 * 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ func (c *Client) handleVersion(buffer []byte) error {
|
|||||||
if err := proto.Unmarshal(buffer, &packet); err != nil {
|
if err := proto.Unmarshal(buffer, &packet); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Mumble 1.5 introduced protobuf UDP audio. Older servers retain the
|
||||||
|
// legacy UDP payload inside the same encrypted envelope.
|
||||||
|
if packet.VersionV1 != nil {
|
||||||
|
c.udpMu.Lock()
|
||||||
|
c.udpProtobuf = *packet.VersionV1 >= ClientVersion
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-4
@@ -63,7 +63,7 @@ func pbDecodeVarint(buf []byte) (uint64, int) {
|
|||||||
// 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.
|
// Uses standard protobuf varint encoding, not Mumble's custom varint.
|
||||||
func encodeUDPAudio(target uint32, frameNumber uint64, opusData []byte, terminator bool) []byte {
|
func encodeUDPAudio(target uint32, frameNumber uint64, opusData []byte, terminator bool, X, Y, Z *float32) []byte {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
var tmp [10]byte // max protobuf varint size
|
var tmp [10]byte // max protobuf varint size
|
||||||
|
|
||||||
@@ -84,6 +84,17 @@ func encodeUDPAudio(target uint32, frameNumber uint64, opusData []byte, terminat
|
|||||||
buf.Write(tmp[:n])
|
buf.Write(tmp[:n])
|
||||||
buf.Write(opusData)
|
buf.Write(opusData)
|
||||||
}
|
}
|
||||||
|
if X != nil && Y != nil && Z != nil {
|
||||||
|
n := pbEncodeVarint(tmp[:], uint64((6<<3)|2))
|
||||||
|
buf.Write(tmp[:n])
|
||||||
|
n = pbEncodeVarint(tmp[:], 12)
|
||||||
|
buf.Write(tmp[:n])
|
||||||
|
for _, value := range []float32{*X, *Y, *Z} {
|
||||||
|
var fixed [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(fixed[:], math.Float32bits(value))
|
||||||
|
buf.Write(fixed[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
if terminator {
|
if terminator {
|
||||||
n := pbEncodeVarint(tmp[:], uint64((16<<3)|0))
|
n := pbEncodeVarint(tmp[:], uint64((16<<3)|0))
|
||||||
buf.Write(tmp[:n])
|
buf.Write(tmp[:n])
|
||||||
@@ -367,11 +378,28 @@ func ocb15Encrypt(key, nonce, plaintext []byte) (ciphertext, tag []byte) {
|
|||||||
for remaining > 16 {
|
for remaining > 16 {
|
||||||
shift2inplace(delta)
|
shift2inplace(delta)
|
||||||
|
|
||||||
|
// Mitigate the XEX* forgery attack (eprint 2019/311), matching
|
||||||
|
// Mumble's CryptStateOCB2 implementation.
|
||||||
|
flipBit := remaining <= 32
|
||||||
|
if flipBit {
|
||||||
|
for _, b := range plaintext[pos : pos+15] {
|
||||||
|
if b != 0 {
|
||||||
|
flipBit = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
xor16(checksum, checksum, plaintext[pos:pos+16])
|
xor16(checksum, checksum, plaintext[pos:pos+16])
|
||||||
|
if flipBit {
|
||||||
|
checksum[0] ^= 1
|
||||||
|
}
|
||||||
|
|
||||||
// C = delta XOR AES_K(delta XOR plaintext)
|
// C = delta XOR AES_K(delta XOR plaintext)
|
||||||
tmp := make([]byte, 16)
|
tmp := make([]byte, 16)
|
||||||
xorBytes(tmp, plaintext[pos:pos+16], delta)
|
xorBytes(tmp, plaintext[pos:pos+16], delta)
|
||||||
|
if flipBit {
|
||||||
|
tmp[0] ^= 1
|
||||||
|
}
|
||||||
block.Encrypt(tmp, tmp)
|
block.Encrypt(tmp, tmp)
|
||||||
xorBytes(tmp, tmp, delta)
|
xorBytes(tmp, tmp, delta)
|
||||||
|
|
||||||
@@ -460,6 +488,18 @@ func ocb15Decrypt(key, nonce, ciphertext []byte) (plaintext, tag []byte, err err
|
|||||||
xor16(csTemp, csTemp, pad)
|
xor16(csTemp, csTemp, pad)
|
||||||
xor16(checksum, checksum, csTemp)
|
xor16(checksum, checksum, csTemp)
|
||||||
|
|
||||||
|
// Reject the XEX* forgery pattern before authenticating the tag.
|
||||||
|
matchesDelta := true
|
||||||
|
for i := 0; i < 15; i++ {
|
||||||
|
if csTemp[i] != delta[i] {
|
||||||
|
matchesDelta = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matchesDelta {
|
||||||
|
return nil, nil, errors.New("gumble: OCB XEX* forgery detected")
|
||||||
|
}
|
||||||
|
|
||||||
// Tag = AES_K(3*delta XOR checksum)
|
// Tag = AES_K(3*delta XOR checksum)
|
||||||
shift3inplace(delta)
|
shift3inplace(delta)
|
||||||
xor16(delta, delta, checksum)
|
xor16(delta, delta, checksum)
|
||||||
@@ -535,10 +575,32 @@ func (c *Client) setUDP15Crypto(key, clientNonce, serverNonce []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeLegacyUDPAudio builds the pre-1.5 UDPVoice packet payload.
|
||||||
|
func encodeLegacyUDPAudio(format, target byte, sequence int64, data []byte, final bool, X, Y, Z *float32) []byte {
|
||||||
|
var header [1 + varint.MaxVarintLen*2]byte
|
||||||
|
header[0] = format<<5 | target
|
||||||
|
n := varint.Encode(header[1:], sequence)
|
||||||
|
length := int64(len(data))
|
||||||
|
if final {
|
||||||
|
length |= 0x2000
|
||||||
|
}
|
||||||
|
m := varint.Encode(header[1+n:], length)
|
||||||
|
payload := append([]byte(nil), header[:1+n+m]...)
|
||||||
|
payload = append(payload, data...)
|
||||||
|
if X != nil && Y != nil && Z != nil {
|
||||||
|
for _, value := range []float32{*X, *Y, *Z} {
|
||||||
|
var fixed [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(fixed[:], math.Float32bits(value))
|
||||||
|
payload = append(payload, fixed[:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// WriteAudioUDP15 writes an encrypted audio packet using Mumble 1.5 native UDP.
|
// WriteAudioUDP15 writes encrypted UDP audio in the negotiated payload format.
|
||||||
// Returns true if sent, false if TCP should be used.
|
// Returns true if sent, false if TCP should be used.
|
||||||
func (c *Client) WriteAudioUDP15(target uint32, data []byte, final bool) (bool, error) {
|
func (c *Client) WriteAudioUDP15(format byte, target uint32, sequence int64, data []byte, final bool, X, Y, Z *float32) (bool, error) {
|
||||||
// Encryption and socket writes must remain ordered: otherwise a later
|
// Encryption and socket writes must remain ordered: otherwise a later
|
||||||
// packet can reach the server before the packet with the preceding IV.
|
// packet can reach the server before the packet with the preceding IV.
|
||||||
c.udpWriteMu.Lock()
|
c.udpWriteMu.Lock()
|
||||||
@@ -546,13 +608,21 @@ func (c *Client) WriteAudioUDP15(target uint32, data []byte, final bool) (bool,
|
|||||||
c.udpMu.Lock()
|
c.udpMu.Lock()
|
||||||
cs, udpConn := c.udpCryptoOut, c.udpConn
|
cs, udpConn := c.udpCryptoOut, c.udpConn
|
||||||
frameNum := c.udpFrameNumber
|
frameNum := c.udpFrameNumber
|
||||||
|
protobuf := c.udpProtobuf
|
||||||
|
if protobuf {
|
||||||
c.udpFrameNumber++
|
c.udpFrameNumber++
|
||||||
|
}
|
||||||
c.udpMu.Unlock()
|
c.udpMu.Unlock()
|
||||||
if cs == nil || udpConn == nil {
|
if cs == nil || udpConn == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := append([]byte{0x00}, encodeUDPAudio(target, frameNum, data, final)...)
|
var payload []byte
|
||||||
|
if protobuf {
|
||||||
|
payload = append([]byte{0x00}, encodeUDPAudio(target, frameNum, data, final, X, Y, Z)...)
|
||||||
|
} else {
|
||||||
|
payload = encodeLegacyUDPAudio(format, byte(target), sequence, data, final, X, Y, Z)
|
||||||
|
}
|
||||||
encrypted, err := cs.encrypt15(payload)
|
encrypted, err := cs.encrypt15(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("UDP15 encrypt failed: %v", err)
|
log.Error("UDP15 encrypt failed: %v", err)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func TestUDPAudioProtobuf(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
encoded := encodeUDPAudio(uint32(tt.session), uint64(tt.frameNumber), tt.opusData, tt.terminator)
|
encoded := encodeUDPAudio(uint32(tt.session), uint64(tt.frameNumber), tt.opusData, tt.terminator, nil, nil, nil)
|
||||||
session, frameNum, opusData, terminator, _, _, _ := decodeUDPAudio(encoded)
|
session, frameNum, opusData, terminator, _, _, _ := decodeUDPAudio(encoded)
|
||||||
|
|
||||||
if session != 0 {
|
if session != 0 {
|
||||||
|
|||||||
@@ -512,6 +512,20 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}(e)
|
}(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyVolumeAdjustment(sample int16, adjustment float32) int16 {
|
||||||
|
if adjustment == 0 || adjustment == 1 {
|
||||||
|
return sample
|
||||||
|
}
|
||||||
|
adjusted := float32(sample) * adjustment
|
||||||
|
if adjusted > 32767 {
|
||||||
|
return 32767
|
||||||
|
}
|
||||||
|
if adjusted < -32768 {
|
||||||
|
return -32768
|
||||||
|
}
|
||||||
|
return int16(adjusted)
|
||||||
|
}
|
||||||
|
|
||||||
// processAudioPacket decodes and queues a single audio packet for playback.
|
// processAudioPacket decodes and queues a single audio packet for playback.
|
||||||
// Returns the updated emptyBufs slice after consuming a buffer.
|
// Returns the updated emptyBufs slice after consuming a buffer.
|
||||||
// The caller must call reclaim() before invoking this to ensure buffers
|
// The caller must call reclaim() before invoking this to ensure buffers
|
||||||
@@ -544,7 +558,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
|||||||
// Process stereo samples as pairs
|
// Process stereo samples as pairs
|
||||||
for i := 0; i < samples*2; i += 2 {
|
for i := 0; i < samples*2; i += 2 {
|
||||||
// Process left channel with saturation protection
|
// Process left channel with saturation protection
|
||||||
sample := packet.AudioBuffer[i]
|
sample := applyVolumeAdjustment(packet.AudioBuffer[i], packet.VolumeAdjustment)
|
||||||
if boost > 1 {
|
if boost > 1 {
|
||||||
boosted := int32(sample) * int32(boost)
|
boosted := int32(sample) * int32(boost)
|
||||||
if boosted > 32767 {
|
if boosted > 32767 {
|
||||||
@@ -563,7 +577,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
|||||||
rawPtr += 2
|
rawPtr += 2
|
||||||
|
|
||||||
// Process right channel with saturation protection
|
// Process right channel with saturation protection
|
||||||
sample = packet.AudioBuffer[i+1]
|
sample = applyVolumeAdjustment(packet.AudioBuffer[i+1], packet.VolumeAdjustment)
|
||||||
if boost > 1 {
|
if boost > 1 {
|
||||||
boosted := int32(sample) * int32(boost)
|
boosted := int32(sample) * int32(boost)
|
||||||
if boosted > 32767 {
|
if boosted > 32767 {
|
||||||
@@ -584,7 +598,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
|||||||
} else {
|
} else {
|
||||||
// Process mono samples with saturation protection
|
// Process mono samples with saturation protection
|
||||||
for i := 0; i < samples; i++ {
|
for i := 0; i < samples; i++ {
|
||||||
sample := packet.AudioBuffer[i]
|
sample := applyVolumeAdjustment(packet.AudioBuffer[i], packet.VolumeAdjustment)
|
||||||
if boost > 1 {
|
if boost > 1 {
|
||||||
boosted := int32(sample) * int32(boost)
|
boosted := int32(sample) * int32(boost)
|
||||||
if boosted > 32767 {
|
if boosted > 32767 {
|
||||||
|
|||||||
Reference in New Issue
Block a user