rtmp, rtsp: support PROXY protocol (#5754)

Support PROXY protocol v1/v2 on RTMP, RTMPS, RTSP, and RTSPS TCP
listeners so real client IPs are visible when running behind L4 proxies
(nginx stream, HAProxy, AWS NLB).

---------

Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
This commit is contained in:
Alex McKenzie
2026-06-05 21:37:53 +02:00
committed by GitHub
co-authored by aler9
parent f5d7ed3138
commit b2dc62e13c
27 changed files with 1009 additions and 489 deletions
+8
View File
@@ -449,6 +449,10 @@ components:
type: array
items:
$ref: "#/components/schemas/RTSPAuthMethod"
rtspTrustedProxies:
type: array
items:
type: string
rtspUDPReadBufferSize:
type: integer
format: uint64
@@ -472,6 +476,10 @@ components:
type: string
rtmpServerCert:
type: string
rtmpTrustedProxies:
type: array
items:
type: string
# HLS server
hls:
+1
View File
@@ -37,6 +37,7 @@ require (
github.com/pion/sdp/v3 v3.0.18
github.com/pion/transport/v4 v4.0.2
github.com/pion/webrtc/v4 v4.2.14
github.com/pires/go-proxyproto v0.12.0
github.com/quic-go/quic-go v0.59.1
github.com/quic-go/webtransport-go v0.10.0
github.com/stretchr/testify v1.11.1
+2
View File
@@ -199,6 +199,8 @@ github.com/pion/turn/v5 v5.0.8 h1:pZUCtmwWCMkrRKqh/8pL3WoGADXBe0/lOPkN7oqFjK8=
github.com/pion/turn/v5 v5.0.8/go.mod h1:1VwvxElZaOdJU0liJ/WUSm/Tsh+n2OxS5ISSDxgOWxU=
github.com/pion/webrtc/v4 v4.2.14 h1:Q6zMs+fSDsYuhZcNlvFGBxCOMHVV9oYcDa6O9/HIGTc=
github.com/pion/webrtc/v4 v4.2.14/go.mod h1:87NVKP86+g4OMrRxWhjWfUjeXP4JrV6RTlUrIW+/Jak=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+2
View File
@@ -335,6 +335,7 @@ type Conf struct {
RTSPServerCert string `json:"rtspServerCert"`
AuthMethods *RTSPAuthMethods `json:"authMethods,omitempty" deprecated:"true"`
RTSPAuthMethods RTSPAuthMethods `json:"rtspAuthMethods"`
RTSPTrustedProxies IPNetworks `json:"rtspTrustedProxies"`
RTSPUDPReadBufferSize *uint `json:"rtspUDPReadBufferSize,omitempty" deprecated:"true"`
// RTMP server
@@ -345,6 +346,7 @@ type Conf struct {
RTMPSAddress string `json:"rtmpsAddress"`
RTMPServerKey string `json:"rtmpServerKey"`
RTMPServerCert string `json:"rtmpServerCert"`
RTMPTrustedProxies IPNetworks `json:"rtmpTrustedProxies"`
// HLS server
HLS bool `json:"hls"`
+8
View File
@@ -491,6 +491,7 @@ func (p *Core) createResources(initial bool) error {
ServerCert: "",
ServerKey: "",
RTSPAddress: p.conf.RTSPAddress,
TrustedProxies: p.conf.RTSPTrustedProxies,
Transports: p.conf.RTSPTransports,
RunOnConnect: p.conf.RunOnConnect,
RunOnConnectRestart: p.conf.RunOnConnectRestart,
@@ -534,6 +535,7 @@ func (p *Core) createResources(initial bool) error {
ServerCert: p.conf.RTSPServerCert,
ServerKey: p.conf.RTSPServerKey,
RTSPAddress: p.conf.RTSPAddress,
TrustedProxies: p.conf.RTSPTrustedProxies,
Transports: p.conf.RTSPTransports,
RunOnConnect: p.conf.RunOnConnect,
RunOnConnectRestart: p.conf.RunOnConnectRestart,
@@ -563,6 +565,7 @@ func (p *Core) createResources(initial bool) error {
ServerCert: "",
ServerKey: "",
RTSPAddress: p.conf.RTSPAddress,
TrustedProxies: p.conf.RTMPTrustedProxies,
RunOnConnect: p.conf.RunOnConnect,
RunOnConnectRestart: p.conf.RunOnConnectRestart,
RunOnDisconnect: p.conf.RunOnDisconnect,
@@ -591,6 +594,7 @@ func (p *Core) createResources(initial bool) error {
ServerKey: p.conf.RTMPServerKey,
DumpPackets: p.conf.DumpPackets,
RTSPAddress: p.conf.RTSPAddress,
TrustedProxies: p.conf.RTMPTrustedProxies,
RunOnConnect: p.conf.RunOnConnect,
RunOnConnectRestart: p.conf.RunOnConnectRestart,
RunOnDisconnect: p.conf.RunOnDisconnect,
@@ -876,6 +880,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.MulticastRTPPort != p.conf.MulticastRTPPort ||
newConf.MulticastRTCPPort != p.conf.MulticastRTCPPort ||
!reflect.DeepEqual(newConf.RTSPTransports, p.conf.RTSPTransports) ||
!reflect.DeepEqual(newConf.RTSPTrustedProxies, p.conf.RTSPTrustedProxies) ||
newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
newConf.RunOnDisconnect != p.conf.RunOnDisconnect ||
@@ -898,6 +903,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RTSPServerKey != p.conf.RTSPServerKey ||
newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.RTSPTransports, p.conf.RTSPTransports) ||
!reflect.DeepEqual(newConf.RTSPTrustedProxies, p.conf.RTSPTrustedProxies) ||
newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
newConf.RunOnDisconnect != p.conf.RunOnDisconnect ||
@@ -913,6 +919,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.RTMPTrustedProxies, p.conf.RTMPTrustedProxies) ||
newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
newConf.RunOnDisconnect != p.conf.RunOnDisconnect ||
@@ -930,6 +937,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RTMPServerCert != p.conf.RTMPServerCert ||
newConf.RTMPServerKey != p.conf.RTMPServerKey ||
newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.RTMPTrustedProxies, p.conf.RTMPTrustedProxies) ||
newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
newConf.RunOnDisconnect != p.conf.RunOnDisconnect ||
+4 -4
View File
@@ -8,6 +8,7 @@ import (
"net"
"os"
"sync"
"sync/atomic"
"time"
"github.com/google/gopacket"
@@ -72,7 +73,7 @@ type conn struct {
Conn net.Conn
ServerSide bool
expectingSecrets int
expectingSecrets atomic.Int32
f *os.File
pw *pcapgo.NgWriter
once sync.Once
@@ -157,7 +158,7 @@ func (c *conn) run() {
}
func (c *conn) processEntry(e dumpEntry) {
if c.expectingSecrets > 0 && e.direction != dirSecret {
if c.expectingSecrets.Load() > 0 && e.direction != dirSecret {
c.delayed = append(c.delayed, e)
return
}
@@ -189,8 +190,7 @@ func (c *conn) processEntry(e dumpEntry) {
c.pw.Flush() //nolint:errcheck
writeDecryptionSecretsBlock(c.f, e.data)
c.expectingSecrets--
if c.expectingSecrets == 0 {
if c.expectingSecrets.Add(-1) == 0 {
for _, e2 := range c.delayed {
c.processEntry(e2)
}
+1 -1
View File
@@ -36,7 +36,7 @@ func (t *DialTLSContext) Do(ctx context.Context, network, addr string) (net.Conn
}
pdConn := netConn.(*conn)
pdConn.expectingSecrets = 4
pdConn.expectingSecrets.Store(4)
tlsConfig.KeyLogWriter = &connKeyLogWriter{c: pdConn}
return tls.Client(netConn, tlsConfig), nil
-23
View File
@@ -1,23 +0,0 @@
package packetdumper
import (
"net"
)
// Listen is a wrapper around net.Listen that dumps packets to disk.
type Listen struct {
Prefix string
}
// Do mimics net.Listen.
func (l *Listen) Do(network, address string) (net.Listener, error) {
netListener, err := net.Listen(network, address)
if err != nil {
return nil, err
}
return &listener{
Prefix: l.Prefix,
Listener: netListener,
}, nil
}
-30
View File
@@ -1,30 +0,0 @@
package packetdumper
import (
"net"
)
// ListenPacket is a wrapper around net.ListenPacket that dumps packets to disk.
type ListenPacket struct {
Prefix string
}
// Do mimics net.ListenPacket.
func (l *ListenPacket) Do(network, address string) (net.PacketConn, error) {
netPacketConn, err := net.ListenPacket(network, address)
if err != nil {
return nil, err
}
pdPacketConn := &packetConn{
Prefix: l.Prefix,
PacketConn: netPacketConn,
}
err = pdPacketConn.Initialize()
if err != nil {
netPacketConn.Close()
return nil, err
}
return pdPacketConn, nil
}
+10 -10
View File
@@ -2,17 +2,17 @@ package packetdumper
import "net"
var _ net.Listener = (*listener)(nil)
var _ net.Listener = (*Listener)(nil)
// listener is a wrapper around a net.Listener that dumps packets to disk.
type listener struct {
// Listener is a wrapper around a net.Listener that dumps packets to disk.
type Listener struct {
Wrapped net.Listener
Prefix string
Listener net.Listener
}
// Accept implements net.Listener.
func (l *listener) Accept() (net.Conn, error) {
netConn, err := l.Listener.Accept()
func (l *Listener) Accept() (net.Conn, error) {
netConn, err := l.Wrapped.Accept()
if err != nil {
return nil, err
}
@@ -32,11 +32,11 @@ func (l *listener) Accept() (net.Conn, error) {
}
// Close implements net.Listener.
func (l *listener) Close() error {
return l.Listener.Close()
func (l *Listener) Close() error {
return l.Wrapped.Close()
}
// Addr implements net.Listener.
func (l *listener) Addr() net.Addr {
return l.Listener.Addr()
func (l *Listener) Addr() net.Addr {
return l.Wrapped.Addr()
}
+44
View File
@@ -0,0 +1,44 @@
package packetdumper
import (
"net"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestListener(t *testing.T) {
innerLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
prefix := filepath.Join(t.TempDir(), "capture")
ln := &Listener{
Wrapped: innerLn,
Prefix: prefix,
}
require.Equal(t, innerLn.Addr(), ln.Addr())
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer clientConn.Close()
serverConn, err := ln.Accept()
require.NoError(t, err)
defer serverConn.Close()
_, err = clientConn.Write([]byte("ping"))
require.NoError(t, err)
buf := make([]byte, 4)
_, err = serverConn.Read(buf)
require.NoError(t, err)
require.Equal(t, []byte("ping"), buf)
serverConn.Close()
clientConn.Close()
ln.Close() //nolint:errcheck
checkPcapngPresence(t, prefix)
}
+24 -24
View File
@@ -14,7 +14,7 @@ import (
"github.com/google/uuid"
)
var _ net.PacketConn = (*packetConn)(nil)
var _ net.PacketConn = (*PacketConn)(nil)
type extendedPacketConn interface {
net.PacketConn
@@ -28,10 +28,10 @@ type packetDumpEntry struct {
src, dst *net.UDPAddr
}
// packetConn is a wrapper around net.PacketConn that dumps packets to disk.
type packetConn struct {
// PacketConn is a wrapper around net.PacketConn that dumps packets to disk.
type PacketConn struct {
Prefix string
PacketConn net.PacketConn
Wrapped net.PacketConn
f *os.File
pw *pcapgo.NgWriter
@@ -43,7 +43,7 @@ type packetConn struct {
}
// Initialize initializes packetConn.
func (c *packetConn) Initialize() error {
func (c *PacketConn) Initialize() error {
var err error
c.f, err = os.Create(fmt.Sprintf("%s_%d_%s.pcapng", c.Prefix, time.Now().UnixNano(), uuid.New().String()))
if err != nil {
@@ -66,15 +66,15 @@ func (c *packetConn) Initialize() error {
}
// Close implements net.PacketConn.
func (c *packetConn) Close() error {
func (c *PacketConn) Close() error {
c.once.Do(func() {
close(c.terminated)
})
<-c.done
return c.PacketConn.Close()
return c.Wrapped.Close()
}
func (c *packetConn) run() {
func (c *PacketConn) run() {
defer close(c.done)
defer c.f.Close()
@@ -97,7 +97,7 @@ func (c *packetConn) run() {
}
}
func (c *packetConn) writePacket(ntp time.Time, src, dst *net.UDPAddr, payload []byte) {
func (c *PacketConn) writePacket(ntp time.Time, src, dst *net.UDPAddr, payload []byte) {
eth := &layers.Ethernet{
SrcMAC: net.HardwareAddr{0, 0, 0, 0, 0, 0},
DstMAC: net.HardwareAddr{0, 0, 0, 0, 0, 0},
@@ -130,7 +130,7 @@ func (c *packetConn) writePacket(ntp time.Time, src, dst *net.UDPAddr, payload [
}, raw)
}
func (c *packetConn) enqueue(e packetDumpEntry) {
func (c *PacketConn) enqueue(e packetDumpEntry) {
select {
case c.queue <- e:
case <-c.terminated:
@@ -138,11 +138,11 @@ func (c *packetConn) enqueue(e packetDumpEntry) {
}
// ReadFrom implements net.PacketConn.
func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.PacketConn.ReadFrom(p)
func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.Wrapped.ReadFrom(p)
if n != 0 {
local := c.PacketConn.LocalAddr().(*net.UDPAddr)
local := c.Wrapped.LocalAddr().(*net.UDPAddr)
remote := addr.(*net.UDPAddr)
c.enqueue(packetDumpEntry{
@@ -157,11 +157,11 @@ func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
}
// WriteTo implements net.PacketConn.
func (c *packetConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
n, err = c.PacketConn.WriteTo(p, addr)
func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
n, err = c.Wrapped.WriteTo(p, addr)
if err == nil {
local := c.PacketConn.LocalAddr().(*net.UDPAddr)
local := c.Wrapped.LocalAddr().(*net.UDPAddr)
remote := addr.(*net.UDPAddr)
c.enqueue(packetDumpEntry{
@@ -176,23 +176,23 @@ func (c *packetConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
}
// LocalAddr implements net.PacketConn.
func (c *packetConn) LocalAddr() net.Addr { return c.PacketConn.LocalAddr() }
func (c *PacketConn) LocalAddr() net.Addr { return c.Wrapped.LocalAddr() }
// SetDeadline implements net.PacketConn.
func (c *packetConn) SetDeadline(t time.Time) error { return c.PacketConn.SetDeadline(t) }
func (c *PacketConn) SetDeadline(t time.Time) error { return c.Wrapped.SetDeadline(t) }
// SetReadDeadline implements net.PacketConn.
func (c *packetConn) SetReadDeadline(t time.Time) error { return c.PacketConn.SetReadDeadline(t) }
func (c *PacketConn) SetReadDeadline(t time.Time) error { return c.Wrapped.SetReadDeadline(t) }
// SetWriteDeadline implements net.PacketConn.
func (c *packetConn) SetWriteDeadline(t time.Time) error { return c.PacketConn.SetWriteDeadline(t) }
func (c *PacketConn) SetWriteDeadline(t time.Time) error { return c.Wrapped.SetWriteDeadline(t) }
// SetReadBuffer implements extendedPacketConn.
func (c *packetConn) SetReadBuffer(bytes int) error {
return c.PacketConn.(extendedPacketConn).SetReadBuffer(bytes)
func (c *PacketConn) SetReadBuffer(bytes int) error {
return c.Wrapped.(extendedPacketConn).SetReadBuffer(bytes)
}
// SyscallConn implements extendedPacketConn.
func (c *packetConn) SyscallConn() (syscall.RawConn, error) {
return c.PacketConn.(extendedPacketConn).SyscallConn()
func (c *PacketConn) SyscallConn() (syscall.RawConn, error) {
return c.Wrapped.(extendedPacketConn).SyscallConn()
}
+7 -7
View File
@@ -37,7 +37,7 @@ func TestPacketConnInitialize_CreatesFile(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
c.Close() //nolint:errcheck
@@ -50,7 +50,7 @@ func TestPacketConnWriteTo(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
n, err := c.WriteTo([]byte("hello world"), server.LocalAddr())
@@ -73,7 +73,7 @@ func TestPacketConnReadFrom(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
_, err := server.WriteTo([]byte("incoming data"), client.LocalAddr())
@@ -96,7 +96,7 @@ func TestPacketConnMultipleWriteRead(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
serverAddr := server.LocalAddr()
@@ -140,7 +140,7 @@ func TestPacketConnCloseIdempotent(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
c.Close() //nolint:errcheck
@@ -154,7 +154,7 @@ func TestPacketConnDelegatesAddrMethods(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
require.Equal(t, client.LocalAddr(), c.LocalAddr())
@@ -173,7 +173,7 @@ func TestPacketConnReadFromRecordsSource(t *testing.T) {
client, server := startUDPPair(t)
prefix := filepath.Join(t.TempDir(), "capture")
c := &packetConn{Prefix: prefix, PacketConn: client}
c := &PacketConn{Prefix: prefix, Wrapped: client}
require.NoError(t, c.Initialize())
_, err := server.WriteTo([]byte("ping"), client.LocalAddr())
-24
View File
@@ -1,24 +0,0 @@
package packetdumper
import (
"crypto/tls"
"net"
)
// TLSListen provides a tls.Listen that also dumps TLS master secrets to disk.
type TLSListen struct {
Listen func(network string, address string) (net.Listener, error)
}
// Do mimics tls.Listen.
func (l *TLSListen) Do(network string, laddr string, tlsConfig *tls.Config) (net.Listener, error) {
netListener, err := l.Listen(network, laddr)
if err != nil {
return nil, err
}
return &tlsListener{
Listener: netListener,
TLSConfig: tlsConfig,
}, nil
}
+15 -9
View File
@@ -20,28 +20,34 @@ func (w *connKeyLogWriter) Write(p []byte) (int, error) {
return len(p), nil
}
type tlsListener struct {
Listener net.Listener
var _ net.Listener = (*TLSListener)(nil)
// TLSListener is a wrapper around a net.Listener that dumps TLS master secrets to disk.
type TLSListener struct {
Wrapped net.Listener
TLSConfig *tls.Config
}
func (l *tlsListener) Close() error {
return l.Listener.Close()
// Close implements net.Listener.
func (l *TLSListener) Close() error {
return l.Wrapped.Close()
}
func (l *tlsListener) Addr() net.Addr {
return l.Listener.Addr()
// Addr implements net.Listener.
func (l *TLSListener) Addr() net.Addr {
return l.Wrapped.Addr()
}
func (l *tlsListener) Accept() (net.Conn, error) {
netConn, err := l.Listener.Accept()
// Accept implements net.Listener.
func (l *TLSListener) Accept() (net.Conn, error) {
netConn, err := l.Wrapped.Accept()
if err != nil {
return nil, err
}
tlsConfig := l.TLSConfig.Clone()
pdConn := netConn.(*conn)
pdConn.expectingSecrets = 4
pdConn.expectingSecrets.Store(4)
tlsConfig.KeyLogWriter = &connKeyLogWriter{c: pdConn}
return tls.Server(netConn, tlsConfig), nil
@@ -0,0 +1,64 @@
package packetdumper
import (
"crypto/tls"
"net"
"path/filepath"
"testing"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
)
func TestTLSListener(t *testing.T) {
cert, err := tls.X509KeyPair(test.TLSCertPub, test.TLSCertKey)
require.NoError(t, err)
serverTLSConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
innerLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
prefix := filepath.Join(t.TempDir(), "capture")
pdLn := &Listener{
Wrapped: innerLn,
Prefix: prefix,
}
tlsLn := &TLSListener{
Wrapped: pdLn,
TLSConfig: serverTLSConfig,
}
require.Equal(t, innerLn.Addr(), tlsLn.Addr())
clientDone := make(chan error, 1)
go func() {
clientConn, err2 := tls.Dial("tcp", tlsLn.Addr().String(), &tls.Config{InsecureSkipVerify: true}) //nolint:gosec
if err2 != nil {
clientDone <- err2
return
}
defer clientConn.Close() //nolint:errcheck
_, err2 = clientConn.Write([]byte("ping"))
clientDone <- err2
}()
serverConn, err := tlsLn.Accept()
require.NoError(t, err)
defer serverConn.Close()
buf := make([]byte, 4)
_, err = serverConn.Read(buf)
require.NoError(t, err)
require.Equal(t, []byte("ping"), buf)
require.NoError(t, <-clientDone)
serverConn.Close()
tlsLn.Close() //nolint:errcheck
checkPcapngPresence(t, prefix)
}
+27 -9
View File
@@ -130,20 +130,38 @@ func (s *Server) Initialize() error {
}
}
var listen func(network string, address string) (net.Listener, error)
var tlsListen func(network string, laddr string, config *tls.Config) (net.Listener, error)
listen := func(network string, address string) (net.Listener, error) {
ln, err := net.Listen(network, address)
if err != nil {
return nil, err
}
if s.DumpPackets {
listen = (&packetdumper.Listen{
ln = &packetdumper.Listener{
Wrapped: ln,
Prefix: s.DumpPacketsPrefix,
}).Do
}
}
tlsListen = (&packetdumper.TLSListen{
Listen: listen,
}).Do
return ln, nil
}
tlsListen := func(network string, laddr string, config *tls.Config) (net.Listener, error) {
ln, err := listen(network, laddr)
if err != nil {
return nil, err
}
if s.DumpPackets {
ln = &packetdumper.TLSListener{
Wrapped: ln,
TLSConfig: config,
}
} else {
listen = net.Listen
tlsListen = tls.Listen
ln = tls.NewListener(ln, config)
}
return ln, nil
}
if tlsConfig != nil {
+49
View File
@@ -0,0 +1,49 @@
// Package proxy provides PROXY protocol support for net.Listener.
package proxy
import (
"net"
"github.com/pires/go-proxyproto"
"github.com/bluenviron/mediamtx/internal/conf"
)
var _ net.Listener = (*Listener)(nil)
// Listener is a net.Listener that supports PROXY protocol.
type Listener struct {
Wrapped net.Listener
TrustedProxies conf.IPNetworks
inner *proxyproto.Listener
}
// Initialize initializes the listener.
func (l *Listener) Initialize() {
l.inner = &proxyproto.Listener{
Listener: l.Wrapped,
Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
tcpAddr, ok := upstream.(*net.TCPAddr)
if ok && l.TrustedProxies.Contains(tcpAddr.IP) {
return proxyproto.USE, nil
}
return proxyproto.IGNORE, nil
},
}
}
// Close implements net.Listener.
func (l *Listener) Close() error {
return l.Wrapped.Close()
}
// Accept implements net.Listener.
func (l *Listener) Accept() (net.Conn, error) {
return l.inner.Accept()
}
// Addr implements net.Listener.
func (l *Listener) Addr() net.Addr {
return l.Wrapped.Addr()
}
+135
View File
@@ -0,0 +1,135 @@
package proxy
import (
"fmt"
"net"
"testing"
"github.com/pires/go-proxyproto"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/conf"
)
func trustedProxies(cidrs ...string) conf.IPNetworks {
networks := make(conf.IPNetworks, 0, len(cidrs))
for _, cidr := range cidrs {
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
networks = append(networks, conf.IPNetwork(*ipnet))
}
return networks
}
func TestListenerTrustedWithHeader(t *testing.T) {
for _, version := range []byte{1, 2} {
t.Run(fmt.Sprintf("v%d", version), func(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
wrapped := &Listener{Wrapped: ln, TrustedProxies: trustedProxies("127.0.0.1/32")}
wrapped.Initialize()
done := make(chan struct{})
go func() {
defer close(done)
conn, err2 := wrapped.Accept()
require.NoError(t, err2)
defer conn.Close()
require.Equal(t, "192.168.1.100:1234", conn.RemoteAddr().String())
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer clientConn.Close()
header := &proxyproto.Header{
Version: version,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 1234},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1935},
}
_, err = header.WriteTo(clientConn)
require.NoError(t, err)
<-done
})
}
}
func TestListenerTrustedWithoutHeader(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
wrapped := &Listener{Wrapped: ln, TrustedProxies: trustedProxies("127.0.0.1/32")}
wrapped.Initialize()
done := make(chan struct{})
go func() {
defer close(done)
conn, err2 := wrapped.Accept()
require.NoError(t, err2)
defer conn.Close()
buf := make([]byte, 7)
_, err2 = conn.Read(buf)
require.NoError(t, err2)
require.Equal(t, []byte("testing"), buf)
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer clientConn.Close()
_, err = clientConn.Write([]byte("testing"))
require.NoError(t, err)
<-done
}
func TestListenerUntrustedIgnoresHeader(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
wrapped := &Listener{Wrapped: ln, TrustedProxies: trustedProxies("10.0.0.0/8")}
wrapped.Initialize()
done := make(chan struct{})
go func() {
defer close(done)
conn, err2 := wrapped.Accept()
require.NoError(t, err2)
defer conn.Close()
require.Equal(t, "127.0.0.1", conn.RemoteAddr().(*net.TCPAddr).IP.String())
}()
clientConn, err := net.Dial("tcp", ln.Addr().String())
require.NoError(t, err)
defer clientConn.Close()
header := &proxyproto.Header{
Version: 1,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 1234},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1935},
}
_, err = header.WriteTo(clientConn)
require.NoError(t, err)
<-done
}
+38 -9
View File
@@ -19,6 +19,7 @@ import (
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/packetdumper"
"github.com/bluenviron/mediamtx/internal/protocols/proxy"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
)
@@ -81,6 +82,7 @@ type Server struct {
ServerCert string
ServerKey string
RTSPAddress string
TrustedProxies conf.IPNetworks
RunOnConnect string
RunOnConnectRestart bool
RunOnDisconnect string
@@ -107,8 +109,11 @@ type Server struct {
// Initialize initializes the server.
func (s *Server) Initialize() error {
var listen func(network string, address string) (net.Listener, error)
var tlsListen func(network string, laddr string, config *tls.Config) (net.Listener, error)
listen := func(network, address string) (net.Listener, error) {
ln, err := net.Listen(network, address)
if err != nil {
return nil, err
}
if s.DumpPackets {
var proto string
@@ -118,16 +123,40 @@ func (s *Server) Initialize() error {
proto = "rtmp"
}
listen = (&packetdumper.Listen{
ln = &packetdumper.Listener{
Wrapped: ln,
Prefix: proto + "_server_conn",
}).Do
}
}
tlsListen = (&packetdumper.TLSListen{
Listen: listen,
}).Do
if len(s.TrustedProxies) > 0 {
pl := &proxy.Listener{
Wrapped: ln,
TrustedProxies: s.TrustedProxies,
}
pl.Initialize()
ln = pl
}
return ln, nil
}
tlsListen := func(network string, laddr string, config *tls.Config) (net.Listener, error) {
ln, err := listen(network, laddr)
if err != nil {
return nil, err
}
if s.DumpPackets {
ln = &packetdumper.TLSListener{
Wrapped: ln,
TLSConfig: config,
}
} else {
listen = net.Listen
tlsListen = tls.Listen
ln = tls.NewListener(ln, config)
}
return ln, nil
}
if s.Encryption {
+42 -6
View File
@@ -3,6 +3,7 @@ package rtmp
import (
"context"
"crypto/tls"
"net"
"net/url"
"testing"
"time"
@@ -10,6 +11,8 @@ import (
"github.com/bluenviron/gortmplib"
"github.com/bluenviron/gortmplib/pkg/codecs"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/pires/go-proxyproto"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
@@ -44,7 +47,11 @@ func TestServerPublish(t *testing.T) {
"plain",
"tls",
} {
t.Run(encrypt, func(t *testing.T) {
for _, proxy := range []string{
"no_proxy",
"proxy",
} {
t.Run(encrypt+"_"+proxy, func(t *testing.T) {
var serverCertFpath string
var serverKeyFpath string
@@ -53,6 +60,10 @@ func TestServerPublish(t *testing.T) {
serverKeyFpath = test.CreateTempFile(t, test.TLSCertKey)
}
_, ipnet, err := net.ParseCIDR("127.0.0.1/32")
require.NoError(t, err)
trustedProxies := conf.IPNetworks{conf.IPNetwork(*ipnet)}
var strm *stream.Stream
var reader *stream.Reader
defer func() {
@@ -74,15 +85,15 @@ func TestServerPublish(t *testing.T) {
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
}
err := strm.Initialize()
require.NoError(t, err)
err2 := strm.Initialize()
require.NoError(t, err2)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
err2 = subStream.Initialize()
require.NoError(t, err2)
reader = &stream.Reader{Parent: test.NilLogger}
@@ -127,6 +138,7 @@ func TestServerPublish(t *testing.T) {
ServerCert: serverCertFpath,
ServerKey: serverKeyFpath,
RTSPAddress: "",
TrustedProxies: trustedProxies,
RunOnConnect: "",
RunOnConnectRestart: false,
RunOnDisconnect: "",
@@ -134,7 +146,7 @@ func TestServerPublish(t *testing.T) {
PathManager: pathManager,
Parent: test.NilLogger,
}
err := s.Initialize()
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
@@ -151,10 +163,33 @@ func TestServerPublish(t *testing.T) {
u, err := url.Parse(rawURL)
require.NoError(t, err)
var dialContext func(ctx context.Context, network, address string) (net.Conn, error)
if proxy == "proxy" {
dialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
c, err2 := (&net.Dialer{}).DialContext(ctx, network, address)
if err2 != nil {
return nil, err2
}
header := &proxyproto.Header{
Version: 1,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 1234},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1939},
}
_, err2 = header.WriteTo(c)
if err2 != nil {
return nil, err2
}
return c, nil
}
}
conn := &gortmplib.Client{
URL: u,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
Publish: true,
DialContext: dialContext,
}
err = conn.Initialize(context.Background())
require.NoError(t, err)
@@ -208,6 +243,7 @@ func TestServerPublish(t *testing.T) {
})
}
}
}
func TestServerRead(t *testing.T) {
for _, encrypt := range []string{
+69 -7
View File
@@ -6,6 +6,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"net"
"reflect"
"sort"
"strings"
@@ -24,6 +25,7 @@ import (
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/packetdumper"
"github.com/bluenviron/mediamtx/internal/protocols/proxy"
)
// ErrConnNotFound is returned when a connection is not found.
@@ -105,6 +107,7 @@ type Server struct {
ServerCert string
ServerKey string
RTSPAddress string
TrustedProxies conf.IPNetworks
Transports conf.RTSPTransports
RunOnConnect string
RunOnConnectRestart bool
@@ -166,6 +169,12 @@ func (s *Server) Initialize() error {
s.srv.TLSConfig = &tls.Config{GetCertificate: s.loader.GetCertificate()}
}
s.srv.Listen = func(network, address string) (net.Listener, error) {
ln, err := net.Listen(network, address)
if err != nil {
return nil, err
}
if s.DumpPackets {
var proto string
if s.Encryption {
@@ -174,17 +183,70 @@ func (s *Server) Initialize() error {
proto = "rtsp"
}
s.srv.Listen = (&packetdumper.Listen{
ln = &packetdumper.Listener{
Wrapped: ln,
Prefix: proto + "_server_conn",
}).Do
}
}
s.srv.ListenPacket = (&packetdumper.ListenPacket{
if len(s.TrustedProxies) > 0 {
pl := &proxy.Listener{
Wrapped: ln,
TrustedProxies: s.TrustedProxies,
}
pl.Initialize()
ln = pl
}
return ln, nil
}
s.srv.TLSListen = func(network, laddr string, config *tls.Config) (net.Listener, error) {
ln, err := s.srv.Listen(network, laddr)
if err != nil {
return nil, err
}
if s.DumpPackets {
ln = &packetdumper.TLSListener{
Wrapped: ln,
TLSConfig: config,
}
} else {
ln = tls.NewListener(ln, config)
}
return ln, nil
}
s.srv.ListenPacket = func(network, address string) (net.PacketConn, error) {
pc, err := net.ListenPacket(network, address)
if err != nil {
return nil, err
}
if s.DumpPackets {
var proto string
if s.Encryption {
proto = "rtsps"
} else {
proto = "rtsp"
}
pc2 := &packetdumper.PacketConn{
Wrapped: pc,
Prefix: proto + "_server_packet_conn",
}).Do
}
err = pc2.Initialize()
if err != nil {
pc.Close() //nolint:errcheck
return nil, err
}
s.srv.TLSListen = (&packetdumper.TLSListen{
Listen: s.srv.Listen,
}).Do
pc = pc2
}
return pc, nil
}
err := s.srv.Start()
+71 -9
View File
@@ -3,7 +3,10 @@ package rtsp
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"sync/atomic"
"testing"
"time"
@@ -15,6 +18,8 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/format"
mpegts "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
tscodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs"
"github.com/pires/go-proxyproto"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
@@ -49,7 +54,21 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
func TestServerPublish(t *testing.T) {
for _, ca := range []string{"basic", "digest", "basic+digest"} {
t.Run(ca, func(t *testing.T) {
for _, encrypt := range []string{"plain", "tls"} {
for _, proxy := range []string{"no_proxy", "proxy"} {
t.Run(ca+"_"+encrypt+"_"+proxy, func(t *testing.T) {
var serverCertFpath string
var serverKeyFpath string
if encrypt == "tls" {
serverCertFpath = test.CreateTempFile(t, test.TLSCertPub)
serverKeyFpath = test.CreateTempFile(t, test.TLSCertKey)
}
_, ipnet, err := net.ParseCIDR("127.0.0.1/32")
require.NoError(t, err)
trustedProxies := conf.IPNetworks{conf.IPNetwork(*ipnet)}
var strm *stream.Stream
var reader *stream.Reader
defer func() {
@@ -96,15 +115,15 @@ func TestServerPublish(t *testing.T) {
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
}
err := strm.Initialize()
require.NoError(t, err)
err2 := strm.Initialize()
require.NoError(t, err2)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
err2 = subStream.Initialize()
require.NoError(t, err2)
reader = &stream.Reader{Parent: test.NilLogger}
@@ -144,19 +163,55 @@ func TestServerPublish(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
WriteQueueSize: 512,
Transports: conf.RTSPTransports{gortsplib.ProtocolTCP: {}},
Encryption: encrypt == "tls",
ServerCert: serverCertFpath,
ServerKey: serverKeyFpath,
TrustedProxies: trustedProxies,
PathManager: pathManager,
Parent: test.NilLogger,
}
err := s.Initialize()
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
source := gortsplib.Client{}
var scheme string
if encrypt == "tls" {
scheme = "rtsps"
} else {
scheme = "rtsp"
}
var dialContext func(ctx context.Context, network, address string) (net.Conn, error)
if proxy == "proxy" {
dialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
c, err2 := (&net.Dialer{}).DialContext(ctx, network, address)
if err2 != nil {
return nil, err2
}
header := &proxyproto.Header{
Version: 1,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 1234},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8557},
}
_, err2 = header.WriteTo(c)
if err2 != nil {
return nil, err2
}
return c, nil
}
}
source := gortsplib.Client{
TLSConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: dialContext,
}
media0 := test.UniqueMediaH264()
err = source.StartRecording(
"rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value",
scheme+"://myuser:mypass@127.0.0.1:8557/teststream?param=value",
&description.Session{Medias: []*description.Media{media0}})
require.NoError(t, err)
defer source.Close()
@@ -197,13 +252,20 @@ func TestServerPublish(t *testing.T) {
Conns: list.Items[0].Conns,
RTPPacketsReceived: list.Items[0].RTPPacketsReceived,
Transport: new("TCP"),
Profile: new("AVP"),
Profile: func() *string {
if encrypt == "tls" {
return new("SAVP")
}
return new("AVP")
}(),
},
},
}, list)
})
}
}
}
}
func TestServerPublishMPEGTS(t *testing.T) {
var strm *stream.Stream
+30 -4
View File
@@ -58,9 +58,17 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
}
if s.DumpPackets {
l.Listen = (&packetdumper.Listen{
l.Listen = func(network, address string) (net.Listener, error) {
ln, err2 := net.Listen(network, address)
if err2 != nil {
return nil, err2
}
return &packetdumper.Listener{
Wrapped: ln,
Prefix: "mpegts_source_unix_conn",
}).Do
}, nil
}
}
err = l.Initialize()
@@ -76,6 +84,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
}
params := udp.URLToParams(u)
l := &udp.Listener{
Address: params.Address,
Source: params.Source,
@@ -83,10 +92,27 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
UDPReadBufferSize: int(udpReadBufferSize),
}
l.ListenPacket = func(network, address string) (net.PacketConn, error) {
pc, err2 := net.ListenPacket(network, address)
if err2 != nil {
return nil, err2
}
if s.DumpPackets {
l.ListenPacket = (&packetdumper.ListenPacket{
pc2 := &packetdumper.PacketConn{
Wrapped: pc,
Prefix: "mpegts_source_packet_conn",
}).Do
}
err2 = pc2.Initialize()
if err2 != nil {
pc.Close() //nolint:errcheck
return nil, err2
}
pc = pc2
}
return pc, nil
}
err = l.Initialize()
+19 -2
View File
@@ -92,10 +92,27 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
UDPReadBufferSize: int(udpReadBufferSize),
}
l.ListenPacket = func(network, address string) (net.PacketConn, error) {
pc, err2 := net.ListenPacket(network, address)
if err2 != nil {
return nil, err2
}
if s.DumpPackets {
l.ListenPacket = (&packetdumper.ListenPacket{
pc2 := &packetdumper.PacketConn{
Wrapped: pc,
Prefix: "rtp_source_packet_conn",
}).Do
}
err2 = pc2.Initialize()
if err2 != nil {
pc.Close() //nolint:errcheck
return nil, err2
}
pc = pc2
}
return pc, nil
}
err = l.Initialize()
+24 -4
View File
@@ -3,6 +3,7 @@ package rtsp
import (
"fmt"
"net"
"net/url"
"time"
@@ -187,10 +188,6 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
Prefix: u.Scheme + "_source_conn",
}).Do
c.ListenPacket = (&packetdumper.ListenPacket{
Prefix: u.Scheme + "_source_packet_conn",
}).Do
c.DialTLSContext = (&packetdumper.DialTLSContext{
DialContext: c.DialContext,
TLSConfig: tlsConfig,
@@ -199,6 +196,29 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
c.TLSConfig = tlsConfig
}
c.ListenPacket = func(network, address string) (net.PacketConn, error) {
pc, err2 := net.ListenPacket(network, address)
if err2 != nil {
return nil, err2
}
if s.DumpPackets {
pc2 := &packetdumper.PacketConn{
Wrapped: pc,
Prefix: u.Scheme + "_source_packet_conn",
}
err2 = pc2.Initialize()
if err2 != nil {
pc.Close() //nolint:errcheck
return nil, err2
}
pc = pc2
}
return pc, nil
}
err = c.Start()
if err != nil {
return err
+8
View File
@@ -281,6 +281,10 @@ rtspServerCert: server.crt
# Authentication methods. Available are "basic" and "digest".
# "digest" doesn't provide any additional security and is available for compatibility only.
rtspAuthMethods: [basic]
# IPs or CIDRs of proxies placed before the RTSP server.
# If the server receives a request from one of these entries, IP in logs
# and authentication will be taken from the PROXY protocol header.
rtspTrustedProxies: []
###############################################
# Global settings -> RTMP server
@@ -301,6 +305,10 @@ rtmpsAddress: :1936
rtmpServerKey: server.key
# Path to the server certificate. This is needed only when encryption is "strict" or "optional".
rtmpServerCert: server.crt
# IPs or CIDRs of proxies placed before the RTMP server.
# If the server receives a request from one of these entries, IP in logs
# and authentication will be taken from the PROXY protocol header.
rtmpTrustedProxies: []
###############################################
# Global settings -> HLS server