~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
197 lines · 3.6 KB Raw History
package guestagent

import (
	"errors"
	"io"
	"log/slog"
	"net"
	"strconv"
	"sync"
	"time"

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

const (
	bridgeChunk = 16 << 10

	bridgeHighWater = 1 << 20

	bridgeLowWater = 256 << 10

	bridgeDrainTimeout = 30 * time.Second

	bridgeDialTimeout = 10 * time.Second
)

type bridge struct {
	dc *webrtc.DataChannel

	port int

	log *slog.Logger

	ready chan struct{}

	low chan struct{}

	done chan struct{}

	mu sync.Mutex

	conn net.Conn

	once sync.Once

	sent int64

	received int64
}

func (s *session) onDataChannel(dc *webrtc.DataChannel) {
	log := s.log.With("channel", channelID(dc))
	if dc.Label() != s.tag {
		log.Warn("a datachannel label is not the session tag; closing it", "labelBytes", len(dc.Label()))
		go func() { _ = dc.Close() }()
		return
	}
	b := &bridge{
		dc:    dc,
		port:  s.port,
		log:   log,
		ready: make(chan struct{}),
		low:   make(chan struct{}, 1),
		done:  make(chan struct{}),
	}
	dc.SetBufferedAmountLowThreshold(bridgeLowWater)
	dc.OnBufferedAmountLow(func() {
		select {
		case b.low <- struct{}{}:
		default:
		}
	})
	dc.OnMessage(b.fromChannel)
	dc.OnClose(func() { b.finish("the datachannel closed") })
	dc.OnOpen(func() { go b.toChannel() })
	go b.dial()
}

func channelID(dc *webrtc.DataChannel) string {
	if id := dc.ID(); id != nil {
		return strconv.Itoa(int(*id))
	}
	return "unset"
}

func (b *bridge) dial() {
	conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(b.port)), bridgeDialTimeout)
	if err != nil {
		b.log.Warn("dialing the tunnel port failed", "port", b.port, "err", err.Error())
		close(b.ready)
		b.finish("the tunnel port is not reachable")
		return
	}
	b.mu.Lock()
	b.conn = conn
	b.mu.Unlock()
	close(b.ready)
	b.log.Info("a datachannel is bridged to the tunnel port", "port", b.port)
	select {
	case <-b.done:
		_ = conn.Close()
	default:
	}
}

func (b *bridge) tcp() net.Conn {
	select {
	case <-b.ready:
	case <-b.done:
		return nil
	}
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.conn
}

func (b *bridge) fromChannel(msg webrtc.DataChannelMessage) {
	conn := b.tcp()
	if conn == nil {
		return
	}
	if _, err := conn.Write(msg.Data); err != nil {
		b.finish("writing to the tunnel port failed")
		return
	}
	b.mu.Lock()
	b.received += int64(len(msg.Data))
	b.mu.Unlock()
}

func (b *bridge) toChannel() {
	conn := b.tcp()
	if conn == nil {
		return
	}
	buf := make([]byte, bridgeChunk)
	for {
		n, err := conn.Read(buf)
		if n > 0 {
			if sendErr := b.dc.Send(buf[:n]); sendErr != nil {
				b.finish("sending on the datachannel failed")
				return
			}
			b.mu.Lock()
			b.sent += int64(n)
			b.mu.Unlock()
			if !b.waitBelow(bridgeHighWater) {
				return
			}
		}
		if err != nil {
			if !errors.Is(err, io.EOF) {
				b.finish("reading the tunnel port failed")
				return
			}
			b.dc.SetBufferedAmountLowThreshold(0)
			if !b.waitBelow(1) {
				return
			}
			b.finish("the tunnel port closed")
			return
		}
	}
}

func (b *bridge) waitBelow(limit uint64) bool {
	deadline := time.NewTimer(bridgeDrainTimeout)
	defer deadline.Stop()
	for b.dc.BufferedAmount() >= limit {
		select {
		case <-b.low:
		case <-b.done:
			return false
		case <-deadline.C:
			deadline.Reset(bridgeDrainTimeout)
			if b.dc.ReadyState() != webrtc.DataChannelStateOpen {
				b.finish("the datachannel is not open")
				return false
			}
		}
	}
	return true
}

func (b *bridge) finish(reason string) {
	b.once.Do(func() {
		close(b.done)
		b.mu.Lock()
		conn, sent, received := b.conn, b.sent, b.received
		b.mu.Unlock()
		if conn != nil {
			_ = conn.Close()
		}
		go func() { _ = b.dc.Close() }()
		b.log.Info("a datachannel bridge ends", "reason", reason, "sentBytes", sent, "receivedBytes", received)
	})
}