Skip to content

update to voice gateway v8 #431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.31.0
golang.org/x/crypto v0.35.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxE
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
4 changes: 2 additions & 2 deletions voice/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ func (c *connImpl) handleMessage(op Opcode, data GatewayMessageData) {
Data: GatewayMessageDataSelectProtocolData{
Address: ourAddress,
Port: ourPort,
Mode: EncryptionModeNormal,
Mode: EncryptionModeAEADXChaCha20Poly1305RTPSize,
},
}); err != nil {
c.config.Logger.Error("voice: failed to send select protocol", slog.Any("err", err))
}

case GatewayMessageDataSessionDescription:
c.udp.SetSecretKey(d.SecretKey)
c.udp.SetSecretKey(d.Mode, d.SecretKey)
c.openedChan <- struct{}{}

case GatewayMessageDataSpeaking:
Expand Down
19 changes: 14 additions & 5 deletions voice/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var (
)

// GatewayVersion is the version of the voice gateway we are using.
const GatewayVersion = 4
const GatewayVersion = 8

// Status returns the current status of the gateway.
type Status int
Expand Down Expand Up @@ -109,6 +109,7 @@ type gatewayImpl struct {

ssrc uint32
state State
seq int

conn *websocket.Conn
connMu sync.Mutex
Expand All @@ -127,13 +128,12 @@ func (g *gatewayImpl) SSRC() uint32 {

func (g *gatewayImpl) Open(ctx context.Context, state State) error {
g.config.Logger.Debug("opening voice gateway connection")
g.state = state

g.connMu.Lock()
defer g.connMu.Unlock()
if g.conn != nil {
return ErrGatewayAlreadyConnected
}
g.state = state
g.status = StatusConnecting

gatewayURL := fmt.Sprintf("wss://%s?v=%d", state.Endpoint, GatewayVersion)
Expand Down Expand Up @@ -181,6 +181,7 @@ func (g *gatewayImpl) CloseWithCode(code int, message string) {
// clear resume data as we closed gracefully
if code == websocket.CloseNormalClosure || code == websocket.CloseGoingAway {
g.ssrc = 0
g.seq = 0
}
}
}
Expand All @@ -200,7 +201,10 @@ func (g *gatewayImpl) sendHeartbeat() {
ctx, cancel := context.WithTimeout(context.Background(), g.heartbeatInterval)
defer cancel()

if err := g.Send(ctx, OpcodeHeartbeat, GatewayMessageDataHeartbeat(g.lastNonce)); err != nil {
if err := g.Send(ctx, OpcodeHeartbeat, GatewayMessageDataHeartbeat{
T: g.lastNonce,
SeqAck: g.seq,
}); err != nil {
if !errors.Is(err, ErrGatewayNotConnected) || errors.Is(err, syscall.EPIPE) {
return
}
Expand Down Expand Up @@ -248,6 +252,10 @@ loop:
continue
}

if message.Seq > 0 {
g.seq = message.Seq
}

switch d := message.D.(type) {
case GatewayMessageDataHello:
g.status = StatusWaitingForReady
Expand All @@ -256,7 +264,7 @@ loop:
go g.heartbeat()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if g.ssrc == 0 {
if g.ssrc == 0 || g.seq == 0 {
g.status = StatusIdentifying
err = g.Send(ctx, OpcodeIdentify, GatewayMessageDataIdentify{
GuildID: g.state.GuildID,
Expand All @@ -270,6 +278,7 @@ loop:
GuildID: g.state.GuildID,
SessionID: g.state.SessionID,
Token: g.state.Token,
SeqAck: g.seq,
})
}
cancel()
Expand Down
34 changes: 25 additions & 9 deletions voice/gateway_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (

// GatewayMessage represents a voice gateway message
type GatewayMessage struct {
Op Opcode `json:"op"`
D GatewayMessageData `json:"d,omitempty"`
Op Opcode `json:"op"`
D GatewayMessageData `json:"d,omitempty"`
Seq int `json:"s,omitempty"`
}

// UnmarshalJSON unmarshalls the GatewayMessage from json
Expand Down Expand Up @@ -125,13 +126,16 @@ type GatewayMessageDataHello struct {

func (GatewayMessageDataHello) voiceGatewayMessageData() {}

type GatewayMessageDataHeartbeat int64
type GatewayMessageDataHeartbeat struct {
T int64 `json:"t"`
SeqAck int `json:"seq_ack"`
}

func (GatewayMessageDataHeartbeat) voiceGatewayMessageData() {}

type GatewayMessageDataSessionDescription struct {
Mode string `json:"mode"`
SecretKey [32]byte `json:"secret_key"`
Mode EncryptionMode `json:"mode"`
SecretKey []byte `json:"secret_key"`
}

func (GatewayMessageDataSessionDescription) voiceGatewayMessageData() {}
Expand All @@ -158,11 +162,22 @@ type GatewayMessageDataSelectProtocolData struct {
// EncryptionMode is the encryption mode used for voice data.
type EncryptionMode string

// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes.
// All possible EncryptionMode(s) https://discord.com/developers/docs/topics/voice-connections#transport-encryption-and-sending-voice.
const (
EncryptionModeNormal EncryptionMode = "xsalsa20_poly1305"
EncryptionModeSuffix EncryptionMode = "xsalsa20_poly1305_suffix"
EncryptionModeLite EncryptionMode = "xsalsa20_poly1305_lite"
// EncryptionModeAEADAES256GCMRTPSize is the preferred encryption mode.
EncryptionModeAEADAES256GCMRTPSize EncryptionMode = "aead_aes256_gcm_rtpsize"
// EncryptionModeAEADXChaCha20Poly1305RTPSize is the required encryption mode.
EncryptionModeAEADXChaCha20Poly1305RTPSize EncryptionMode = "aead_xchacha20_poly1305_rtpsize"
// Deprecated: EncryptionModeXSalsa20Poly1305LiteRTPSize is deprecated.
EncryptionModeXSalsa20Poly1305LiteRTPSize EncryptionMode = "xsalsa20_poly1305_lite_rtpsize"
// Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated.
EncryptionModeAEADAES256GCM EncryptionMode = "aead_aes256_gcm"
// Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated.
EncryptionModeXSalsa20Poly1305 EncryptionMode = "xsalsa20_poly1305"
// Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated.
EncryptionModeXSalsa20Poly1305Suffix EncryptionMode = "xsalsa20_poly1305_suffix"
// Deprecated: EncryptionModeXSalsa20Poly1305Lite is deprecated.
EncryptionModeXSalsa20Poly1305Lite EncryptionMode = "xsalsa20_poly1305_lite"
)

type GatewayMessageDataSpeaking struct {
Expand All @@ -187,6 +202,7 @@ type GatewayMessageDataResume struct {
GuildID snowflake.ID `json:"server_id"` // wtf is this?
SessionID string `json:"session_id"`
Token string `json:"token"`
SeqAck int `json:"seq"`
}

func (GatewayMessageDataResume) voiceGatewayMessageData() {}
Expand Down
45 changes: 35 additions & 10 deletions voice/udp_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package voice

import (
"context"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
Expand All @@ -13,6 +14,7 @@ import (
"sync"
"time"

"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/nacl/secretbox"
)

Expand Down Expand Up @@ -49,7 +51,7 @@ type (
RemoteAddr() net.Addr

// SetSecretKey sets the secret key used to encrypt packets.
SetSecretKey(secretKey [32]byte)
SetSecretKey(encryptionMode EncryptionMode, secretKey []byte)

SetDeadline(t time.Time) error

Expand Down Expand Up @@ -106,12 +108,15 @@ type udpConnImpl struct {
conn net.Conn
connMu sync.Mutex

packet [12]byte
secretKey [32]byte
cipher cipher.AEAD

sequence uint16
timestamp uint32
nonce [24]byte
packet [12]byte

sequence uint16
i uint32
timestamp uint32
nonce [24]byte
associatedData [12]byte

receiveNonce [24]byte
receiveBuffer []byte
Expand All @@ -129,8 +134,23 @@ func (u *udpConnImpl) RemoteAddr() net.Addr {
return u.conn.RemoteAddr()
}

func (u *udpConnImpl) SetSecretKey(secretKey [32]byte) {
u.secretKey = secretKey
func (u *udpConnImpl) SetSecretKey(encryptionMode EncryptionMode, secretKey []byte) {
var (
c cipher.AEAD
err error
)
switch encryptionMode {
case EncryptionModeAEADXChaCha20Poly1305RTPSize:
c, err = chacha20poly1305.NewX(secretKey)
default:
u.config.Logger.Error("unknown encryption mode", slog.String("mode", string(encryptionMode)))
return
}
if err != nil {
u.config.Logger.Error("failed to create cipher", slog.Any("err", err))
return
}
u.cipher = c
}

func (u *udpConnImpl) SetDeadline(t time.Time) error {
Expand Down Expand Up @@ -222,6 +242,9 @@ func (u *udpConnImpl) Write(p []byte) (int, error) {
binary.BigEndian.PutUint16(u.packet[2:4], u.sequence)
u.sequence++

u.i++
binary.BigEndian.PutUint32(u.associatedData[:], u.i)

binary.BigEndian.PutUint32(u.packet[4:8], u.timestamp)
u.timestamp += 960

Expand All @@ -231,7 +254,9 @@ func (u *udpConnImpl) Write(p []byte) (int, error) {
u.connMu.Lock()
conn := u.conn
u.connMu.Unlock()
if _, err := conn.Write(secretbox.Seal(u.packet[:], p, &u.nonce, &u.secretKey)); err != nil {

//secretbox.Seal(u.packet[:], p, &u.nonce, &u.secretKey)
if _, err := conn.Write(u.cipher.Seal(u.packet[12:], u.nonce[:], p, u.associatedData[:])); err != nil {
return 0, fmt.Errorf("failed to write packet: %w", err)
}
return len(p), nil
Expand Down Expand Up @@ -261,7 +286,7 @@ func (u *udpConnImpl) ReadPacket() (*Packet, error) {

copy(u.receiveNonce[:], u.receiveBuffer[0:OpusPacketHeaderSize])

opus, ok := secretbox.Open(nil, u.receiveBuffer[OpusPacketHeaderSize:i], &u.receiveNonce, &u.secretKey)
opus, ok := secretbox.Open(nil, u.receiveBuffer[OpusPacketHeaderSize:i], &u.receiveNonce, nil)
if !ok {
return nil, ErrDecryptionFailed
}
Expand Down
Loading