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
|
||||
udpCryptoIn *cryptState15
|
||||
udpFrameNumber uint64
|
||||
udpProtobuf bool
|
||||
udpFallbackLogged atomic.Bool
|
||||
udpFirstRecv atomic.Bool
|
||||
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 {
|
||||
// Try Mumble 1.5 native UDP first (unless disabled)
|
||||
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 {
|
||||
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]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,13 @@ func (c *Client) handleVersion(buffer []byte) error {
|
||||
if err := proto.Unmarshal(buffer, &packet); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+75
-5
@@ -63,7 +63,7 @@ func pbDecodeVarint(buf []byte) (uint64, int) {
|
||||
// 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(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 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(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 {
|
||||
n := pbEncodeVarint(tmp[:], uint64((16<<3)|0))
|
||||
buf.Write(tmp[:n])
|
||||
@@ -367,11 +378,28 @@ func ocb15Encrypt(key, nonce, plaintext []byte) (ciphertext, tag []byte) {
|
||||
for remaining > 16 {
|
||||
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])
|
||||
if flipBit {
|
||||
checksum[0] ^= 1
|
||||
}
|
||||
|
||||
// C = delta XOR AES_K(delta XOR plaintext)
|
||||
tmp := make([]byte, 16)
|
||||
xorBytes(tmp, plaintext[pos:pos+16], delta)
|
||||
if flipBit {
|
||||
tmp[0] ^= 1
|
||||
}
|
||||
block.Encrypt(tmp, tmp)
|
||||
xorBytes(tmp, tmp, delta)
|
||||
|
||||
@@ -460,6 +488,18 @@ func ocb15Decrypt(key, nonce, ciphertext []byte) (plaintext, tag []byte, err err
|
||||
xor16(csTemp, csTemp, pad)
|
||||
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)
|
||||
shift3inplace(delta)
|
||||
xor16(delta, delta, checksum)
|
||||
@@ -535,10 +575,32 @@ func (c *Client) setUDP15Crypto(key, clientNonce, serverNonce []byte) error {
|
||||
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.
|
||||
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
|
||||
// packet can reach the server before the packet with the preceding IV.
|
||||
c.udpWriteMu.Lock()
|
||||
@@ -546,13 +608,21 @@ func (c *Client) WriteAudioUDP15(target uint32, data []byte, final bool) (bool,
|
||||
c.udpMu.Lock()
|
||||
cs, udpConn := c.udpCryptoOut, c.udpConn
|
||||
frameNum := c.udpFrameNumber
|
||||
c.udpFrameNumber++
|
||||
protobuf := c.udpProtobuf
|
||||
if protobuf {
|
||||
c.udpFrameNumber++
|
||||
}
|
||||
c.udpMu.Unlock()
|
||||
if cs == nil || udpConn == 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)
|
||||
if err != nil {
|
||||
log.Error("UDP15 encrypt failed: %v", err)
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestUDPAudioProtobuf(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if session != 0 {
|
||||
|
||||
@@ -512,6 +512,20 @@ func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
}(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.
|
||||
// Returns the updated emptyBufs slice after consuming a buffer.
|
||||
// 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
|
||||
for i := 0; i < samples*2; i += 2 {
|
||||
// Process left channel with saturation protection
|
||||
sample := packet.AudioBuffer[i]
|
||||
sample := applyVolumeAdjustment(packet.AudioBuffer[i], packet.VolumeAdjustment)
|
||||
if boost > 1 {
|
||||
boosted := int32(sample) * int32(boost)
|
||||
if boosted > 32767 {
|
||||
@@ -563,7 +577,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
||||
rawPtr += 2
|
||||
|
||||
// Process right channel with saturation protection
|
||||
sample = packet.AudioBuffer[i+1]
|
||||
sample = applyVolumeAdjustment(packet.AudioBuffer[i+1], packet.VolumeAdjustment)
|
||||
if boost > 1 {
|
||||
boosted := int32(sample) * int32(boost)
|
||||
if boosted > 32767 {
|
||||
@@ -584,7 +598,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
||||
} else {
|
||||
// Process mono samples with saturation protection
|
||||
for i := 0; i < samples; i++ {
|
||||
sample := packet.AudioBuffer[i]
|
||||
sample := applyVolumeAdjustment(packet.AudioBuffer[i], packet.VolumeAdjustment)
|
||||
if boost > 1 {
|
||||
boosted := int32(sample) * int32(boost)
|
||||
if boosted > 32767 {
|
||||
|
||||
Reference in New Issue
Block a user