mirror of
https://github.com/bluenviron/mediamtx
synced 2024-12-22 06:40:30 +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
|
||||
|
||||
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
|
||||
@ -171,7 +171,7 @@ Streams can then be published and read with the `rtsps` scheme and the `8555` po
|
||||
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
|
||||
|
@ -66,34 +66,41 @@ func CheckPathName(name string) error {
|
||||
|
||||
// PathConf is a path configuration.
|
||||
type PathConf struct {
|
||||
Regexp *regexp.Regexp `yaml:"-" json:"-"`
|
||||
Regexp *regexp.Regexp `yaml:"-" json:"-"`
|
||||
|
||||
// source
|
||||
Source string `yaml:"source"`
|
||||
SourceProtocol string `yaml:"sourceProtocol"`
|
||||
SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"`
|
||||
SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"`
|
||||
SourceOnDemand bool `yaml:"sourceOnDemand"`
|
||||
SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"`
|
||||
SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"`
|
||||
SourceRedirect string `yaml:"sourceRedirect"`
|
||||
DisablePublisherOverride bool `yaml:"disablePublisherOverride"`
|
||||
Fallback string `yaml:"fallback"`
|
||||
RunOnInit string `yaml:"runOnInit"`
|
||||
RunOnInitRestart bool `yaml:"runOnInitRestart"`
|
||||
RunOnDemand string `yaml:"runOnDemand"`
|
||||
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
|
||||
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
|
||||
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
|
||||
RunOnPublish string `yaml:"runOnPublish"`
|
||||
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
|
||||
RunOnRead string `yaml:"runOnRead"`
|
||||
RunOnReadRestart bool `yaml:"runOnReadRestart"`
|
||||
PublishUser string `yaml:"publishUser"`
|
||||
PublishPass string `yaml:"publishPass"`
|
||||
PublishIps []string `yaml:"publishIps"`
|
||||
PublishIpsParsed []interface{} `yaml:"-" json:"-"`
|
||||
ReadUser string `yaml:"readUser"`
|
||||
ReadPass string `yaml:"readPass"`
|
||||
ReadIps []string `yaml:"readIps"`
|
||||
ReadIpsParsed []interface{} `yaml:"-" json:"-"`
|
||||
|
||||
// custom commands
|
||||
RunOnInit string `yaml:"runOnInit"`
|
||||
RunOnInitRestart bool `yaml:"runOnInitRestart"`
|
||||
RunOnDemand string `yaml:"runOnDemand"`
|
||||
RunOnDemandRestart bool `yaml:"runOnDemandRestart"`
|
||||
RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"`
|
||||
RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"`
|
||||
RunOnPublish string `yaml:"runOnPublish"`
|
||||
RunOnPublishRestart bool `yaml:"runOnPublishRestart"`
|
||||
RunOnRead string `yaml:"runOnRead"`
|
||||
RunOnReadRestart bool `yaml:"runOnReadRestart"`
|
||||
|
||||
// authentication
|
||||
PublishUser string `yaml:"publishUser"`
|
||||
PublishPass string `yaml:"publishPass"`
|
||||
PublishIps []string `yaml:"publishIps"`
|
||||
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 {
|
||||
@ -163,6 +170,10 @@ func (pconf *PathConf) fillAndCheck(name string) error {
|
||||
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://"):
|
||||
if pconf.Regexp != nil {
|
||||
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.conf.Source,
|
||||
pa.conf.SourceProtocolParsed,
|
||||
pa.conf.SourceFingerprint,
|
||||
pa.readTimeout,
|
||||
pa.writeTimeout,
|
||||
pa.readBufferCount,
|
||||
|
@ -1,6 +1,11 @@
|
||||
package sourcertsp
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -28,6 +33,7 @@ type Parent interface {
|
||||
type Source struct {
|
||||
ur string
|
||||
proto *gortsplib.StreamProtocol
|
||||
fingerprint string
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
readBufferCount int
|
||||
@ -41,8 +47,10 @@ type Source struct {
|
||||
}
|
||||
|
||||
// New allocates a Source.
|
||||
func New(ur string,
|
||||
func New(
|
||||
ur string,
|
||||
proto *gortsplib.StreamProtocol,
|
||||
fingerprint string,
|
||||
readTimeout time.Duration,
|
||||
writeTimeout time.Duration,
|
||||
readBufferCount int,
|
||||
@ -53,6 +61,7 @@ func New(ur string,
|
||||
s := &Source{
|
||||
ur: ur,
|
||||
proto: proto,
|
||||
fingerprint: fingerprint,
|
||||
readTimeout: readTimeout,
|
||||
writeTimeout: writeTimeout,
|
||||
readBufferCount: readBufferCount,
|
||||
@ -121,7 +130,23 @@ func (s *Source) runInner() bool {
|
||||
defer close(dialDone)
|
||||
|
||||
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,
|
||||
WriteTimeout: s.writeTimeout,
|
||||
ReadBufferCount: s.readBufferCount,
|
||||
|
@ -98,6 +98,7 @@ func TestSourceRTSP(t *testing.T) {
|
||||
"paths:\n" +
|
||||
" proxied:\n" +
|
||||
" source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
|
||||
" sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
|
||||
" sourceOnDemand: yes\n")
|
||||
require.Equal(t, true, ok)
|
||||
defer p2.close()
|
||||
|
@ -60,6 +60,9 @@ rtpPort: 8000
|
||||
# port of the UDP/RTCP listener. This is used only if "udp" is in protocols.
|
||||
rtcpPort: 8001
|
||||
# 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
|
||||
# path to the server certificate. This is used only if encryption is "strict" or "optional".
|
||||
serverCert: server.crt
|
||||
@ -92,16 +95,23 @@ paths:
|
||||
# source of the stream - this can be:
|
||||
# * record -> the stream is published by a RTSP or RTMP client
|
||||
# * 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
|
||||
# * redirect -> the stream is provided by another path or server
|
||||
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".
|
||||
# the tcp protocol can help to overcome the error "no UDP packets received recently".
|
||||
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
|
||||
# one reader is connected, saving bandwidth.
|
||||
sourceOnDemand: no
|
||||
|
Loading…
Reference in New Issue
Block a user