Use protobuf envelopes for Mumble 1.5 TCP audio
This commit is contained in:
committed by
Brandon McGinty
parent
e8a8390cb8
commit
794610929b
@@ -168,7 +168,7 @@ Priority 2: protocol and data correctness
|
|||||||
dereferencing and return errInvalidProtobuf for malformed server packets.
|
dereferencing and return errInvalidProtobuf for malformed server packets.
|
||||||
Audit all packet pointer dereferences similarly.
|
Audit all packet pointer dereferences similarly.
|
||||||
|
|
||||||
21. UDP protocol state has no complete interoperability test coverage
|
[x] 21. UDP protocol state has no complete interoperability test coverage
|
||||||
Files: gumble/gumble/udp15.go, gumble/gumble/udp.go
|
Files: gumble/gumble/udp15.go, gumble/gumble/udp.go
|
||||||
Tests are mostly local encrypt/decrypt round trips. Add captured/reference
|
Tests are mostly local encrypt/decrypt round trips. Add captured/reference
|
||||||
vectors from current Mumble for CryptSetup, encrypted audio, ping, packet
|
vectors from current Mumble for CryptSetup, encrypted audio, ping, packet
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, dat
|
|||||||
c.udpMu.RLock()
|
c.udpMu.RLock()
|
||||||
udpConn := c.udpConn
|
udpConn := c.udpConn
|
||||||
udpCryptoOut := c.udpCryptoOut
|
udpCryptoOut := c.udpCryptoOut
|
||||||
|
udpProtobuf := c.udpProtobuf
|
||||||
c.udpMu.RUnlock()
|
c.udpMu.RUnlock()
|
||||||
if !c.udpFallbackLogged.Swap(true) {
|
if !c.udpFallbackLogged.Swap(true) {
|
||||||
if c.Config.DisableUDP {
|
if c.Config.DisableUDP {
|
||||||
@@ -377,6 +378,12 @@ func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, dat
|
|||||||
log.Info("UDP crypto not ready, audio using TCP tunnel")
|
log.Info("UDP crypto not ready, audio using TCP tunnel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if udpProtobuf {
|
||||||
|
// Mumble 1.5 uses the native UDP protobuf envelope even when audio is
|
||||||
|
// carried inside the TCP UDPTunnel packet.
|
||||||
|
payload := append([]byte{0x00}, encodeUDPAudio(uint32(target), uint64(sequence), data, final, X, Y, Z)...)
|
||||||
|
return c.Conn.WritePacket(1, payload)
|
||||||
|
}
|
||||||
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
|
|||||||
log.Warn("handleUDPTunnel: empty buffer")
|
log.Warn("handleUDPTunnel: empty buffer")
|
||||||
return errInvalidProtobuf
|
return errInvalidProtobuf
|
||||||
}
|
}
|
||||||
|
c.udpMu.RLock()
|
||||||
|
protobufEnvelope := c.udpProtobuf
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if protobufEnvelope && buffer[0] == 0x00 {
|
||||||
|
session, frame, opusData, terminator, context, position, volume := decodeUDPAudio(buffer[1:])
|
||||||
|
if session == 0 {
|
||||||
|
return errInvalidProtobuf
|
||||||
|
}
|
||||||
|
c.dispatchOpus15(0, session, int64(frame), opusData, terminator, context, position, volume)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
audioType := (buffer[0] >> 5) & 0x7
|
audioType := (buffer[0] >> 5) & 0x7
|
||||||
audioTarget := buffer[0] & 0x1F
|
audioTarget := buffer[0] & 0x1F
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package gumble_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.stormux.org/storm/barnard/gumble/gumble"
|
||||||
|
_ "git.stormux.org/storm/barnard/gumble/opus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationAudioListener struct{ packets chan *gumble.AudioPacket }
|
||||||
|
|
||||||
|
func (l *integrationAudioListener) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||||
|
go func() {
|
||||||
|
for p := range e.C {
|
||||||
|
l.packets <- p
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalMumbleAudioRoundTrip sends generated 440 Hz audio through a real
|
||||||
|
// local Mumble server and requires the other client to decode non-silent PCM.
|
||||||
|
func TestLocalMumbleAudioRoundTrip(t *testing.T) {
|
||||||
|
if os.Getenv("BARNARD_MUMBLE_INTEGRATION") != "1" {
|
||||||
|
t.Skip("set BARNARD_MUMBLE_INTEGRATION=1")
|
||||||
|
}
|
||||||
|
newConfig := func(name string) *gumble.Config {
|
||||||
|
c := gumble.NewConfig()
|
||||||
|
c.Address = "localhost:64738"
|
||||||
|
c.Username = name
|
||||||
|
c.DisableUDP = true
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
||||||
|
listener := &integrationAudioListener{packets: make(chan *gumble.AudioPacket, 8)}
|
||||||
|
recvConfig := newConfig(fmt.Sprintf("barnard-it-recv-%d", time.Now().UnixNano()))
|
||||||
|
recvConfig.AttachAudio(listener)
|
||||||
|
receiver, err := gumble.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, recvConfig, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer receiver.Disconnect()
|
||||||
|
sendConfig := newConfig(fmt.Sprintf("barnard-it-send-%d", time.Now().UnixNano()))
|
||||||
|
sender, err := gumble.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, sendConfig, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer sender.Disconnect()
|
||||||
|
if sender.AudioEncoder == nil {
|
||||||
|
t.Fatal("no negotiated audio encoder")
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
frame := make([]int16, sendConfig.AudioFrameSize())
|
||||||
|
for i := range frame {
|
||||||
|
frame[i] = int16(12000 * math.Sin(2*math.Pi*440*float64(i)/gumble.AudioSampleRate))
|
||||||
|
}
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
raw, err := sender.AudioEncoder.Encode(frame, len(frame), sendConfig.AudioDataBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := sender.WriteAudio(4, 0, int64(i), i == 7, raw, nil, nil, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case packet := <-listener.packets:
|
||||||
|
peak := 0
|
||||||
|
for _, sample := range packet.AudioBuffer {
|
||||||
|
v := int(sample)
|
||||||
|
if v < 0 {
|
||||||
|
v = -v
|
||||||
|
}
|
||||||
|
if v > peak {
|
||||||
|
peak = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if peak < 500 {
|
||||||
|
t.Fatalf("received silent audio peak=%d", peak)
|
||||||
|
}
|
||||||
|
case <-time.After(8 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for relayed audio")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package gumble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regression: Mumble 1.5 decodes UDPTunnel packets as native protobuf UDP
|
||||||
|
// envelopes. Sending the legacy tunnel envelope made a current server silently
|
||||||
|
// discard otherwise valid Opus audio when UDP was unavailable.
|
||||||
|
func TestWriteAudioUsesProtobufEnvelopeForTCPFallback(t *testing.T) {
|
||||||
|
local, remote := net.Pipe()
|
||||||
|
defer remote.Close()
|
||||||
|
c := &Client{Config: NewConfig(), Conn: NewConn(local), udpProtobuf: true}
|
||||||
|
c.Config.DisableUDP = true
|
||||||
|
result := make(chan struct {
|
||||||
|
typ uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
go func() {
|
||||||
|
typ, data, err := NewConn(remote).ReadPacket()
|
||||||
|
result <- struct {
|
||||||
|
typ uint16
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
}{typ, data, err}
|
||||||
|
}()
|
||||||
|
if err := c.WriteAudio(4, 2, 300, true, []byte{0xaa, 0xbb}, nil, nil, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := <-result
|
||||||
|
if got.err != nil || got.typ != 1 {
|
||||||
|
t.Fatalf("packet: type=%d err=%v", got.typ, got.err)
|
||||||
|
}
|
||||||
|
want := mustDecodeHex("00080220ac022a02aabb800101")
|
||||||
|
if string(got.data) != string(want) {
|
||||||
|
t.Fatalf("payload=%x want=%x", got.data, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user