feat: apply socket protection to Pion

This commit is contained in:
hawkff
2026-06-28 19:58:43 -04:00
committed by hawkff
parent e75ce60bb3
commit a68f2b668f
4 changed files with 483 additions and 4 deletions
+2 -2
View File
@@ -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
+14 -2
View File
@@ -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.
+247
View File
@@ -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)
+220
View File
@@ -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)")
}
}