~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
238 lines · 5.4 KB Raw History
package webrtcproxy

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"sync"
	"time"

	"github.com/pion/webrtc/v4"
)

const (
	chunk = 16 << 10

	highWater = 1 << 20

	lowWater = 256 << 10

	drainTimeout = 30 * time.Second
)

var ErrClosed = errors.New("webrtcproxy: the datachannel is closed")

type Conn struct {
	pc *webrtc.PeerConnection

	dc *webrtc.DataChannel

	log *slog.Logger

	in *io.PipeReader

	inW *io.PipeWriter

	opened chan struct{}

	low chan struct{}

	done chan struct{}

	failed chan struct{}

	failOnce sync.Once

	closeOnce sync.Once

	mu sync.Mutex

	sent int64

	received int64
}

func newConn(tag string, log *slog.Logger) (*Conn, error) {
	pc, err := newAPI(log).NewPeerConnection(webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{STUNServer}}}})
	if err != nil {
		return nil, fmt.Errorf("webrtcproxy: the peer connection: %w", err)
	}
	ordered := true
	dc, err := pc.CreateDataChannel(tag, &webrtc.DataChannelInit{Ordered: &ordered})
	if err != nil {
		_ = pc.Close()
		return nil, fmt.Errorf("webrtcproxy: the datachannel: %w", err)
	}
	in, inW := io.Pipe()
	c := &Conn{
		pc:     pc,
		dc:     dc,
		log:    log,
		in:     in,
		inW:    inW,
		opened: make(chan struct{}),
		low:    make(chan struct{}, 1),
		done:   make(chan struct{}),
		failed: make(chan struct{}),
	}
	dc.SetBufferedAmountLowThreshold(lowWater)
	dc.OnBufferedAmountLow(func() {
		select {
		case c.low <- struct{}{}:
		default:
		}
	})
	dc.OnOpen(func() { close(c.opened) })
	dc.OnMessage(func(msg webrtc.DataChannelMessage) {
		if _, err := c.inW.Write(msg.Data); err != nil {
			return
		}
		c.mu.Lock()
		c.received += int64(len(msg.Data))
		c.mu.Unlock()
	})
	dc.OnClose(func() { c.finish(io.EOF) })
	pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
		log.Info("the ICE connection state changed", "state", state.String())
	})
	pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
		log.Info("the peer connection state changed", "state", state.String())
		switch state {
		case webrtc.PeerConnectionStateConnected:
			log.Info("the selected candidate pair", selectedPair(pc)...)
		case webrtc.PeerConnectionStateFailed:
			c.finish(fmt.Errorf("webrtcproxy: the peer connection failed (ICE %s)", pc.ICEConnectionState()))
		case webrtc.PeerConnectionStateClosed:
			c.finish(io.EOF)
		}
	})
	return c, nil
}

func (c *Conn) finish(err error) {
	c.failOnce.Do(func() {
		close(c.failed)
		_ = c.inW.CloseWithError(err)
	})
}

func (c *Conn) waitOpen(ctx context.Context, timeout time.Duration) error {
	deadline := time.NewTimer(timeout)
	defer deadline.Stop()
	select {
	case <-c.opened:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	case <-c.failed:
		return fmt.Errorf("webrtcproxy: the peer connection failed before the datachannel opened: ICE %s, connection %s; a symmetric NAT on either side defeats STUN-only WebRTC (stun:stun.cloudflare.com:3478, no TURN)",
			c.pc.ICEConnectionState(), c.pc.ConnectionState())
	case <-deadline.C:
		return fmt.Errorf("webrtcproxy: the datachannel did not open within %s: ICE %s, connection %s; a symmetric NAT on either side defeats STUN-only WebRTC (stun:stun.cloudflare.com:3478, no TURN)",
			timeout, c.pc.ICEConnectionState(), c.pc.ConnectionState())
	}
}

func (c *Conn) Read(p []byte) (int, error) {
	return c.in.Read(p)
}

func (c *Conn) Write(p []byte) (int, error) {
	written := 0
	for written < len(p) {
		n := min(len(p)-written, chunk)
		if err := c.dc.Send(p[written : written+n]); err != nil {
			return written, fmt.Errorf("webrtcproxy: sending on the datachannel: %w", err)
		}
		written += n
		c.mu.Lock()
		c.sent += int64(n)
		c.mu.Unlock()
		if err := c.waitBelow(highWater); err != nil {
			return written, err
		}
	}
	return written, nil
}

func (c *Conn) waitBelow(limit uint64) error {
	deadline := time.NewTimer(drainTimeout)
	defer deadline.Stop()
	for c.dc.BufferedAmount() >= limit {
		select {
		case <-c.low:
		case <-c.done:
			return ErrClosed
		case <-c.failed:
			return ErrClosed
		case <-deadline.C:
			deadline.Reset(drainTimeout)
			if c.dc.ReadyState() != webrtc.DataChannelStateOpen {
				return ErrClosed
			}
		}
	}
	return nil
}

func (c *Conn) Drain() error {
	c.dc.SetBufferedAmountLowThreshold(0)
	return c.waitBelow(1)
}

func (c *Conn) Close() error {
	c.closeOnce.Do(func() {
		close(c.done)
		c.finish(io.EOF)
		_ = c.dc.Close()
		_ = c.pc.Close()
		c.mu.Lock()
		sent, received := c.sent, c.received
		c.mu.Unlock()
		c.log.Info("the datachannel session ends", "sentBytes", sent, "receivedBytes", received)
	})
	return nil
}

func Pipe(ctx context.Context, c *Conn, stdin io.Reader, stdout io.Writer) error {
	remote := make(chan error, 1)
	go func() {
		_, err := io.Copy(stdout, c)
		remote <- err
	}()
	local := make(chan error, 1)
	go func() {
		_, err := io.Copy(c, stdin)
		if err == nil {
			err = c.Drain()
		}
		local <- err
	}()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case err := <-remote:
			return err
		case err := <-local:
			if err != nil {
				return err
			}
			local = nil
		}
	}
}

func selectedPair(pc *webrtc.PeerConnection) []any {
	sctp := pc.SCTP()
	if sctp == nil {
		return []any{"local", "unknown", "remote", "unknown"}
	}
	pair, err := sctp.Transport().ICETransport().GetSelectedCandidatePair()
	if err != nil || pair == nil || pair.Local == nil || pair.Remote == nil {
		return []any{"local", "unknown", "remote", "unknown"}
	}
	return []any{"local", pair.Local.Typ.String(), "remote", pair.Remote.Typ.String(), "protocol", pair.Local.Protocol.String()}
}