From c475f84e5db1c3b15e2192203569b4e4d49f3d40 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 5 Jul 2025 13:46:59 +0200 Subject: [PATCH] rtsp: support encrypting UDP and UDP-multicast streams (#4690) --- README.md | 18 ++++++--- apidocs/openapi.yaml | 8 ++++ go.mod | 4 +- go.sum | 8 ++-- internal/conf/conf.go | 54 +++++++++++++-------------- internal/conf/conf_test.go | 12 ------ internal/core/core.go | 17 +++++---- internal/core/path_test.go | 54 ++++++++++++++++++--------- internal/servers/rtsp/server_test.go | 18 ++++++--- internal/staticsources/rtsp/source.go | 14 ++++--- mediamtx.yml | 14 +++++-- 11 files changed, 131 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index cd40c224..90db238f 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,14 @@ The RTSP protocol supports multiple underlying transport protocols, each with it ```sh gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \ -d.video_0 ! rtspclientsink protocols=tcp name=s location=rtsp://localhost:8554/mystream +d.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream protocols=tcp +``` + +If encryption is enabled, the `tls-validation-flags` and `profiles` options must be specified too: + +```sh +gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \ +d.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream tls-validation-flags=0 profiles=GST_RTSP_PROFILE_SAVP ``` The resulting stream is available in path `/mystream`. @@ -2407,9 +2414,9 @@ ffmpeg -i rtsp://original-source \ The RTSP protocol supports different underlying transport protocols, that are chosen by clients during the handshake with the server: -* UDP: the most performant, but doesn't work when there's a NAT/firewall between server and clients. It doesn't support encryption. -* UDP-multicast: allows to save bandwidth when clients are all in the same LAN, by sending packets once to a fixed multicast IP. It doesn't support encryption. -* TCP: the most versatile, does support encryption. +* UDP: the most performant, but doesn't work when there's a NAT/firewall between server and clients. +* UDP-multicast: allows to save bandwidth when clients are all in the same LAN, by sending packets once to a fixed multicast IP. +* TCP: the most versatile. The default transport protocol is UDP. To change the transport protocol, you have to tune the configuration of your client of choice. @@ -2422,10 +2429,9 @@ openssl genrsa -out server.key 2048 openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 ``` -Edit `mediamtx.yml` and set the `rtspTransports`, `encryption`, `serverKey` and serverCert parameters: +Edit `mediamtx.yml` and set the `encryption`, `serverKey` and serverCert parameters: ```yml -rtspTransports: [tcp] rtspEncryption: optional rtspServerKey: server.key rtspServerCert: server.crt diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 761953c9..bb98a299 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -195,6 +195,14 @@ components: type: integer multicastRTCPPort: type: integer + srtpAddress: + type: string + srtcpAddress: + type: string + multicastSRTPPort: + type: integer + multicastSRTCPPort: + type: integer rtspServerKey: type: string rtspServerCert: diff --git a/go.mod b/go.mod index f95ef3b8..5a0d33a0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/alecthomas/kong v1.12.0 github.com/asticode/go-astits v1.13.0 github.com/bluenviron/gohlslib/v2 v2.2.0 - github.com/bluenviron/gortsplib/v4 v4.14.1 + github.com/bluenviron/gortsplib/v4 v4.14.2-0.20250705110245-9c1011567a97 github.com/bluenviron/mediacommon/v2 v2.2.0 github.com/datarhei/gosrt v0.9.0 github.com/fsnotify/fsnotify v1.9.0 @@ -73,7 +73,7 @@ require ( github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/sctp v1.8.36 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index c5283b3a..29b2fe79 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib/v2 v2.2.0 h1:eIsCai3IHP0F538h2tCPCRkhQ7XSOaxeceMyPns0o1k= github.com/bluenviron/gohlslib/v2 v2.2.0/go.mod h1:sLyKB5iM6Su1kucNHuDUU9aeN/Hw4WxsV2y9k2IHMGs= -github.com/bluenviron/gortsplib/v4 v4.14.1 h1:v99NmXeeJFfbrO+ipPzPxYGibQaR5ZOUESOA9UQZhsI= -github.com/bluenviron/gortsplib/v4 v4.14.1/go.mod h1:3LaEcg0d47+kfXju5KSlsSxCiZ3IKBI/sqIrBPcsS64= +github.com/bluenviron/gortsplib/v4 v4.14.2-0.20250705110245-9c1011567a97 h1:V8m1pyQOYVEJK5RBy1SLg/Y+hgXYFFiMZOd7NhWWLAE= +github.com/bluenviron/gortsplib/v4 v4.14.2-0.20250705110245-9c1011567a97/go.mod h1:rur2QGh1wRU6KINZn8LwU8qTPFt1XafJGtsfs0KYzRo= github.com/bluenviron/mediacommon/v2 v2.2.0 h1:fGXEX0OEvv5VhGHOv3Q2ABzOtSkIpl9UbwOHrnKWNTk= github.com/bluenviron/mediacommon/v2 v2.2.0/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= @@ -173,8 +173,8 @@ github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0= github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 85a10fc0..2c458954 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -220,25 +220,29 @@ type Conf struct { PlaybackTrustedProxies IPNetworks `json:"playbackTrustedProxies"` // RTSP server - RTSP bool `json:"rtsp"` - RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated - Protocols *RTSPTransports `json:"protocols,omitempty"` // deprecated - RTSPTransports RTSPTransports `json:"rtspTransports"` - Encryption *Encryption `json:"encryption,omitempty"` // deprecated - RTSPEncryption Encryption `json:"rtspEncryption"` - RTSPAddress string `json:"rtspAddress"` - RTSPSAddress string `json:"rtspsAddress"` - RTPAddress string `json:"rtpAddress"` - RTCPAddress string `json:"rtcpAddress"` - MulticastIPRange string `json:"multicastIPRange"` - MulticastRTPPort int `json:"multicastRTPPort"` - MulticastRTCPPort int `json:"multicastRTCPPort"` - ServerKey *string `json:"serverKey,omitempty"` - ServerCert *string `json:"serverCert,omitempty"` - RTSPServerKey string `json:"rtspServerKey"` - RTSPServerCert string `json:"rtspServerCert"` - AuthMethods *RTSPAuthMethods `json:"authMethods,omitempty"` // deprecated - RTSPAuthMethods RTSPAuthMethods `json:"rtspAuthMethods"` + RTSP bool `json:"rtsp"` + RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated + Protocols *RTSPTransports `json:"protocols,omitempty"` // deprecated + RTSPTransports RTSPTransports `json:"rtspTransports"` + Encryption *Encryption `json:"encryption,omitempty"` // deprecated + RTSPEncryption Encryption `json:"rtspEncryption"` + RTSPAddress string `json:"rtspAddress"` + RTSPSAddress string `json:"rtspsAddress"` + RTPAddress string `json:"rtpAddress"` + RTCPAddress string `json:"rtcpAddress"` + MulticastIPRange string `json:"multicastIPRange"` + MulticastRTPPort int `json:"multicastRTPPort"` + MulticastRTCPPort int `json:"multicastRTCPPort"` + SRTPAddress string `json:"srtpAddress"` + SRTCPAddress string `json:"srtcpAddress"` + MulticastSRTPPort int `json:"multicastSRTPPort"` + MulticastSRTCPPort int `json:"multicastSRTCPPort"` + ServerKey *string `json:"serverKey,omitempty"` + ServerCert *string `json:"serverCert,omitempty"` + RTSPServerKey string `json:"rtspServerKey"` + RTSPServerCert string `json:"rtspServerCert"` + AuthMethods *RTSPAuthMethods `json:"authMethods,omitempty"` // deprecated + RTSPAuthMethods RTSPAuthMethods `json:"rtspAuthMethods"` // RTMP server RTMP bool `json:"rtmp"` @@ -376,6 +380,10 @@ func (conf *Conf) setDefaults() { conf.MulticastIPRange = "224.1.0.0/16" conf.MulticastRTPPort = 8002 conf.MulticastRTCPPort = 8003 + conf.SRTPAddress = ":8004" + conf.SRTCPAddress = ":8005" + conf.MulticastSRTPPort = 8006 + conf.MulticastSRTCPPort = 8007 conf.RTSPServerKey = "server.key" conf.RTSPServerCert = "server.crt" conf.RTSPAuthMethods = RTSPAuthMethods{auth.VerifyMethodBasic} @@ -619,14 +627,6 @@ func (conf *Conf) Validate(l logger.Writer) error { l.Log(logger.Warn, "parameter 'encryption' is deprecated and has been replaced with 'rtspEncryption'") conf.RTSPEncryption = *conf.Encryption } - if conf.RTSPEncryption == EncryptionStrict { - if _, ok := conf.RTSPTransports[gortsplib.TransportUDP]; ok { - return fmt.Errorf("strict encryption cannot be used with the UDP transport protocol") - } - if _, ok := conf.RTSPTransports[gortsplib.TransportUDPMulticast]; ok { - return fmt.Errorf("strict encryption cannot be used with the UDP-multicast transport protocol") - } - } if conf.AuthMethods != nil { l.Log(logger.Warn, "parameter 'authMethods' is deprecated and has been replaced with 'rtspAuthMethods'") conf.RTSPAuthMethods = *conf.AuthMethods diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index ce3e8f98..56adb951 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -291,18 +291,6 @@ func TestConfErrors(t *testing.T) { "udpMaxPayloadSize: 5000\n", "'udpMaxPayloadSize' must be less than 1472", }, - { - "invalid strict encryption 1", - "rtspEncryption: strict\n" + - "rtspTransports: [udp]\n", - "strict encryption cannot be used with the UDP transport protocol", - }, - { - "invalid strict encryption 2", - "rtspEncryption: strict\n" + - "rtspTransports: [multicast]\n", - "strict encryption cannot be used with the UDP-multicast transport protocol", - }, { "invalid ICE server", "webrtcICEServers: [testing]\n", diff --git a/internal/core/core.go b/internal/core/core.go index 4ce961f5..8a9847b0 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -410,19 +410,22 @@ func (p *Core) createResources(initial bool) error { (p.conf.RTSPEncryption == conf.EncryptionStrict || p.conf.RTSPEncryption == conf.EncryptionOptional) && p.rtspsServer == nil { + _, useUDP := p.conf.RTSPTransports[gortsplib.TransportUDP] + _, useMulticast := p.conf.RTSPTransports[gortsplib.TransportUDPMulticast] + i := &rtsp.Server{ Address: p.conf.RTSPSAddress, AuthMethods: p.conf.RTSPAuthMethods, ReadTimeout: p.conf.ReadTimeout, WriteTimeout: p.conf.WriteTimeout, WriteQueueSize: p.conf.WriteQueueSize, - UseUDP: false, - UseMulticast: false, - RTPAddress: "", - RTCPAddress: "", - MulticastIPRange: "", - MulticastRTPPort: 0, - MulticastRTCPPort: 0, + UseUDP: useUDP, + UseMulticast: useMulticast, + RTPAddress: p.conf.SRTPAddress, + RTCPAddress: p.conf.SRTCPAddress, + MulticastIPRange: p.conf.MulticastIPRange, + MulticastRTPPort: p.conf.MulticastSRTPPort, + MulticastRTCPPort: p.conf.MulticastSRTCPPort, IsTLS: true, ServerCert: p.conf.RTSPServerCert, ServerKey: p.conf.RTSPServerKey, diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 36e21aa9..2becc779 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -398,12 +398,15 @@ func TestPathRunOnRead(t *testing.T) { switch ca { case "rtsp": - reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://127.0.0.1:8554/test?query=value") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() @@ -417,12 +420,16 @@ func TestPathRunOnRead(t *testing.T) { require.NoError(t, err) case "rtsps": - reader := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}} - u, err := base.ParseURL("rtsps://127.0.0.1:8322/test?query=value") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() @@ -649,12 +656,15 @@ func TestPathMaxReaders(t *testing.T) { defer source.Close() for i := 0; i < 2; i++ { - reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://127.0.0.1:8554/mystream") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() @@ -796,8 +806,12 @@ func TestPathFallback(t *testing.T) { u, err := base.ParseURL("rtsp://localhost:8554/path1") require.NoError(t, err) - dest := gortsplib.Client{} - err = dest.Start(u.Scheme, u.Host) + dest := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = dest.Start2() require.NoError(t, err) defer dest.Close() @@ -856,12 +870,15 @@ func TestPathResolveSource(t *testing.T) { require.Equal(t, true, ok) defer p.Close() - reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a?key=val") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() @@ -909,12 +926,15 @@ func TestPathOverridePublisher(t *testing.T) { frameRecv := make(chan struct{}) - c := gortsplib.Client{} - u, err := base.ParseURL("rtsp://localhost:8554/teststream") require.NoError(t, err) - err = c.Start(u.Scheme, u.Host) + c := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = c.Start2() require.NoError(t, err) defer c.Close() diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index c7e12c6d..f926ed32 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -253,12 +253,15 @@ func TestServerRead(t *testing.T) { require.NoError(t, err) defer s.Close() - reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() @@ -370,12 +373,15 @@ func TestServerRedirect(t *testing.T) { require.NoError(t, err) defer s.Close() - reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/path1?param=value") require.NoError(t, err) - err = reader.Start(u.Scheme, u.Host) + reader := gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, + } + + err = reader.Start2() require.NoError(t, err) defer reader.Close() diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index 49bf4434..7171cad3 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -110,7 +110,14 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { decodeErrors.Start() defer decodeErrors.Stop() + u, err := base.ParseURL(params.ResolvedSource) + if err != nil { + return err + } + c := &gortsplib.Client{ + Scheme: u.Scheme, + Host: u.Host, Transport: params.Conf.RTSPTransport.Transport, TLSConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint), ReadTimeout: time.Duration(s.ReadTimeout), @@ -134,12 +141,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { }, } - u, err := base.ParseURL(params.ResolvedSource) - if err != nil { - return err - } - - err = c.Start(u.Scheme, u.Host) + err = c.Start2() if err != nil { return err } diff --git a/mediamtx.yml b/mediamtx.yml index 69d922f4..052257df 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -237,12 +237,12 @@ playbackTrustedProxies: [] rtsp: yes # List of enabled RTSP transport protocols. # UDP is the most performant, but doesn't work when there's a NAT/firewall between -# server and clients, and doesn't support encryption. +# server and clients. # UDP-multicast allows to save bandwidth when clients are all in the same LAN. -# TCP is the most versatile, and does support encryption. +# TCP is the most versatile. # The handshake is always performed with TCP. rtspTransports: [udp, multicast, tcp] -# Encrypt handshakes and TCP streams with TLS (RTSPS). +# Use secure protocol variants (RTSPS, TLS, SRTP). # Available values are "no", "strict", "optional". rtspEncryption: "no" # Address of the TCP/RTSP listener. This is needed only when encryption is "no" or "optional". @@ -259,6 +259,14 @@ multicastIPRange: 224.1.0.0/16 multicastRTPPort: 8002 # Port of all UDP-multicast/RTCP listeners. This is needed only when "multicast" is in rtspTransports. multicastRTCPPort: 8003 +# Address of the UDP/SRTP listener. This is needed only when "udp" is in rtspTransports and encryption is enabled. +srtpAddress: :8004 +# Address of the UDP/SRTCP listener. This is needed only when "udp" is in rtspTransports and encryption is enabled. +srtcpAddress: :8005 +# Port of all UDP-multicast/SRTP listeners. This is needed only when "multicast" is in rtspTransports and encryption is enabled. +multicastSRTPPort: 8006 +# Port of all UDP-multicast/SRTCP listeners. This is needed only when "multicast" is in rtspTransports and encryption is enabled. +multicastSRTCPPort: 8007 # Path to the server key. This is needed only when encryption is "strict" or "optional". # This can be generated with: # openssl genrsa -out server.key 2048