mirror of
https://github.com/bluenviron/mediamtx
synced 2025-03-11 06:47:58 +00:00
RTSP source: add mandatory parameter sourceFingerprint to validate server certificates and prevent man-in-the-middle attacks (#350)
This commit is contained in:
parent
cbda813e1a
commit
3d1b5296d7
@ -149,7 +149,7 @@ The configuration can be changed dinamically when the server is running (hot rel
|
|||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A TLS certificate must be installed on the server; if the server is installed on a machine that is publicly accessible from the internet, a certificate can be requested from a Certificate authority by using tools like [Certbot](https://certbot.eff.org/); otherwise, a self-signed certificate can be generated with openSSL:
|
Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A self-signed TLS certificate is needed and can be generated with openSSL:
|
||||||
|
|
||||||
```
|
```
|
||||||
openssl genrsa -out server.key 2048
|
openssl genrsa -out server.key 2048
|
||||||
@ -171,7 +171,7 @@ Streams can then be published and read with the `rtsps` scheme and the `8555` po
|
|||||||
ffmpeg -i rtsps://ip:8555/...
|
ffmpeg -i rtsps://ip:8555/...
|
||||||
```
|
```
|
||||||
|
|
||||||
If the client is _GStreamer_ and the server certificate is self signed, remember to disable the certificate validation:
|
If the client is _GStreamer_, disable the certificate validation:
|
||||||
|
|
||||||
```
|
```
|
||||||
gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0
|
gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0
|
||||||
|
@ -66,34 +66,41 @@ func CheckPathName(name string) error {
|
|||||||
|
|
||||||
// PathConf is a path configuration.
|
// PathConf is a path configuration.
|
||||||
type PathConf struct {
|
type PathConf struct {
|
||||||
Regexp *regexp.Regexp `yaml:"-" json:"-"`
|
Regexp *regexp.Regexp `yaml:"-" json:"-"`
|
||||||
|
|
||||||
|
// source
|
||||||
Source string `yaml:"source"`
|
Source string `yaml:"source"`
|
||||||
SourceProtocol string `yaml:"sourceProtocol"`
|
SourceProtocol string `yaml:"sourceProtocol"`
|
||||||
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
|
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
|
||||||
|
SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"`
|
||||||
SourceOnDemand bool `yaml:"sourceOnDemand"`
|
SourceOnDemand bool `yaml:"sourceOnDemand"`
|
||||||
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
|
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
|
||||||
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
|
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
|
||||||
SourceRedirect string `yaml:"sourceRedirect"`
|
SourceRedirect string `yaml:"sourceRedirect"`
|
||||||
DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
|
DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
|
||||||
Fallback string `yaml:"fallback"`
|
Fallback string `yaml:"fallback"`
|
||||||
RunOnInit string `yaml:"runOnInit"`
|
|
||||||
RunOnInitRestart bool `yaml:"runOnInitRestart"`
|
// custom commands
|
||||||
RunOnDemand string `yaml:"runOnDemand"`
|
RunOnInit string `yaml:"runOnInit"`
|
||||||
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
|
RunOnInitRestart bool `yaml:"runOnInitRestart"`
|
||||||
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
|
RunOnDemand string `yaml:"runOnDemand"`
|
||||||
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
|
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
|
||||||
RunOnPublish string `yaml:"runOnPublish"`
|
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
|
||||||
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
|
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
|
||||||
RunOnRead string `yaml:"runOnRead"`
|
RunOnPublish string `yaml:"runOnPublish"`
|
||||||
RunOnReadRestart bool `yaml:"runOnReadRestart"`
|
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
|
||||||
PublishUser string `yaml:"publishUser"`
|
RunOnRead string `yaml:"runOnRead"`
|
||||||
PublishPass string `yaml:"publishPass"`
|
RunOnReadRestart bool `yaml:"runOnReadRestart"`
|
||||||
PublishIps []string `yaml:"publishIps"`
|
|
||||||
PublishIpsParsed []interface{} `yaml:"-" json:"-"`
|
// authentication
|
||||||
ReadUser string `yaml:"readUser"`
|
PublishUser string `yaml:"publishUser"`
|
||||||
ReadPass string `yaml:"readPass"`
|
PublishPass string `yaml:"publishPass"`
|
||||||
ReadIps []string `yaml:"readIps"`
|
PublishIps []string `yaml:"publishIps"`
|
||||||
ReadIpsParsed []interface{} `yaml:"-" json:"-"`
|
PublishIpsParsed []interface{} `yaml:"-" json:"-"`
|
||||||
|
ReadUser string `yaml:"readUser"`
|
||||||
|
ReadPass string `yaml:"readPass"`
|
||||||
|
ReadIps []string `yaml:"readIps"`
|
||||||
|
ReadIpsParsed []interface{} `yaml:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pconf *PathConf) fillAndCheck(name string) error {
|
func (pconf *PathConf) fillAndCheck(name string) error {
|
||||||
@ -163,6 +170,10 @@ func (pconf *PathConf) fillAndCheck(name string) error {
|
|||||||
return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol)
|
return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pconf.Source, "rtsps://") && pconf.SourceFingerprint == "" {
|
||||||
|
return fmt.Errorf("sourceFingerprint is required with a RTSPS URL")
|
||||||
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "rtmp://"):
|
case strings.HasPrefix(pconf.Source, "rtmp://"):
|
||||||
if pconf.Regexp != nil {
|
if pconf.Regexp != nil {
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path")
|
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path")
|
||||||
|
@ -406,6 +406,7 @@ func (pa *Path) startExternalSource() {
|
|||||||
pa.source = sourcertsp.New(
|
pa.source = sourcertsp.New(
|
||||||
pa.conf.Source,
|
pa.conf.Source,
|
||||||
pa.conf.SourceProtocolParsed,
|
pa.conf.SourceProtocolParsed,
|
||||||
|
pa.conf.SourceFingerprint,
|
||||||
pa.readTimeout,
|
pa.readTimeout,
|
||||||
pa.writeTimeout,
|
pa.writeTimeout,
|
||||||
pa.readBufferCount,
|
pa.readBufferCount,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package sourcertsp
|
package sourcertsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -28,6 +33,7 @@ type Parent interface {
|
|||||||
type Source struct {
|
type Source struct {
|
||||||
ur string
|
ur string
|
||||||
proto *gortsplib.StreamProtocol
|
proto *gortsplib.StreamProtocol
|
||||||
|
fingerprint string
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
writeTimeout time.Duration
|
writeTimeout time.Duration
|
||||||
readBufferCount int
|
readBufferCount int
|
||||||
@ -41,8 +47,10 @@ type Source struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New allocates a Source.
|
// New allocates a Source.
|
||||||
func New(ur string,
|
func New(
|
||||||
|
ur string,
|
||||||
proto *gortsplib.StreamProtocol,
|
proto *gortsplib.StreamProtocol,
|
||||||
|
fingerprint string,
|
||||||
readTimeout time.Duration,
|
readTimeout time.Duration,
|
||||||
writeTimeout time.Duration,
|
writeTimeout time.Duration,
|
||||||
readBufferCount int,
|
readBufferCount int,
|
||||||
@ -53,6 +61,7 @@ func New(ur string,
|
|||||||
s := &Source{
|
s := &Source{
|
||||||
ur: ur,
|
ur: ur,
|
||||||
proto: proto,
|
proto: proto,
|
||||||
|
fingerprint: fingerprint,
|
||||||
readTimeout: readTimeout,
|
readTimeout: readTimeout,
|
||||||
writeTimeout: writeTimeout,
|
writeTimeout: writeTimeout,
|
||||||
readBufferCount: readBufferCount,
|
readBufferCount: readBufferCount,
|
||||||
@ -121,7 +130,23 @@ func (s *Source) runInner() bool {
|
|||||||
defer close(dialDone)
|
defer close(dialDone)
|
||||||
|
|
||||||
conf := gortsplib.ClientConf{
|
conf := gortsplib.ClientConf{
|
||||||
StreamProtocol: s.proto,
|
StreamProtocol: s.proto,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(cs.PeerCertificates[0].Raw)
|
||||||
|
hstr := hex.EncodeToString(h.Sum(nil))
|
||||||
|
fingerprintLower := strings.ToLower(s.fingerprint)
|
||||||
|
|
||||||
|
if hstr != fingerprintLower {
|
||||||
|
return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
|
||||||
|
fingerprintLower, hstr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
ReadTimeout: s.readTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
WriteTimeout: s.writeTimeout,
|
WriteTimeout: s.writeTimeout,
|
||||||
ReadBufferCount: s.readBufferCount,
|
ReadBufferCount: s.readBufferCount,
|
||||||
|
@ -98,6 +98,7 @@ func TestSourceRTSP(t *testing.T) {
|
|||||||
"paths:\n" +
|
"paths:\n" +
|
||||||
" proxied:\n" +
|
" proxied:\n" +
|
||||||
" source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
|
" source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
|
||||||
|
" sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
|
||||||
" sourceOnDemand: yes\n")
|
" sourceOnDemand: yes\n")
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
defer p2.close()
|
defer p2.close()
|
||||||
|
@ -60,6 +60,9 @@ rtpPort: 8000
|
|||||||
# port of the UDP/RTCP listener. This is used only if "udp" is in protocols.
|
# port of the UDP/RTCP listener. This is used only if "udp" is in protocols.
|
||||||
rtcpPort: 8001
|
rtcpPort: 8001
|
||||||
# path to the server key. This is used only if encryption is "strict" or "optional".
|
# path to the server key. This is used only if encryption is "strict" or "optional".
|
||||||
|
# this can be generated with:
|
||||||
|
# openssl genrsa -out server.key 2048
|
||||||
|
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||||
serverKey: server.key
|
serverKey: server.key
|
||||||
# path to the server certificate. This is used only if encryption is "strict" or "optional".
|
# path to the server certificate. This is used only if encryption is "strict" or "optional".
|
||||||
serverCert: server.crt
|
serverCert: server.crt
|
||||||
@ -92,16 +95,23 @@ paths:
|
|||||||
# source of the stream - this can be:
|
# source of the stream - this can be:
|
||||||
# * record -> the stream is published by a RTSP or RTMP client
|
# * record -> the stream is published by a RTSP or RTMP client
|
||||||
# * rtsp://existing-url -> the stream is pulled from another RTSP server
|
# * rtsp://existing-url -> the stream is pulled from another RTSP server
|
||||||
# * rtsps://existing-url -> the stream is pulled from another RTSP server
|
# * rtsps://existing-url -> the stream is pulled from another RTSP server, with RTSPS
|
||||||
# * rtmp://existing-url -> the stream is pulled from a RTMP server
|
# * rtmp://existing-url -> the stream is pulled from a RTMP server
|
||||||
# * redirect -> the stream is provided by another path or server
|
# * redirect -> the stream is provided by another path or server
|
||||||
source: record
|
source: record
|
||||||
|
|
||||||
# if the source is an RTSP URL, this is the protocol that will be used to
|
# if the source is an RTSP or RTSPS URL, this is the protocol that will be used to
|
||||||
# pull the stream. available options are "automatic", "udp", "tcp".
|
# pull the stream. available options are "automatic", "udp", "tcp".
|
||||||
# the tcp protocol can help to overcome the error "no UDP packets received recently".
|
# the tcp protocol can help to overcome the error "no UDP packets received recently".
|
||||||
sourceProtocol: automatic
|
sourceProtocol: automatic
|
||||||
|
|
||||||
|
# if the source is an RTSPS URL, the fingerprint of the certificate of the source
|
||||||
|
# must be provided in order to prevent man-in-the-middle attacks.
|
||||||
|
# it can be obtained from the source by running:
|
||||||
|
# openssl s_client -connect source_ip:source_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
|
||||||
|
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
|
||||||
|
sourceFingerprint:
|
||||||
|
|
||||||
# if the source is an RTSP or RTMP URL, it will be pulled only when at least
|
# if the source is an RTSP or RTMP URL, it will be pulled only when at least
|
||||||
# one reader is connected, saving bandwidth.
|
# one reader is connected, saving bandwidth.
|
||||||
sourceOnDemand: no
|
sourceOnDemand: no
|
||||||
|
Loading…
Reference in New Issue
Block a user