The official Mumble client has two protections Barnard was missing. After UDP decryption fails for over five seconds, it requests a cryptographic resync, retrying no more than every five seconds. It always accepts TCP-tunneled voice, even while UDP is active, because transports can briefly overlap during fallback. These are now implemented.
This commit is contained in:
@@ -83,6 +83,8 @@ type Client struct {
|
|||||||
udpProtobuf bool
|
udpProtobuf bool
|
||||||
udpFallbackLogged atomic.Bool
|
udpFallbackLogged atomic.Bool
|
||||||
udpFirstRecv atomic.Bool
|
udpFirstRecv atomic.Bool
|
||||||
|
udpLastGood time.Time
|
||||||
|
udpLastRequest time.Time
|
||||||
cryptOut cryptState // client→server encryption
|
cryptOut cryptState // client→server encryption
|
||||||
cryptIn cryptState // server→client encryption
|
cryptIn cryptState // server→client encryption
|
||||||
|
|
||||||
@@ -328,14 +330,10 @@ func (c *Client) readRoutine() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// When UDP audio is active, ignore TCP-tunneled audio
|
// Always accept TCP-tunneled audio, even after UDP has worked. The
|
||||||
// (packet type 1) to avoid double-processing packets.
|
// server can move a client back to the tunnel when the UDP return path
|
||||||
c.udpMu.RLock()
|
// fails, and UDP and TCP packets may briefly overlap during that
|
||||||
udpActive := c.udpActive
|
// transition. The per-user sequence handling rejects duplicates.
|
||||||
c.udpMu.RUnlock()
|
|
||||||
if pType == 1 && udpActive {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if int(pType) < len(handlers) {
|
if int(pType) < len(handlers) {
|
||||||
handlers[pType](c, data)
|
handlers[pType](c, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ func (c *Conn) WritePacket(ptype uint16, data []byte) error {
|
|||||||
if err := c.writeHeader(uint16(ptype), uint32(len(data))); err != nil {
|
if err := c.writeHeader(uint16(ptype), uint32(len(data))); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if _, err := c.Conn.Write(data); err != nil {
|
if _, err := c.Conn.Write(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
||||||
"git.stormux.org/storm/barnard/log"
|
"git.stormux.org/storm/barnard/log"
|
||||||
@@ -321,7 +322,39 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
|||||||
log.Info("received CryptSetup: key_len=%d client_nonce_len=%d server_nonce_len=%d",
|
log.Info("received CryptSetup: key_len=%d client_nonce_len=%d server_nonce_len=%d",
|
||||||
len(packet.Key), len(packet.ClientNonce), len(packet.ServerNonce))
|
len(packet.Key), len(packet.ClientNonce), len(packet.ServerNonce))
|
||||||
}
|
}
|
||||||
} else if !c.cryptOut.isInitialized() {
|
} else if packet.ServerNonce != nil {
|
||||||
|
c.udpMu.RLock()
|
||||||
|
inbound := c.udpCryptoIn
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if inbound == nil {
|
||||||
|
return errors.New("gumble: received UDP resync before crypto initialization")
|
||||||
|
}
|
||||||
|
if err := inbound.setDecryptIV(packet.ServerNonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.udpMu.Lock()
|
||||||
|
c.udpLastGood = time.Now()
|
||||||
|
c.udpLastRequest = time.Time{}
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
log.Info("UDP crypto resynchronized from server nonce")
|
||||||
|
} else if c.cryptOut.isInitialized() {
|
||||||
|
// An empty CryptSetup is the server asking us for the current client
|
||||||
|
// nonce so it can resynchronize decryption in the other direction.
|
||||||
|
c.udpMu.RLock()
|
||||||
|
outbound := c.udpCryptoOut
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if outbound == nil {
|
||||||
|
return errors.New("gumble: received UDP resync request without outbound crypto")
|
||||||
|
}
|
||||||
|
clientNonce, err := outbound.getEncryptIV()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Conn.WriteProto(&MumbleProto.CryptSetup{ClientNonce: clientNonce}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info("sent client nonce for UDP crypto resync")
|
||||||
|
} else {
|
||||||
// Only log incomplete once before crypto is set up
|
// Only log incomplete once before crypto is set up
|
||||||
log.Debug("received CryptSetup with incomplete fields, waiting for full key exchange")
|
log.Debug("received CryptSetup with incomplete fields, waiting for full key exchange")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gumble
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Regression: Mumble 1.5 decodes UDPTunnel packets as native protobuf UDP
|
// Regression: Mumble 1.5 decodes UDPTunnel packets as native protobuf UDP
|
||||||
@@ -38,3 +39,46 @@ func TestWriteAudioUsesProtobufEnvelopeForTCPFallback(t *testing.T) {
|
|||||||
t.Fatalf("payload=%x want=%x", got.data, want)
|
t.Fatalf("payload=%x want=%x", got.data, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression: once an authenticated UDP response arrived, readRoutine used to
|
||||||
|
// discard every later TCP UDPTunnel packet. If the server moved voice back to
|
||||||
|
// TCP after the UDP return path failed, incoming audio stayed silent until the
|
||||||
|
// client restarted and cleared udpActive.
|
||||||
|
func TestReadRoutineAcceptsTCPAudioWhileUDPIsActive(t *testing.T) {
|
||||||
|
local, remote := net.Pipe()
|
||||||
|
config := NewConfig()
|
||||||
|
listener := &terminatorListener{packets: make(chan *AudioPacket, 1)}
|
||||||
|
config.AttachAudio(listener)
|
||||||
|
user := &User{Session: 1, Name: "speaker", decoder: &terminatorDecoder{}}
|
||||||
|
client := &Client{
|
||||||
|
Config: config,
|
||||||
|
Conn: NewConn(local),
|
||||||
|
Users: Users{user.Session: user},
|
||||||
|
end: make(chan struct{}),
|
||||||
|
state: uint32(StateSynced),
|
||||||
|
udpActive: true,
|
||||||
|
udpProtobuf: true,
|
||||||
|
}
|
||||||
|
go client.readRoutine()
|
||||||
|
|
||||||
|
// Native UDP type byte followed by MumbleUDP.Audio fields:
|
||||||
|
// sender_session=1, frame_number=1, opus_data={0x01}.
|
||||||
|
payload := mustDecodeHex("00180120012a0101")
|
||||||
|
if err := NewConn(remote).WritePacket(1, payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case packet := <-listener.packets:
|
||||||
|
if packet.Sender != user {
|
||||||
|
t.Fatalf("TCP-tunneled audio sender = %v; want %v", packet.Sender, user)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("TCP-tunneled audio was discarded while UDP was active")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := remote.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
<-client.end
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
||||||
"git.stormux.org/storm/barnard/log"
|
"git.stormux.org/storm/barnard/log"
|
||||||
)
|
)
|
||||||
@@ -193,6 +195,7 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint64, opusData [
|
|||||||
const (
|
const (
|
||||||
udp15BlockSize = 16
|
udp15BlockSize = 16
|
||||||
udp15HeaderSize = 4 // 1 byte IV + 3 bytes tag
|
udp15HeaderSize = 4 // 1 byte IV + 3 bytes tag
|
||||||
|
udpResyncDelay = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// cryptState15 implements Mumble 1.5 native UDP encryption.
|
// cryptState15 implements Mumble 1.5 native UDP encryption.
|
||||||
@@ -333,6 +336,31 @@ func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) {
|
|||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setDecryptIV applies the server nonce returned after a CryptSetup resync
|
||||||
|
// request while preserving the existing key and outbound crypto state.
|
||||||
|
func (cs *cryptState15) setDecryptIV(serverNonce []byte) error {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
if !cs.initialized {
|
||||||
|
return errors.New("gumble: crypto not initialized")
|
||||||
|
}
|
||||||
|
if len(serverNonce) != len(cs.decryptIV) {
|
||||||
|
return errors.New("gumble: invalid server nonce")
|
||||||
|
}
|
||||||
|
copy(cs.decryptIV[:], serverNonce)
|
||||||
|
cs.history = [256]byte{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *cryptState15) getEncryptIV() ([]byte, error) {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
if !cs.initialized {
|
||||||
|
return nil, errors.New("gumble: crypto not initialized")
|
||||||
|
}
|
||||||
|
return append([]byte(nil), cs.encryptIV[:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
// advanceIV increments a 16-byte IV as a little-endian integer.
|
// advanceIV increments a 16-byte IV as a little-endian integer.
|
||||||
func advanceIV(iv []byte) {
|
func advanceIV(iv []byte) {
|
||||||
for i := 0; i < len(iv); i++ {
|
for i := 0; i < len(iv); i++ {
|
||||||
@@ -579,6 +607,8 @@ func (c *Client) setUDP15Crypto(key, clientNonce, serverNonce []byte) error {
|
|||||||
c.udpCryptoOut = outbound
|
c.udpCryptoOut = outbound
|
||||||
c.udpCryptoIn = inbound
|
c.udpCryptoIn = inbound
|
||||||
c.udpFrameNumber = 0
|
c.udpFrameNumber = 0
|
||||||
|
c.udpLastGood = time.Now()
|
||||||
|
c.udpLastRequest = time.Time{}
|
||||||
c.udpMu.Unlock()
|
c.udpMu.Unlock()
|
||||||
c.udpWriteMu.Unlock()
|
c.udpWriteMu.Unlock()
|
||||||
log.Info("Mumble 1.5 native UDP crypto initialized")
|
log.Info("Mumble 1.5 native UDP crypto initialized")
|
||||||
@@ -674,6 +704,7 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
|||||||
plaintext, err := cs.decrypt15(packet)
|
plaintext, err := cs.decrypt15(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("UDP15 #%d: decrypt failed: %v", pktNum, err)
|
log.Warn("UDP15 #%d: decrypt failed: %v", pktNum, err)
|
||||||
|
c.requestUDPResync(time.Now())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,9 +747,36 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
|||||||
func (c *Client) markUDPActive() {
|
func (c *Client) markUDPActive() {
|
||||||
c.udpMu.Lock()
|
c.udpMu.Lock()
|
||||||
c.udpActive = true
|
c.udpActive = true
|
||||||
|
c.udpLastGood = time.Now()
|
||||||
c.udpMu.Unlock()
|
c.udpMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestUDPResync mirrors Mumble's recovery for a UDP crypto stream that has
|
||||||
|
// stopped decrypting. A request is sent only after five seconds without a good
|
||||||
|
// packet and is rate-limited to one every five seconds until recovery.
|
||||||
|
func (c *Client) requestUDPResync(now time.Time) {
|
||||||
|
c.udpMu.Lock()
|
||||||
|
lastGood := c.udpLastGood
|
||||||
|
lastRequest := c.udpLastRequest
|
||||||
|
if lastGood.IsZero() {
|
||||||
|
c.udpLastGood = now
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if now.Sub(lastGood) <= udpResyncDelay || (!lastRequest.IsZero() && now.Sub(lastRequest) <= udpResyncDelay) {
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.udpLastRequest = now
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
|
||||||
|
if err := c.Conn.WriteProto(&MumbleProto.CryptSetup{}); err != nil {
|
||||||
|
log.Warn("requesting UDP crypto resync failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("requested UDP crypto resync after sustained decrypt failures")
|
||||||
|
}
|
||||||
|
|
||||||
// dispatchOpus15 processes a decoded MumbleUDP.Audio frame and dispatches
|
// dispatchOpus15 processes a decoded MumbleUDP.Audio frame and dispatches
|
||||||
// the decoded PCM to audio listeners.
|
// the decoded PCM to audio listeners.
|
||||||
func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, opusData []byte, terminator bool, context uint32, position *[3]float32, volumeAdjustment float32) {
|
func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, opusData []byte, terminator bool, context uint32, position *[3]float32, volumeAdjustment float32) {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package gumble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUDPResyncWaitsForSustainedDecryptFailure(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
client := &Client{udpLastGood: now.Add(-4 * time.Second)}
|
||||||
|
|
||||||
|
client.requestUDPResync(now)
|
||||||
|
|
||||||
|
if !client.udpLastRequest.IsZero() {
|
||||||
|
t.Fatal("UDP resync was requested before five seconds without a good packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPResyncRequestUsesEmptyCryptSetup(t *testing.T) {
|
||||||
|
local, remote := net.Pipe()
|
||||||
|
defer local.Close()
|
||||||
|
defer remote.Close()
|
||||||
|
now := time.Now()
|
||||||
|
client := &Client{
|
||||||
|
Conn: NewConn(local),
|
||||||
|
udpLastGood: now.Add(-6 * time.Second),
|
||||||
|
}
|
||||||
|
received := make(chan struct {
|
||||||
|
packetType uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
go func() {
|
||||||
|
packetType, data, err := NewConn(remote).ReadPacket()
|
||||||
|
received <- struct {
|
||||||
|
packetType uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}{packetType, data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client.requestUDPResync(now)
|
||||||
|
packet := <-received
|
||||||
|
if packet.err != nil {
|
||||||
|
t.Fatal(packet.err)
|
||||||
|
}
|
||||||
|
if packet.packetType != 15 || len(packet.data) != 0 {
|
||||||
|
t.Fatalf("resync packet: type=%d data=%x; want empty CryptSetup", packet.packetType, packet.data)
|
||||||
|
}
|
||||||
|
if !client.udpLastRequest.Equal(now) {
|
||||||
|
t.Fatalf("last request = %v; want %v", client.udpLastRequest, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.requestUDPResync(now.Add(time.Second))
|
||||||
|
if !client.udpLastRequest.Equal(now) {
|
||||||
|
t.Fatal("rate-limited resync changed the last-request time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCryptSetupServerNonceResynchronizesInboundUDP(t *testing.T) {
|
||||||
|
key := make([]byte, 16)
|
||||||
|
clientNonce := make([]byte, 16)
|
||||||
|
serverNonce := make([]byte, 16)
|
||||||
|
serverNonce[0] = 7
|
||||||
|
client := &Client{}
|
||||||
|
if err := client.setUDP15Crypto(key, clientNonce, make([]byte, 16)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client.udpLastRequest = time.Now()
|
||||||
|
data, err := proto.Marshal(&MumbleProto.CryptSetup{ServerNonce: serverNonce})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.handleCryptSetup(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.udpCryptoIn.mu.Lock()
|
||||||
|
gotNonce := client.udpCryptoIn.decryptIV
|
||||||
|
client.udpCryptoIn.mu.Unlock()
|
||||||
|
if gotNonce != [16]byte{7} {
|
||||||
|
t.Fatalf("inbound nonce = %x; want %x", gotNonce, serverNonce)
|
||||||
|
}
|
||||||
|
if !client.udpLastRequest.IsZero() {
|
||||||
|
t.Fatal("successful resync did not clear the request timer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyCryptSetupReturnsCurrentClientNonce(t *testing.T) {
|
||||||
|
local, remote := net.Pipe()
|
||||||
|
defer local.Close()
|
||||||
|
defer remote.Close()
|
||||||
|
key := make([]byte, 16)
|
||||||
|
clientNonce := make([]byte, 16)
|
||||||
|
clientNonce[0] = 9
|
||||||
|
client := &Client{Conn: NewConn(local)}
|
||||||
|
if err := client.cryptOut.setup(key, clientNonce); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := client.setUDP15Crypto(key, clientNonce, make([]byte, 16)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
received := make(chan struct {
|
||||||
|
packetType uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
go func() {
|
||||||
|
packetType, data, err := NewConn(remote).ReadPacket()
|
||||||
|
received <- struct {
|
||||||
|
packetType uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}{packetType, data, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := client.handleCryptSetup(nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
packet := <-received
|
||||||
|
if packet.err != nil {
|
||||||
|
t.Fatal(packet.err)
|
||||||
|
}
|
||||||
|
var response MumbleProto.CryptSetup
|
||||||
|
if err := proto.Unmarshal(packet.data, &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if packet.packetType != 15 || string(response.ClientNonce) != string(clientNonce) {
|
||||||
|
t.Fatalf("resync response: type=%d nonce=%x; want type=15 nonce=%x",
|
||||||
|
packet.packetType, response.ClientNonce, clientNonce)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user