snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 20:06:07 +05:00
commit 8acb1a3a29
394 changed files with 78746 additions and 0 deletions

107
internal/mux/endpoint.go Normal file
View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package mux
import (
"errors"
"io"
"net"
"time"
"github.com/pion/ice/v4"
"github.com/pion/transport/v4/packetio"
)
// Endpoint implements net.Conn. It is used to read muxed packets.
type Endpoint struct {
mux *Mux
buffer *packetio.Buffer
onClose func()
}
// Close unregisters the endpoint from the Mux.
func (e *Endpoint) Close() (err error) {
if e.onClose != nil {
e.onClose()
}
if err = e.close(); err != nil {
return err
}
e.mux.RemoveEndpoint(e)
return nil
}
func (e *Endpoint) close() error {
return e.buffer.Close()
}
// Read reads a packet of len(p) bytes from the underlying conn
// that are matched by the associated MuxFunc.
func (e *Endpoint) Read(p []byte) (int, error) {
return e.buffer.Read(p)
}
// ReadFrom reads a packet of len(p) bytes from the underlying conn
// that are matched by the associated MuxFunc.
func (e *Endpoint) ReadFrom(p []byte) (int, net.Addr, error) {
i, err := e.Read(p)
return i, nil, err
}
// Write writes len(p) bytes to the underlying conn.
func (e *Endpoint) Write(p []byte) (int, error) {
n, err := e.mux.nextConn.Write(p)
if errors.Is(err, ice.ErrNoCandidatePairs) {
return 0, nil
} else if errors.Is(err, ice.ErrClosed) {
return 0, io.ErrClosedPipe
}
return n, err
}
// WriteTo writes len(p) bytes to the underlying conn.
func (e *Endpoint) WriteTo(p []byte, _ net.Addr) (int, error) {
return e.Write(p)
}
// LocalAddr returns the local network address, if known.
func (e *Endpoint) LocalAddr() net.Addr {
return e.mux.nextConn.LocalAddr()
}
// RemoteAddr returns the remote network address, if known.
func (e *Endpoint) RemoteAddr() net.Addr {
return e.mux.nextConn.RemoteAddr()
}
// SetDeadline sets the read and write deadlines on the shared underlying
// connection. Because the connection is shared, this applies to all endpoints
// on the mux. Per-endpoint read deadlines can be set with SetReadDeadline.
func (e *Endpoint) SetDeadline(t time.Time) error {
return e.mux.nextConn.SetDeadline(t)
}
// SetReadDeadline sets the read deadline for this Endpoint's internal
// packet buffer. This timeout applies only to reads from this Endpoint,
// not to the shared underlying connection.
func (e *Endpoint) SetReadDeadline(t time.Time) error {
return e.buffer.SetReadDeadline(t)
}
// SetWriteDeadline sets the write deadline on the shared underlying connection.
// Because the connection is shared, this applies to all endpoints on the mux.
func (e *Endpoint) SetWriteDeadline(t time.Time) error {
return e.mux.nextConn.SetWriteDeadline(t)
}
// SetOnClose is a user set callback that
// will be executed when `Close` is called.
func (e *Endpoint) SetOnClose(onClose func()) {
e.onClose = onClose
}

216
internal/mux/mux.go Normal file
View File

@@ -0,0 +1,216 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package mux multiplexes packets on a single socket (RFC7983)
package mux
import (
"errors"
"io"
"net"
"sync"
"github.com/pion/ice/v4"
"github.com/pion/logging"
"github.com/pion/transport/v4/packetio"
)
const (
// The maximum amount of data that can be buffered before returning errors.
maxBufferSize = 1000 * 1000 // 1MB
// How many total pending packets can be cached.
maxPendingPackets = 15
)
// Config collects the arguments to mux.Mux construction into
// a single structure.
type Config struct {
Conn net.Conn
BufferSize int
LoggerFactory logging.LoggerFactory
}
// Mux allows multiplexing.
type Mux struct {
nextConn net.Conn
bufferSize int
lock sync.Mutex
endpoints map[*Endpoint]MatchFunc
isClosed bool
pendingPackets [][]byte
closedCh chan struct{}
log logging.LeveledLogger
}
// NewMux creates a new Mux.
func NewMux(config Config) *Mux {
mux := &Mux{
nextConn: config.Conn,
endpoints: make(map[*Endpoint]MatchFunc),
bufferSize: config.BufferSize,
closedCh: make(chan struct{}),
log: config.LoggerFactory.NewLogger("mux"),
}
go mux.readLoop()
return mux
}
// NewEndpoint creates a new Endpoint.
func (m *Mux) NewEndpoint(matchFunc MatchFunc) *Endpoint {
endpoint := &Endpoint{
mux: m,
buffer: packetio.NewBuffer(),
}
// Set a maximum size of the buffer in bytes.
endpoint.buffer.SetLimitSize(maxBufferSize)
m.lock.Lock()
m.endpoints[endpoint] = matchFunc
m.lock.Unlock()
go m.handlePendingPackets(endpoint, matchFunc)
return endpoint
}
// RemoveEndpoint removes an endpoint from the Mux.
func (m *Mux) RemoveEndpoint(e *Endpoint) {
m.lock.Lock()
defer m.lock.Unlock()
delete(m.endpoints, e)
}
// Close closes the Mux and all associated Endpoints.
func (m *Mux) Close() error {
m.lock.Lock()
for e := range m.endpoints {
if err := e.close(); err != nil {
m.lock.Unlock()
return err
}
delete(m.endpoints, e)
}
m.isClosed = true
m.lock.Unlock()
err := m.nextConn.Close()
if err != nil {
return err
}
// Wait for readLoop to end
<-m.closedCh
return nil
}
func (m *Mux) readLoop() {
defer func() {
close(m.closedCh)
}()
buf := make([]byte, m.bufferSize)
for {
n, err := m.nextConn.Read(buf)
switch {
case errors.Is(err, io.EOF), errors.Is(err, ice.ErrClosed):
return
case errors.Is(err, io.ErrShortBuffer), errors.Is(err, packetio.ErrTimeout):
m.log.Errorf("mux: failed to read from packetio.Buffer %s", err.Error())
continue
case err != nil:
m.log.Errorf("mux: ending readLoop packetio.Buffer error %s", err.Error())
return
}
if err = m.dispatch(buf[:n]); err != nil {
if errors.Is(err, io.ErrClosedPipe) {
// if the buffer was closed, that's not an error we care to report
return
}
m.log.Errorf("mux: ending readLoop dispatch error %s", err.Error())
return
}
}
}
func (m *Mux) dispatch(buf []byte) error {
if len(buf) == 0 {
m.log.Warnf("Warning: mux: unable to dispatch zero length packet")
return nil
}
var endpoint *Endpoint
m.lock.Lock()
for e, f := range m.endpoints {
if f(buf) {
endpoint = e
break
}
}
if endpoint == nil {
defer m.lock.Unlock()
if !m.isClosed {
if len(m.pendingPackets) >= maxPendingPackets {
m.log.Warnf(
"Warning: mux: no endpoint for packet starting with %d, not adding to queue size(%d)",
buf[0], //nolint:gosec // G602, false positive?
len(m.pendingPackets),
)
} else {
m.log.Warnf(
"Warning: mux: no endpoint for packet starting with %d, adding to queue size(%d)",
buf[0], //nolint:gosec // G602, false positive?
len(m.pendingPackets),
)
m.pendingPackets = append(m.pendingPackets, append([]byte{}, buf...))
}
}
return nil
}
m.lock.Unlock()
_, err := endpoint.buffer.Write(buf)
// Expected when bytes are received faster than the endpoint can process them (#2152, #2180)
if errors.Is(err, packetio.ErrFull) {
m.log.Infof("mux: endpoint buffer is full, dropping packet")
return nil
}
return err
}
func (m *Mux) handlePendingPackets(endpoint *Endpoint, matchFunc MatchFunc) {
m.lock.Lock()
defer m.lock.Unlock()
pendingPackets := make([][]byte, 0, len(m.pendingPackets))
for _, buf := range m.pendingPackets {
if matchFunc(buf) {
if _, err := endpoint.buffer.Write(buf); err != nil {
m.log.Warnf("Warning: mux: error writing packet to endpoint from pending queue: %s", err)
}
} else {
pendingPackets = append(pendingPackets, buf)
}
}
m.pendingPackets = pendingPackets
}

317
internal/mux/mux_test.go Normal file
View File

@@ -0,0 +1,317 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package mux
import (
"errors"
"io"
"net"
"testing"
"time"
"github.com/pion/logging"
"github.com/pion/transport/v4/packetio"
"github.com/pion/transport/v4/test"
"github.com/stretchr/testify/require"
)
const testPipeBufferSize = 8192
func TestNoEndpoints(t *testing.T) {
// In memory pipe
ca, cb := net.Pipe()
require.NoError(t, cb.Close())
mux := NewMux(Config{
Conn: ca,
BufferSize: testPipeBufferSize,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, mux.dispatch(make([]byte, 1)))
require.NoError(t, mux.Close())
require.NoError(t, ca.Close())
}
func TestEndpointDeadline(t *testing.T) {
tests := []struct {
name string
setDeadline func(*Endpoint, time.Time) error
}{
{
name: "SetReadDeadline",
setDeadline: (*Endpoint).SetReadDeadline,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lim := test.TimeOut(2 * time.Second)
defer lim.Stop()
ca, cb := net.Pipe()
defer func() {
_ = ca.Close()
_ = cb.Close()
}()
mux := NewMux(Config{
Conn: ca,
BufferSize: testPipeBufferSize,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
endpoint := mux.NewEndpoint(MatchAll)
require.NoError(t, tt.setDeadline(endpoint, time.Now().Add(10*time.Millisecond)))
_, err := endpoint.Read(make([]byte, testPipeBufferSize))
require.Error(t, err)
var netErr interface{ Timeout() bool }
require.ErrorAs(t, err, &netErr)
require.True(t, netErr.Timeout())
require.NoError(t, mux.Close())
})
}
}
type writeDeadlineConn struct {
net.Conn
writeDeadline time.Time
}
func (w *writeDeadlineConn) SetWriteDeadline(t time.Time) error {
w.writeDeadline = t
if w.Conn == nil {
return nil
}
return w.Conn.SetWriteDeadline(t)
}
func TestEndpointSetWriteDeadline(t *testing.T) {
lim := test.TimeOut(2 * time.Second)
defer lim.Stop()
ca, cb := net.Pipe()
defer func() {
_ = cb.Close()
}()
rdConn := &writeDeadlineConn{Conn: ca}
mux := NewMux(Config{
Conn: rdConn,
BufferSize: testPipeBufferSize,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
endpoint := mux.NewEndpoint(MatchAll)
deadline := time.Now().Add(10 * time.Millisecond)
require.NoError(t, endpoint.SetWriteDeadline(deadline))
require.WithinDuration(t, deadline, rdConn.writeDeadline, time.Millisecond)
require.NoError(t, mux.Close())
}
type writeDeadlineErrorConn struct {
net.Conn
deadlineErr error
}
func (w *writeDeadlineErrorConn) SetDeadline(t time.Time) error {
if w.deadlineErr != nil {
return w.deadlineErr
}
return nil
}
var errDeadlineTest = errors.New("write deadline failed")
func TestEndpointSetDeadlineWriteDeadlineError(t *testing.T) {
lim := test.TimeOut(2 * time.Second)
defer lim.Stop()
ca, cb := net.Pipe()
defer func() {
_ = ca.Close()
_ = cb.Close()
}()
rdConn := &writeDeadlineErrorConn{Conn: ca, deadlineErr: errDeadlineTest}
mux := NewMux(Config{
Conn: rdConn,
BufferSize: testPipeBufferSize,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
endpoint := mux.NewEndpoint(MatchAll)
err := endpoint.SetDeadline(time.Now().Add(10 * time.Millisecond))
require.Error(t, err)
require.ErrorIs(t, err, errDeadlineTest)
require.NoError(t, mux.Close())
require.NoError(t, ca.Close())
require.NoError(t, rdConn.Close())
}
type muxErrorConnReadResult struct {
err error
data []byte
}
// muxErrorConn.
type muxErrorConn struct {
net.Conn
readResults []muxErrorConnReadResult
}
func (m *muxErrorConn) Read(b []byte) (n int, err error) {
err = m.readResults[0].err
copy(b, m.readResults[0].data)
n = len(m.readResults[0].data)
m.readResults = m.readResults[1:]
return
}
/*
Don't end the mux readLoop for packetio.ErrTimeout or io.ErrShortBuffer, assert the following
- io.ErrShortBuffer and packetio.ErrTimeout don't end the read loop
- io.EOF ends the loop
pion/webrtc#1720
*/
func TestNonFatalRead(t *testing.T) {
// Limit runtime in case of deadlocks
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
expectedData := []byte("expectedData")
// In memory pipe
ca, cb := net.Pipe()
require.NoError(t, cb.Close())
conn := &muxErrorConn{ca, []muxErrorConnReadResult{
// Non-fatal timeout error
{packetio.ErrTimeout, nil},
{nil, expectedData},
{io.ErrShortBuffer, nil},
{nil, expectedData},
{io.EOF, nil},
}}
mux := NewMux(Config{
Conn: conn,
BufferSize: testPipeBufferSize,
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
e := mux.NewEndpoint(MatchAll)
buff := make([]byte, testPipeBufferSize)
n, err := e.Read(buff)
require.NoError(t, err)
require.Equal(t, buff[:n], expectedData)
n, err = e.Read(buff)
require.NoError(t, err)
require.Equal(t, buff[:n], expectedData)
<-mux.closedCh
require.NoError(t, mux.Close())
require.NoError(t, ca.Close())
}
// If a endpoint returns packetio.ErrFull it is a non-fatal error and shouldn't cause
// the mux to be destroyed
// pion/webrtc#2180
// .
func TestNonFatalDispatch(t *testing.T) {
in, out := net.Pipe()
mux := NewMux(Config{
Conn: out,
LoggerFactory: logging.NewDefaultLoggerFactory(),
BufferSize: 1500,
})
e := mux.NewEndpoint(MatchSRTP)
e.buffer.SetLimitSize(1)
for i := 0; i <= 25; i++ {
srtpPacket := []byte{128, 1, 2, 3, 4}
_, err := in.Write(srtpPacket)
require.NoError(t, err)
}
require.NoError(t, mux.Close())
require.NoError(t, in.Close())
require.NoError(t, out.Close())
}
func BenchmarkDispatch(b *testing.B) {
mux := &Mux{
endpoints: make(map[*Endpoint]MatchFunc),
log: logging.NewDefaultLoggerFactory().NewLogger("mux"),
}
endpoint := mux.NewEndpoint(MatchSRTP)
mux.NewEndpoint(MatchSRTCP)
buf := []byte{128, 1, 2, 3, 4}
buf2 := make([]byte, 1200)
b.StartTimer()
for i := 0; i < b.N; i++ {
err := mux.dispatch(buf)
if err != nil {
b.Errorf("dispatch: %v", err)
}
_, err = endpoint.buffer.Read(buf2)
if err != nil {
b.Errorf("read: %v", err)
}
}
}
func TestPendingQueue(t *testing.T) {
factory := logging.NewDefaultLoggerFactory()
factory.DefaultLogLevel = logging.LogLevelDebug
mux := &Mux{
endpoints: make(map[*Endpoint]MatchFunc),
log: factory.NewLogger("mux"),
}
// Assert empty packets don't end up in queue
require.NoError(t, mux.dispatch([]byte{}))
require.Equal(t, len(mux.pendingPackets), 0)
// Test Happy Case
inBuffer := []byte{20, 1, 2, 3, 4}
outBuffer := make([]byte, len(inBuffer))
require.NoError(t, mux.dispatch(inBuffer))
endpoint := mux.NewEndpoint(MatchDTLS)
require.NotNil(t, endpoint)
_, err := endpoint.Read(outBuffer)
require.NoError(t, err)
require.Equal(t, outBuffer, inBuffer)
// Assert limit on pendingPackets
for i := 0; i <= 100; i++ {
require.NoError(t, mux.dispatch([]byte{64, 65, 66}))
}
require.Equal(t, len(mux.pendingPackets), maxPendingPackets)
}

67
internal/mux/muxfunc.go Normal file
View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package mux
// MatchFunc allows custom logic for mapping packets to an Endpoint.
type MatchFunc func([]byte) bool
// MatchAll always returns true.
func MatchAll([]byte) bool {
return true
}
// MatchRange returns true if the first byte of buf is in [lower..upper].
func MatchRange(lower, upper byte, buf []byte) bool {
if len(buf) < 1 {
return false
}
b := buf[0]
return b >= lower && b <= upper
}
// MatchFuncs as described in RFC7983
// https://tools.ietf.org/html/rfc7983
// +----------------+
// | [0..3] -+--> forward to STUN
// | |
// | [16..19] -+--> forward to ZRTP
// | |
// packet --> | [20..63] -+--> forward to DTLS
// | |
// | [64..79] -+--> forward to TURN Channel
// | |
// | [128..191] -+--> forward to RTP/RTCP
// +----------------+
// MatchDTLS is a MatchFunc that accepts packets with the first byte in [20..63]
// as defied in RFC7983.
func MatchDTLS(b []byte) bool {
return MatchRange(20, 63, b)
}
// MatchSRTPOrSRTCP is a MatchFunc that accepts packets with the first byte in [128..191]
// as defied in RFC7983.
func MatchSRTPOrSRTCP(b []byte) bool {
return MatchRange(128, 191, b)
}
func isRTCP(buf []byte) bool {
// Not long enough to determine RTP/RTCP
if len(buf) < 4 {
return false
}
return buf[1] >= 192 && buf[1] <= 223
}
// MatchSRTP is a MatchFunc that only matches SRTP and not SRTCP.
func MatchSRTP(buf []byte) bool {
return MatchSRTPOrSRTCP(buf) && !isRTCP(buf)
}
// MatchSRTCP is a MatchFunc that only matches SRTCP and not SRTP.
func MatchSRTCP(buf []byte) bool {
return MatchSRTPOrSRTCP(buf) && isRTCP(buf)
}