github.com/diamondburned/arikawa@v1.3.14/voice/udp/udp.go (about)

     1  package udp
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/binary"
     7  	"io"
     8  	"net"
     9  	"time"
    10  
    11  	"github.com/pkg/errors"
    12  	"golang.org/x/crypto/nacl/secretbox"
    13  	"golang.org/x/time/rate"
    14  )
    15  
    16  // Dialer is the default dialer that this package uses for all its dialing.
    17  var Dialer = net.Dialer{
    18  	Timeout: 10 * time.Second,
    19  }
    20  
    21  // ErrClosed is returned if a Write was called on a closed connection.
    22  var ErrClosed = errors.New("UDP connection closed")
    23  
    24  type Connection struct {
    25  	GatewayIP   string
    26  	GatewayPort uint16
    27  
    28  	mutex chan struct{} // for ctx
    29  
    30  	context context.Context
    31  	conn    net.Conn
    32  	ssrc    uint32
    33  
    34  	frequency rate.Limiter
    35  	packet    [12]byte
    36  	secret    [32]byte
    37  
    38  	sequence  uint16
    39  	timestamp uint32
    40  	nonce     [24]byte
    41  }
    42  
    43  func DialConnectionCtx(ctx context.Context, addr string, ssrc uint32) (*Connection, error) {
    44  	// Create a new UDP connection.
    45  	conn, err := Dialer.DialContext(ctx, "udp", addr)
    46  	if err != nil {
    47  		return nil, errors.Wrap(err, "failed to dial host")
    48  	}
    49  
    50  	// https://discordapp.com/developers/docs/topics/voice-connections#ip-discovery
    51  	ssrcBuffer := [70]byte{
    52  		0x1, 0x2,
    53  	}
    54  	binary.BigEndian.PutUint16(ssrcBuffer[2:4], 70)
    55  	binary.BigEndian.PutUint32(ssrcBuffer[4:8], ssrc)
    56  
    57  	_, err = conn.Write(ssrcBuffer[:])
    58  	if err != nil {
    59  		return nil, errors.Wrap(err, "failed to write SSRC buffer")
    60  	}
    61  
    62  	var ipBuffer [70]byte
    63  
    64  	// ReadFull makes sure to read all 70 bytes.
    65  	_, err = io.ReadFull(conn, ipBuffer[:])
    66  	if err != nil {
    67  		return nil, errors.Wrap(err, "failed to read IP buffer")
    68  	}
    69  
    70  	ipbody := ipBuffer[4:68]
    71  
    72  	nullPos := bytes.Index(ipbody, []byte{'\x00'})
    73  	if nullPos < 0 {
    74  		return nil, errors.New("UDP IP discovery did not contain a null terminator")
    75  	}
    76  
    77  	ip := ipbody[:nullPos]
    78  	port := binary.LittleEndian.Uint16(ipBuffer[68:70])
    79  
    80  	// https://discordapp.com/developers/docs/topics/voice-connections#encrypting-and-sending-voice
    81  	packet := [12]byte{
    82  		0: 0x80, // Version + Flags
    83  		1: 0x78, // Payload Type
    84  		// [2:4] // Sequence
    85  		// [4:8] // Timestamp
    86  	}
    87  
    88  	// Write SSRC to the header.
    89  	binary.BigEndian.PutUint32(packet[8:12], ssrc) // SSRC
    90  
    91  	return &Connection{
    92  		GatewayIP:   string(ip),
    93  		GatewayPort: port,
    94  		// 50 sends per second, 960 samples each at 48kHz
    95  		frequency: *rate.NewLimiter(rate.Every(20*time.Millisecond), 1),
    96  		context:   context.Background(),
    97  		mutex:     make(chan struct{}, 1),
    98  		packet:    packet,
    99  		ssrc:      ssrc,
   100  		conn:      conn,
   101  	}, nil
   102  }
   103  
   104  // UseSecret uses the given secret. This method is not thread-safe, so it should
   105  // only be used right after initialization.
   106  func (c *Connection) UseSecret(secret [32]byte) {
   107  	c.secret = secret
   108  }
   109  
   110  // UseContext lets the connection use the given context for its Write method.
   111  // WriteCtx will override this context.
   112  func (c *Connection) UseContext(ctx context.Context) error {
   113  	c.mutex <- struct{}{}
   114  	defer func() { <-c.mutex }()
   115  
   116  	return c.useContext(ctx)
   117  }
   118  
   119  func (c *Connection) useContext(ctx context.Context) error {
   120  	if c.conn == nil {
   121  		return ErrClosed
   122  	}
   123  
   124  	if c.context == ctx {
   125  		return nil
   126  	}
   127  
   128  	c.context = ctx
   129  
   130  	if deadline, ok := c.context.Deadline(); ok {
   131  		return c.conn.SetWriteDeadline(deadline)
   132  	} else {
   133  		return c.conn.SetWriteDeadline(time.Time{})
   134  	}
   135  }
   136  
   137  func (c *Connection) Close() error {
   138  	c.mutex <- struct{}{}
   139  	err := c.conn.Close()
   140  	c.conn = nil
   141  	<-c.mutex
   142  	return err
   143  }
   144  
   145  // Write sends bytes into the voice UDP connection.
   146  func (c *Connection) Write(b []byte) (int, error) {
   147  	select {
   148  	case c.mutex <- struct{}{}:
   149  		defer func() { <-c.mutex }()
   150  	case <-c.context.Done():
   151  		return 0, c.context.Err()
   152  	}
   153  
   154  	if c.conn == nil {
   155  		return 0, ErrClosed
   156  	}
   157  
   158  	return c.write(b)
   159  }
   160  
   161  // WriteCtx sends bytes into the voice UDP connection with a timeout.
   162  func (c *Connection) WriteCtx(ctx context.Context, b []byte) (int, error) {
   163  	select {
   164  	case c.mutex <- struct{}{}:
   165  		defer func() { <-c.mutex }()
   166  	case <-c.context.Done():
   167  		return 0, c.context.Err()
   168  	case <-ctx.Done():
   169  		return 0, ctx.Err()
   170  	}
   171  
   172  	if err := c.useContext(ctx); err != nil {
   173  		return 0, errors.Wrap(err, "failed to use context")
   174  	}
   175  
   176  	return c.write(b)
   177  }
   178  
   179  // write is thread-unsafe.
   180  func (c *Connection) write(b []byte) (int, error) {
   181  	// Write a new sequence.
   182  	binary.BigEndian.PutUint16(c.packet[2:4], c.sequence)
   183  	c.sequence++
   184  
   185  	binary.BigEndian.PutUint32(c.packet[4:8], c.timestamp)
   186  	c.timestamp += 960 // Samples
   187  
   188  	copy(c.nonce[:], c.packet[:])
   189  
   190  	if err := c.frequency.Wait(c.context); err != nil {
   191  		return 0, errors.Wrap(err, "failed to wait for frequency tick")
   192  	}
   193  
   194  	toSend := secretbox.Seal(c.packet[:], b, &c.nonce, &c.secret)
   195  
   196  	n, err := c.conn.Write(toSend)
   197  	if err != nil {
   198  		return n, errors.Wrap(err, "failed to write to UDP connection")
   199  	}
   200  
   201  	// We're not really returning everything, since we're "sealing" the bytes.
   202  	return len(b), nil
   203  }