mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-07-03 14:05:39 +02:00
feat: apply socket protection to Pion
This commit is contained in:
@@ -9,9 +9,11 @@ require (
|
||||
github.com/livekit/protocol v1.46.0
|
||||
github.com/livekit/server-sdk-go/v2 v2.16.4-0.20260522175902-00c9771fae5a
|
||||
github.com/magefile/mage v1.17.2
|
||||
github.com/pion/ice/v4 v4.2.5
|
||||
github.com/pion/interceptor v0.1.45
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/rtp v1.10.2
|
||||
github.com/pion/transport/v4 v4.0.1
|
||||
github.com/pion/webrtc/v4 v4.2.13
|
||||
github.com/xtaci/kcp-go/v5 v5.6.72
|
||||
github.com/xtaci/smux v1.5.57
|
||||
@@ -57,7 +59,6 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/ice/v4 v4.2.5 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
@@ -65,7 +66,6 @@ require (
|
||||
github.com/pion/sdp/v3 v3.0.18 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.2 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pion/turn/v5 v5.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
|
||||
@@ -32,6 +32,8 @@ import (
|
||||
|
||||
"github.com/openlibrecommunity/olcrtc/internal/engine"
|
||||
"github.com/openlibrecommunity/olcrtc/internal/logger"
|
||||
"github.com/openlibrecommunity/olcrtc/internal/protect"
|
||||
"github.com/pion/ice/v4"
|
||||
pioninterceptor "github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
@@ -68,7 +70,7 @@ const (
|
||||
// A half-open TCP connection lets Send() succeed while replies never
|
||||
// arrive; waiting for the IQ result detects this and triggers reconnect.
|
||||
xmppKeepaliveTimeout = 15 * time.Second
|
||||
reconnectJoinTimeout = 30 * time.Second
|
||||
reconnectJoinTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// bridgeMagic tags every EndpointMessage produced by this engine. JVB broadcasts
|
||||
@@ -480,6 +482,17 @@ func (s *Session) negotiatePC(ctx context.Context, jSess *j.Session, sctpBridge
|
||||
settings := webrtc.SettingEngine{}
|
||||
settings.LoggerFactory = logger.NewPionLoggerFactory()
|
||||
|
||||
// When a socket protector is set, route Pion sockets through ProtectedNet.
|
||||
// Do not fall back to the default network path if setup fails.
|
||||
if protect.Protector != nil {
|
||||
pnet, perr := protect.NewProtectedNet()
|
||||
if perr != nil {
|
||||
return fmt.Errorf("protected net: %w", perr)
|
||||
}
|
||||
settings.SetNet(pnet)
|
||||
settings.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
}
|
||||
|
||||
// pion auto-registers a default interceptor chain (sender reports,
|
||||
// receiver reports, NACK, etc.) when none is supplied. Several of
|
||||
// those probe the DTLS transport on a tick - until DTLS comes up
|
||||
@@ -758,7 +771,6 @@ func randomTrackSuffix() string {
|
||||
return base64.RawURLEncoding.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
|
||||
// updates its endpoint lastActivity timestamp. Without this, JVB expires the
|
||||
// endpoint after its inactivity timeout (~30-60s) when the ICE/DTLS path is
|
||||
// routed through a TURN relay whose allocation silently dies.
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// SPDX-License-Identifier: WTFPL
|
||||
//
|
||||
// ProtectedNet wraps Pion's network adapter. It applies Protector to each
|
||||
// socket fd and hides tunnel-style interfaces from candidate gathering. Callers
|
||||
// install it only when Protector is set, so default builds keep Pion's standard
|
||||
// network stack.
|
||||
package protect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/pion/transport/v4"
|
||||
"github.com/pion/transport/v4/stdnet"
|
||||
)
|
||||
|
||||
// tunInterfacePrefixes lists interface name prefixes excluded from candidate
|
||||
// gathering. Keep pptp explicit; it does not match the ppp prefix.
|
||||
//
|
||||
//nolint:gochecknoglobals // fixed lookup table; a slice cannot be const
|
||||
var tunInterfacePrefixes = []string{"tun", "ppp", "pptp"}
|
||||
|
||||
// ErrUnexpectedConnType is returned when a protected listen/dial yields an
|
||||
// unexpected concrete type. The caller closes that connection instead of using
|
||||
// an unprotected fallback.
|
||||
//
|
||||
//nolint:gochecknoglobals // sentinel error
|
||||
var ErrUnexpectedConnType = fmt.Errorf("protect: unexpected connection type")
|
||||
|
||||
// ProtectedNet wraps Pion's standard net.
|
||||
type ProtectedNet struct {
|
||||
*stdnet.Net
|
||||
}
|
||||
|
||||
// NewProtectedNet builds a ProtectedNet over Pion's standard net.
|
||||
func NewProtectedNet() (*ProtectedNet, error) {
|
||||
base, err := stdnet.NewNet()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdnet: %w", err)
|
||||
}
|
||||
return &ProtectedNet{Net: base}, nil
|
||||
}
|
||||
|
||||
// Interfaces returns system interfaces after filtering tunnel-style devices.
|
||||
func (n *ProtectedNet) Interfaces() ([]*transport.Interface, error) {
|
||||
all, err := n.Net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list interfaces: %w", err)
|
||||
}
|
||||
out := make([]*transport.Interface, 0, len(all))
|
||||
for _, ifc := range all {
|
||||
if !isTunInterface(ifc.Name) {
|
||||
out = append(out, ifc)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// InterfaceByName applies the same filtering as Interfaces.
|
||||
func (n *ProtectedNet) InterfaceByName(name string) (*transport.Interface, error) {
|
||||
if isTunInterface(name) {
|
||||
return nil, transport.ErrInterfaceNotFound
|
||||
}
|
||||
ifc, err := n.Net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lookup interface %q: %w", name, err)
|
||||
}
|
||||
return ifc, nil
|
||||
}
|
||||
|
||||
func isTunInterface(name string) bool {
|
||||
for _, p := range tunInterfacePrefixes {
|
||||
if strings.HasPrefix(name, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) ListenPacket(network, address string) (net.PacketConn, error) {
|
||||
lc := net.ListenConfig{Control: controlFunc}
|
||||
conn, err := lc.ListenPacket(context.Background(), network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen packet %s %q: %w", network, address, err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
|
||||
lc := net.ListenConfig{Control: controlFunc}
|
||||
address := udpAddrString(locAddr)
|
||||
pc, err := lc.ListenPacket(context.Background(), network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen udp %s %q: %w", network, address, err)
|
||||
}
|
||||
uc, ok := pc.(*net.UDPConn)
|
||||
if !ok {
|
||||
_ = pc.Close()
|
||||
return nil, ErrUnexpectedConnType
|
||||
}
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) Dial(network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{Control: controlFunc}
|
||||
conn, err := d.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial %s %q: %w", network, address, err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
|
||||
d := net.Dialer{Control: controlFunc}
|
||||
if laddr != nil {
|
||||
d.LocalAddr = laddr
|
||||
}
|
||||
address := udpAddrString(raddr)
|
||||
conn, err := d.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial udp %s %q: %w", network, address, err)
|
||||
}
|
||||
uc, ok := conn.(*net.UDPConn)
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
return nil, ErrUnexpectedConnType
|
||||
}
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) {
|
||||
d := net.Dialer{Control: controlFunc}
|
||||
if laddr != nil {
|
||||
d.LocalAddr = laddr
|
||||
}
|
||||
address := tcpAddrString(raddr)
|
||||
conn, err := d.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial tcp %s %q: %w", network, address, err)
|
||||
}
|
||||
tc, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
return nil, ErrUnexpectedConnType
|
||||
}
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (n *ProtectedNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) {
|
||||
lc := net.ListenConfig{Control: controlFunc}
|
||||
address := tcpAddrString(laddr)
|
||||
l, err := lc.Listen(context.Background(), network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen tcp %s %q: %w", network, address, err)
|
||||
}
|
||||
tl, ok := l.(*net.TCPListener)
|
||||
if !ok {
|
||||
_ = l.Close()
|
||||
return nil, ErrUnexpectedConnType
|
||||
}
|
||||
return protectedTCPListener{tl}, nil
|
||||
}
|
||||
|
||||
// CreateDialer returns a dialer that protects each fd. It copies d and chains
|
||||
// any existing Control hook.
|
||||
func (n *ProtectedNet) CreateDialer(d *net.Dialer) transport.Dialer {
|
||||
var dialer net.Dialer
|
||||
if d != nil {
|
||||
dialer = *d
|
||||
}
|
||||
if dialer.ControlContext != nil {
|
||||
dialer.ControlContext = chainControlContext(dialer.ControlContext)
|
||||
} else {
|
||||
dialer.Control = chainControl(dialer.Control)
|
||||
}
|
||||
return n.Net.CreateDialer(&dialer)
|
||||
}
|
||||
|
||||
// CreateListenConfig returns a listen config that protects each fd. It copies
|
||||
// lc and chains any existing Control hook.
|
||||
func (n *ProtectedNet) CreateListenConfig(lc *net.ListenConfig) transport.ListenConfig {
|
||||
var cfg net.ListenConfig
|
||||
if lc != nil {
|
||||
cfg = *lc
|
||||
}
|
||||
// net.ListenConfig exposes Control only; net.Dialer is the type with ControlContext.
|
||||
cfg.Control = chainControl(cfg.Control)
|
||||
return n.Net.CreateListenConfig(&cfg)
|
||||
}
|
||||
|
||||
// chainControl runs the protector first, then any existing Control hook.
|
||||
func chainControl(
|
||||
next func(network, address string, c syscall.RawConn) error,
|
||||
) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
if err := controlFunc(network, address, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if next != nil {
|
||||
return next(network, address, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// chainControlContext runs the protector first, then any existing ControlContext hook.
|
||||
func chainControlContext(
|
||||
next func(context.Context, string, string, syscall.RawConn) error,
|
||||
) func(context.Context, string, string, syscall.RawConn) error {
|
||||
return func(ctx context.Context, network, address string, c syscall.RawConn) error {
|
||||
if err := controlFunc(network, address, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if next != nil {
|
||||
return next(ctx, network, address, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type protectedTCPListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (l protectedTCPListener) AcceptTCP() (transport.TCPConn, error) {
|
||||
return l.TCPListener.AcceptTCP()
|
||||
}
|
||||
|
||||
func udpAddrString(a *net.UDPAddr) string {
|
||||
if a == nil {
|
||||
return ":0"
|
||||
}
|
||||
return a.String()
|
||||
}
|
||||
|
||||
func tcpAddrString(a *net.TCPAddr) string {
|
||||
if a == nil {
|
||||
return ":0"
|
||||
}
|
||||
return a.String()
|
||||
}
|
||||
|
||||
// Compile-time assertion that ProtectedNet satisfies Pion's Net.
|
||||
var _ transport.Net = (*ProtectedNet)(nil)
|
||||
@@ -0,0 +1,220 @@
|
||||
// SPDX-License-Identifier: WTFPL
|
||||
|
||||
package protect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/transport/v4"
|
||||
)
|
||||
|
||||
func TestIsTunInterface(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]bool{
|
||||
"tun0": true,
|
||||
"tun": true,
|
||||
"ppp0": true,
|
||||
"pptp0": true,
|
||||
"wlan0": false,
|
||||
"eth0": false,
|
||||
"rmnet0": false,
|
||||
"lo": false,
|
||||
}
|
||||
for name, want := range cases {
|
||||
if got := isTunInterface(name); got != want {
|
||||
t.Errorf("isTunInterface(%q) = %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfacesHidesTun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
n, err := NewProtectedNet()
|
||||
if err != nil {
|
||||
t.Fatalf("NewProtectedNet: %v", err)
|
||||
}
|
||||
ifaces, err := n.Interfaces()
|
||||
if err != nil {
|
||||
t.Fatalf("Interfaces: %v", err)
|
||||
}
|
||||
for _, ifc := range ifaces {
|
||||
if isTunInterface(ifc.Name) {
|
||||
t.Errorf("Interfaces returned tun device %q", ifc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceByNameRejectsTun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
n, err := NewProtectedNet()
|
||||
if err != nil {
|
||||
t.Fatalf("NewProtectedNet: %v", err)
|
||||
}
|
||||
if _, err := n.InterfaceByName("tun0"); !errors.Is(err, transport.ErrInterfaceNotFound) {
|
||||
t.Errorf("InterfaceByName(tun0) error = %v, want %v", err, transport.ErrInterfaceNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// TestControlFuncFailClosed verifies that Protector can reject a socket.
|
||||
//
|
||||
//nolint:tparallel // mutates package-level Protector
|
||||
func TestControlFuncFailClosed(t *testing.T) {
|
||||
old := Protector
|
||||
t.Cleanup(func() { Protector = old })
|
||||
|
||||
Protector = func(int) bool { return false }
|
||||
lc := net.ListenConfig{Control: controlFunc}
|
||||
pc, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0")
|
||||
if err == nil {
|
||||
_ = pc.Close()
|
||||
t.Fatal("expected protected ListenPacket to fail when Protector rejects fd")
|
||||
}
|
||||
}
|
||||
|
||||
// TestControlFuncProtects verifies that Protector receives a real fd.
|
||||
//
|
||||
//nolint:tparallel // mutates package-level Protector
|
||||
func TestControlFuncProtects(t *testing.T) {
|
||||
old := Protector
|
||||
t.Cleanup(func() { Protector = old })
|
||||
|
||||
var calls int
|
||||
Protector = func(fd int) bool {
|
||||
if fd < 0 {
|
||||
t.Errorf("protector got negative fd %d", fd)
|
||||
}
|
||||
calls++
|
||||
return true
|
||||
}
|
||||
lc := net.ListenConfig{Control: controlFunc}
|
||||
pc, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("ListenPacket: %v", err)
|
||||
}
|
||||
defer pc.Close()
|
||||
if calls == 0 {
|
||||
t.Error("protector was not invoked")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateDialerProtectsAndChains verifies that CreateDialer copies the
|
||||
// caller's Dialer and keeps the caller's Control hook.
|
||||
//
|
||||
//nolint:tparallel // mutates package-level Protector
|
||||
func TestCreateDialerProtectsAndChains(t *testing.T) {
|
||||
old := Protector
|
||||
t.Cleanup(func() { Protector = old })
|
||||
|
||||
var protectorRan bool
|
||||
Protector = func(int) bool { protectorRan = true; return true }
|
||||
|
||||
n, err := NewProtectedNet()
|
||||
if err != nil {
|
||||
t.Fatalf("NewProtectedNet: %v", err)
|
||||
}
|
||||
|
||||
// Dial a local TCP listener.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
go func() {
|
||||
if c, aerr := ln.Accept(); aerr == nil {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var callerControlRan bool
|
||||
caller := &net.Dialer{
|
||||
Control: func(_, _ string, _ syscall.RawConn) error {
|
||||
callerControlRan = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
callerControl := caller.Control
|
||||
|
||||
dialer := n.CreateDialer(caller)
|
||||
|
||||
// Keep the caller's Control unchanged.
|
||||
if reflect.ValueOf(caller.Control).Pointer() != reflect.ValueOf(callerControl).Pointer() {
|
||||
t.Error("CreateDialer mutated the caller's Dialer.Control")
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("dial via CreateDialer: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
if !protectorRan {
|
||||
t.Error("protector hook did not run for the CreateDialer dialer")
|
||||
}
|
||||
if !callerControlRan {
|
||||
t.Error("caller's Control hook did not run (chain dropped it)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateDialerProtectsAndChainsControlContext verifies that CreateDialer
|
||||
// keeps the caller's ControlContext hook.
|
||||
//
|
||||
//nolint:tparallel // mutates package-level Protector
|
||||
func TestCreateDialerProtectsAndChainsControlContext(t *testing.T) {
|
||||
old := Protector
|
||||
t.Cleanup(func() { Protector = old })
|
||||
|
||||
var protectorRan bool
|
||||
Protector = func(int) bool { protectorRan = true; return true }
|
||||
|
||||
n, err := NewProtectedNet()
|
||||
if err != nil {
|
||||
t.Fatalf("NewProtectedNet: %v", err)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
go func() {
|
||||
if c, aerr := ln.Accept(); aerr == nil {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var callerControlContextRan bool
|
||||
caller := &net.Dialer{
|
||||
ControlContext: func(_ context.Context, _, _ string, _ syscall.RawConn) error {
|
||||
callerControlContextRan = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
callerControlContext := caller.ControlContext
|
||||
|
||||
dialer := n.CreateDialer(caller)
|
||||
|
||||
if reflect.ValueOf(caller.ControlContext).Pointer() != reflect.ValueOf(callerControlContext).Pointer() {
|
||||
t.Error("CreateDialer mutated the caller's Dialer.ControlContext")
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("dial via CreateDialer: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
if !protectorRan {
|
||||
t.Error("protector hook did not run for the CreateDialer dialer")
|
||||
}
|
||||
if !callerControlContextRan {
|
||||
t.Error("caller's ControlContext hook did not run (chain dropped it)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user