From 75f518a8279a8c381ab5fa2dd05fa17326ca3497 Mon Sep 17 00:00:00 2001 From: rmcnew Date: Sat, 23 Sep 2023 04:55:45 -0600 Subject: [PATCH] Support SRT encryption passphrases on configured paths (#2385) * Support SRT encryption passphrases on configured paths * Fix namespace; make passphrase use more clear * Updates from Alessandro's feedback and lint output * Fix lint findings * Update lint.yml * Update lint.yml * Fix lint findings * Add SRT passphrase tests for conf/path * Reformat test to pass lint * More changes to pass lint * More changes to pass lint * Use safeConf to prevent race conditions * update API docs * split configuration checks from connection checks * add tests * rename publishSRTPassphrase into srtPublishPassphrase, readSRTPassphrase into srtReadPassphrase * remove redundant alias --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com> --- apidocs/openapi.yaml | 4 + internal/conf/conf_test.go | 14 +++ internal/conf/path.go | 27 +++++ internal/core/srt_conn.go | 27 +++++ internal/core/srt_server_test.go | 198 ++++++++++++++++++------------- mediamtx.yml | 4 + 6 files changed, 189 insertions(+), 85 deletions(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index e391aa94..64da3c1d 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -239,6 +239,10 @@ components: type: boolean fallback: type: string + srtPublishPassphrase: + type: string + srtReadPassphrase: + type: string # RTSP sourceProtocol: diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index cfce1c05..b6b6057d 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -301,6 +301,20 @@ func TestConfErrors(t *testing.T) { " source: rpiCamera\n", "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam1' and 'cam2'", }, + { + "invalid srt publish passphrase", + "paths:\n" + + " mypath:\n" + + " srtPublishPassphrase: a\n", + `invalid 'srtPublishPassphrase': must be between 10 and 79 characters`, + }, + { + "invalid srt read passphrase", + "paths:\n" + + " mypath:\n" + + " srtReadPassphrase: a\n", + `invalid 'readRTPassphrase': must be between 10 and 79 characters`, + }, } { t.Run(ca.name, func(t *testing.T) { tmpf, err := writeTempFile([]byte(ca.conf)) diff --git a/internal/conf/path.go b/internal/conf/path.go index c7199bb4..f0533c7c 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -38,6 +38,16 @@ func IsValidPathName(name string) error { return nil } +func srtCheckPassphrase(passphrase string) error { + switch { + case len(passphrase) < 10 || len(passphrase) > 79: + return fmt.Errorf("must be between 10 and 79 characters") + + default: + return nil + } +} + // PathConf is a path configuration. type PathConf struct { Regexp *regexp.Regexp `json:"-"` @@ -63,6 +73,8 @@ type PathConf struct { OverridePublisher bool `json:"overridePublisher"` DisablePublisherOverride bool `json:"disablePublisherOverride"` // deprecated Fallback string `json:"fallback"` + SRTPublishPassphrase string `json:"srtPublishPassphrase"` + SRTReadPassphrase string `json:"srtReadPassphrase"` // RTSP SourceProtocol SourceProtocol `json:"sourceProtocol"` @@ -362,6 +374,21 @@ func (pconf *PathConf) check(conf *Conf, name string) error { } } + // SRT + + if pconf.SRTPublishPassphrase != "" { + err := srtCheckPassphrase(pconf.SRTPublishPassphrase) + if err != nil { + return fmt.Errorf("invalid 'srtPublishPassphrase': %v", err) + } + } + if pconf.SRTReadPassphrase != "" { + err := srtCheckPassphrase(pconf.SRTReadPassphrase) + if err != nil { + return fmt.Errorf("invalid 'readRTPassphrase': %v", err) + } + } + // Hooks if pconf.RunOnInit != "" && pconf.Regexp != nil { diff --git a/internal/core/srt_conn.go b/internal/core/srt_conn.go index cb212c14..96bcc6b4 100644 --- a/internal/core/srt_conn.go +++ b/internal/core/srt_conn.go @@ -31,6 +31,23 @@ func durationGoToMPEGTS(v time.Duration) int64 { return int64(v.Seconds() * 90000) } +func srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error { + if passphrase == "" { + return nil + } + + if !connReq.IsEncrypted() { + return fmt.Errorf("connection is encrypted, but not passphrase is defined in configuration") + } + + err := connReq.SetPassphrase(passphrase) + if err != nil { + return fmt.Errorf("invalid passphrase") + } + + return nil +} + type srtConnState int const ( @@ -222,6 +239,11 @@ func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pa defer res.path.removePublisher(pathRemovePublisherReq{author: c}) + err := srtCheckPassphrase(req.connReq, res.path.conf.SRTPublishPassphrase) + if err != nil { + return false, err + } + sconn, err := c.exchangeRequestWithConn(req) if err != nil { return true, err @@ -314,6 +336,11 @@ func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass defer res.path.removeReader(pathRemoveReaderReq{author: c}) + err := srtCheckPassphrase(req.connReq, res.path.conf.SRTReadPassphrase) + if err != nil { + return false, err + } + sconn, err := c.exchangeRequestWithConn(req) if err != nil { return true, err diff --git a/internal/core/srt_server_test.go b/internal/core/srt_server_test.go index 73a83987..d1afc29a 100644 --- a/internal/core/srt_server_test.go +++ b/internal/core/srt_server_test.go @@ -11,108 +11,136 @@ import ( ) func TestSRTServer(t *testing.T) { - p, ok := newInstance("paths:\n" + - " all:\n") - require.Equal(t, true, ok) - defer p.Close() + for _, ca := range []string{ + "no passphrase", + "publish passphrase", + "read passphrase", + } { + t.Run(ca, func(t *testing.T) { + conf := "paths:\n" + + " all:\n" - conf := srt.DefaultConfig() - address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=publish:mypath") - require.NoError(t, err) + switch ca { + case "publish passphrase": + conf += " srtPublishPassphrase: 123456789abcde" - err = conf.Validate() - require.NoError(t, err) + case "read passphrase": + conf += " srtReadPassphrase: 123456789abcde" + } - publisher, err := srt.Dial("srt", address, conf) - require.NoError(t, err) - defer publisher.Close() + p, ok := newInstance(conf) + require.Equal(t, true, ok) + defer p.Close() - track := &mpegts.Track{ - Codec: &mpegts.CodecH264{}, - } + u := "srt://localhost:8890?streamid=publish:mypath" + if ca == "publish passphrase" { + u += "&passphrase=123456789abcde" + } - bw := bufio.NewWriter(publisher) - w := mpegts.NewWriter(bw, []*mpegts.Track{track}) - require.NoError(t, err) + srtConf := srt.DefaultConfig() + address, err := srtConf.UnmarshalURL(u) + require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{ - { // SPS - 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, - 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, - 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, - 0x20, - }, - { // PPS - 0x08, 0x06, 0x07, 0x08, - }, - { // IDR - 0x05, 1, - }, - }) - require.NoError(t, err) + err = srtConf.Validate() + require.NoError(t, err) - err = bw.Flush() - require.NoError(t, err) + publisher, err := srt.Dial("srt", address, srtConf) + require.NoError(t, err) + defer publisher.Close() - time.Sleep(500 * time.Millisecond) + track := &mpegts.Track{ + Codec: &mpegts.CodecH264{}, + } - conf = srt.DefaultConfig() - address, err = conf.UnmarshalURL("srt://localhost:8890?streamid=read:mypath") - require.NoError(t, err) + bw := bufio.NewWriter(publisher) + w := mpegts.NewWriter(bw, []*mpegts.Track{track}) + require.NoError(t, err) - err = conf.Validate() - require.NoError(t, err) + err = w.WriteH26x(track, 0, 0, true, [][]byte{ + { // SPS + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, + 0x20, + }, + { // PPS + 0x08, 0x06, 0x07, 0x08, + }, + { // IDR + 0x05, 1, + }, + }) + require.NoError(t, err) - reader, err := srt.Dial("srt", address, conf) - require.NoError(t, err) - defer reader.Close() + err = bw.Flush() + require.NoError(t, err) - err = w.WriteH26x(track, 2*90000, 1*90000, true, [][]byte{ - { // IDR - 0x05, 2, - }, - }) - require.NoError(t, err) + time.Sleep(500 * time.Millisecond) - err = bw.Flush() - require.NoError(t, err) + u = "srt://localhost:8890?streamid=read:mypath" + if ca == "read passphrase" { + u += "&passphrase=123456789abcde" + } - r, err := mpegts.NewReader(reader) - require.NoError(t, err) + srtConf = srt.DefaultConfig() + address, err = srtConf.UnmarshalURL(u) + require.NoError(t, err) - require.Equal(t, []*mpegts.Track{{ - PID: 256, - Codec: &mpegts.CodecH264{}, - }}, r.Tracks()) + err = srtConf.Validate() + require.NoError(t, err) - received := false + reader, err := srt.Dial("srt", address, srtConf) + require.NoError(t, err) + defer reader.Close() - r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { - require.Equal(t, int64(0), pts) - require.Equal(t, int64(0), dts) - require.Equal(t, [][]byte{ - { // SPS - 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, - 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, - 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, - 0x20, - }, - { // PPS - 0x08, 0x06, 0x07, 0x08, - }, - { // IDR - 0x05, 1, - }, - }, au) - received = true - return nil - }) + err = w.WriteH26x(track, 2*90000, 1*90000, true, [][]byte{ + { // IDR + 0x05, 2, + }, + }) + require.NoError(t, err) - for { - err = r.Read() - require.NoError(t, err) - if received { - break - } + err = bw.Flush() + require.NoError(t, err) + + r, err := mpegts.NewReader(reader) + require.NoError(t, err) + + require.Equal(t, []*mpegts.Track{{ + PID: 256, + Codec: &mpegts.CodecH264{}, + }}, r.Tracks()) + + received := false + + r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { + require.Equal(t, int64(0), pts) + require.Equal(t, int64(0), dts) + require.Equal(t, [][]byte{ + { // SPS + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, + 0x20, + }, + { // PPS + 0x08, 0x06, 0x07, 0x08, + }, + { // IDR + 0x05, 1, + }, + }, au) + received = true + return nil + }) + + for { + err = r.Read() + require.NoError(t, err) + if received { + break + } + } + }) } } diff --git a/mediamtx.yml b/mediamtx.yml index 8a55b85a..4c2e860c 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -338,6 +338,10 @@ paths: # if no one is publishing, redirect readers to this path. # It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL. fallback: + # SRT encryption passphrase required to publish on this path + srtPublishPassphrase: + # SRT encryption passphrase require to read from this path + srtReadPassphrase: ############################################### # RTSP path settings (when source is a RTSP or a RTSPS URL)