From f5d7ed3138a5f57a1c37e499f019270cc4e09716 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Fri, 5 Jun 2026 17:01:36 +0200 Subject: [PATCH] improve anti-brute force mechanism (#5835) delay authentication failure responses by a random amount of time, use the same anti-brute force mechanism with all users. --- README.md | 2 +- docs/1-kickoff/1-introduction.md | 2 +- docs/3-publish/01-moq-clients.md | 4 +- docs/4-read/06-hls.md | 64 +------------------------- internal/api/api.go | 5 +- internal/auth/delay_brute_force.go | 28 +++++++++++ internal/auth/manager.go | 3 -- internal/core/path.go | 8 ++-- internal/core/path_manager.go | 25 ++++++---- internal/core/path_manager_test.go | 6 +-- internal/metrics/metrics.go | 5 +- internal/playback/server.go | 5 +- internal/playback/server_test.go | 4 -- internal/pprof/pprof.go | 5 +- internal/servers/hls/http_server.go | 12 ----- internal/servers/hls/server_test.go | 4 -- internal/servers/moq/http_server.go | 3 -- internal/servers/moq/session.go | 10 ---- internal/servers/rtmp/conn.go | 10 ---- internal/servers/rtsp/conn.go | 16 +++---- internal/servers/rtsp/server.go | 2 +- internal/servers/rtsp/server_test.go | 33 +++++++------ internal/servers/srt/conn.go | 4 -- internal/servers/webrtc/http_server.go | 9 ---- internal/servers/webrtc/server_test.go | 19 ++++---- internal/test/path_manager.go | 4 +- 26 files changed, 100 insertions(+), 192 deletions(-) create mode 100644 internal/auth/delay_brute_force.go diff --git a/README.md b/README.md index 3e1c9410..14b98e09 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@
-_MediaMTX_ is a ready-to-use and zero-dependency real-time media server and media proxy that allows to publish, read, proxy, record and playback video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability. +_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy that allows to publish, read, proxy, record and playback real-time video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability.
diff --git a/docs/1-kickoff/1-introduction.md b/docs/1-kickoff/1-introduction.md index f31261f8..9a23b31d 100644 --- a/docs/1-kickoff/1-introduction.md +++ b/docs/1-kickoff/1-introduction.md @@ -2,7 +2,7 @@ Welcome to the MediaMTX documentation! -_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability. +_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy that allows to publish, read, proxy, record and playback real-time video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability. Main features: diff --git a/docs/3-publish/01-moq-clients.md b/docs/3-publish/01-moq-clients.md index d476fa96..7cdf3fa5 100644 --- a/docs/3-publish/01-moq-clients.md +++ b/docs/3-publish/01-moq-clients.md @@ -11,7 +11,7 @@ Media-over-QUIC has a wide range of features and variants, most of them in activ - We support `draft-18` of the [main specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-18). - We only support using Media-over-QUIC through browsers and in particular through the WebTransport API. We do not support using QUIC directly. -- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used by a routing solution like _MediaMTX_. +- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used with a routing solution like _MediaMTX_. - We use the MOQT Streaming Format (MSF) to advertise tracks, described in [this specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00). - We use the Low Overhead Media Container (LOC) to ship frames, described in [this specification](https://datatracker.ietf.org/doc/draft-ietf-moq-loc/). - We host web pages through a HTTP/2 listener and host the WebTransport endpoint through a HTTP/3 listener. This hybrid setup allows to use self-signed certificates, that are normally forbidden in pure HTTP/3. @@ -23,7 +23,7 @@ There are some server requirements: And there are some client (browser) requirements: -- If the server certificate is self-signed, browser must support the [serverCertificatesHashes option](https://caniuse.com/mdn-api_webtransport_webtransport_options_servercertificatehashes_parameter) (all except iOS safari do). +- If the server certificate is self-signed, browser must support the [serverCertificatesHashes option](https://caniuse.com/mdn-api_webtransport_webtransport_options_servercertificatehashes_parameter) (all except iOS Safari do). - Browser must support [WebTransport](https://caniuse.com/webtransport) and [WebCodecs](https://caniuse.com/webcodecs) (all modern browsers do) - When publishing tracks, the browser to support [MediaStreamTrackProcessor](https://caniuse.com/mdn-api_mediastreamtrackprocessor) (only Chrome does). diff --git a/docs/4-read/06-hls.md b/docs/4-read/06-hls.md index aefcc92f..fbb5e977 100644 --- a/docs/4-read/06-hls.md +++ b/docs/4-read/06-hls.md @@ -20,68 +20,6 @@ http://localhost:8888/mystream/index.m3u8 Some clients that can read with HLS are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md), [VLC](10-vlc.md) and [web browsers](07-web-browsers.md). -_MediaMTX_ supports generating HLS in several variants (including Low-Latency mode), and provides various parameters to tune HLS generation. These are listed in the [configuration file](../5-references/1-configuration-file.md): - -```yml -# Allow clients to read streams with the HLS protocol. -hls: true -# Address of the HLS listener. -hlsAddress: :8888 -# Enable HTTPS on the HLS server. -# This is required for Low-Latency HLS to function correctly on Apple devices. -hlsEncryption: false -# Path to the server key. This is needed only when encryption is yes. -# This can be generated with: -# openssl genrsa -out server.key 2048 -# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -hlsServerKey: server.key -# Path to the server certificate. -hlsServerCert: server.crt -# Allowed CORS origins. -# Supports wildcards: ['http://*.example.com'] -hlsAllowOrigins: ["*"] -# IPs or CIDRs of proxies placed before the HLS server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. -hlsTrustedProxies: [] -# By default, HLS is generated only when requested by a user. -# This option allows to generate it always, avoiding the delay between request and generation. -hlsAlwaysRemux: false -# Variant of the HLS protocol to use. Available options are: -# * mpegts - uses MPEG-TS segments, for maximum compatibility. -# * fmp4 - uses fragmented MP4 segments, more efficient. -# * lowLatency - uses Low-Latency HLS. -hlsVariant: lowLatency -# Number of HLS segments to keep on the server. -# Segments allow to seek through the stream. -# Their number doesn't influence latency. -hlsSegmentCount: 7 -# Minimum duration of each segment. -# A player usually puts 3 segments in a buffer before reproducing the stream. -# The final segment duration is also influenced by the interval between IDR frames, -# since the server changes the duration in order to include at least one IDR frame -# in each segment. -hlsSegmentDuration: 1s -# Minimum duration of each part. -# A player usually puts 3 parts in a buffer before reproducing the stream. -# Parts are used in Low-Latency HLS in place of segments. -# Part duration is influenced by the distance between video/audio samples -# and is adjusted in order to produce segments with a similar duration. -hlsPartDuration: 200ms -# Maximum size of each segment. -# This prevents RAM exhaustion. -hlsSegmentMaxSize: 50M -# Directory in which to save segments and non-low-latency playlists. -# This has two purposes: offloading RAM and creating a self-consistent directory -# that can be served by a CDN. -hlsDirectory: "" -# The muxer will be closed when there are no -# reader requests and this amount of time has passed. -hlsMuxerCloseAfter: 60s -# Secret to identify requests coming from a CDN. -# The CDN must insert this secret in every request in the -# 'Authorization: Bearer' header. -hlsCDNSecret: "" -``` +_MediaMTX_ supports generating HLS in several variants (including Low-Latency mode), and provides various parameters to tune HLS generation. These are listed in the [configuration file](../5-references/1-configuration-file.md). HLS can also be used to [scale the server](../2-features/20-scalability.md) through a CDN. diff --git a/internal/api/api.go b/internal/api/api.go index 5646b5ba..9ebf9518 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -257,6 +257,8 @@ func (a *API) middlewareAuth(ctx *gin.Context) { _, err := a.AuthManager.Authenticate(req) if err != nil { + auth.DelayBruteForce(err) + if err.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) @@ -265,9 +267,6 @@ func (a *API) middlewareAuth(ctx *gin.Context) { a.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } diff --git a/internal/auth/delay_brute_force.go b/internal/auth/delay_brute_force.go new file mode 100644 index 00000000..b37dd840 --- /dev/null +++ b/internal/auth/delay_brute_force.go @@ -0,0 +1,28 @@ +package auth + +import ( + "crypto/rand" + "errors" + "math/big" + "time" +) + +const ( + minPause = 0 * time.Second + maxPause = 4 * time.Second +) + +// DelayBruteForce delays brute force attacks by waiting some seconds after an authentication error. +func DelayBruteForce(err error) { + if terr, ok := errors.AsType[*Error](err); ok { + if !terr.AskCredentials { + var n *big.Int + n, err = rand.Int(rand.Reader, big.NewInt(int64(maxPause-minPause))) + if err != nil { + <-time.After(maxPause) + return + } + <-time.After(minPause + time.Duration(n.Int64())) + } + } +} diff --git a/internal/auth/manager.go b/internal/auth/manager.go index 0e8c75b2..f3bf922b 100644 --- a/internal/auth/manager.go +++ b/internal/auth/manager.go @@ -21,9 +21,6 @@ import ( ) const ( - // PauseAfterError is the pause to apply after an authentication failure. - PauseAfterError = 2 * time.Second - maxInboundBodySize = 128 * 1024 jwksRefreshPeriod = 60 * 60 * time.Second ) diff --git a/internal/core/path.go b/internal/core/path.go index ad04c696..2ac04811 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -1040,12 +1040,14 @@ func (pa *path) StaticSourceHandlerSetNotReady( } // describe is called by a reader or publisher through pathManager. -func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes { +func (pa *path) describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { select { case pa.chDescribe <- req: - return <-req.Res + res := <-req.Res + return &res, res.Err + case <-pa.ctx.Done(): - return defs.PathDescribeRes{Err: fmt.Errorf("terminated")} + return nil, fmt.Errorf("terminated") } } diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index dbd29d5e..7614e7b0 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -529,7 +529,13 @@ func (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin select { case pm.chFindPathConf <- req: res := <-req.Res - return &res, res.Err + + if res.Err != nil { + auth.DelayBruteForce(res.Err) + return nil, res.Err + } + + return &res, nil case <-pm.ctx.Done(): return nil, fmt.Errorf("terminated") @@ -537,25 +543,26 @@ func (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin } // Describe is called by a reader or publisher. -func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes { +func (pm *pathManager) Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { req.Res = make(chan defs.PathDescribeRes) select { case pm.chDescribe <- req: res1 := <-req.Res if res1.Err != nil { - return res1 + auth.DelayBruteForce(res1.Err) + return nil, res1.Err } - res2 := res1.Path.(*path).describe(req) - if res2.Err != nil { - return res2 + res2, err := res1.Path.(*path).describe(req) + if err != nil { + return nil, err } res2.Path = res1.Path - return res2 + return res2, nil case <-pm.ctx.Done(): - return defs.PathDescribeRes{Err: fmt.Errorf("terminated")} + return nil, fmt.Errorf("terminated") } } @@ -566,6 +573,7 @@ func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) (*defs.PathAdd case pm.chAddPublisher <- req: res1 := <-req.Res if res1.Err != nil { + auth.DelayBruteForce(res1.Err) return nil, res1.Err } @@ -591,6 +599,7 @@ func (pm *pathManager) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReader case pm.chAddReader <- req: res1 := <-req.Res if res1.Err != nil { + auth.DelayBruteForce(res1.Err) return nil, res1.Err } diff --git a/internal/core/path_manager_test.go b/internal/core/path_manager_test.go index cc1bec06..f9a18867 100644 --- a/internal/core/path_manager_test.go +++ b/internal/core/path_manager_test.go @@ -58,12 +58,12 @@ func TestPathManagerDynamicPathAutoDeletion(t *testing.T) { func() { if ca == "describe" { - res := pm.Describe(defs.PathDescribeReq{ + _, err := pm.Describe(defs.PathDescribeReq{ AccessRequest: defs.PathAccessRequest{ Name: "mypath", }, }) - require.EqualError(t, res.Err, "no stream is available on path 'mypath'") + require.EqualError(t, err, "no stream is available on path 'mypath'") } else { _, err := pm.AddReader(defs.PathAddReaderReq{ Author: &dummyReader{}, @@ -104,7 +104,7 @@ func TestPathManagerDynamicPathDescribeAndPublish(t *testing.T) { go func() { for range 10 { - pm.Describe(defs.PathDescribeReq{ + pm.Describe(defs.PathDescribeReq{ //nolint:errcheck AccessRequest: defs.PathAccessRequest{ Name: "mypath", }, diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index ea9e44de..62e42f84 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -198,6 +198,8 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) { _, err := m.AuthManager.Authenticate(req) if err != nil { + auth.DelayBruteForce(err) + if err.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) @@ -206,9 +208,6 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) { m.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } diff --git a/internal/playback/server.go b/internal/playback/server.go index 061fa051..621596e1 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -142,6 +142,8 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { _, err := s.AuthManager.Authenticate(req) if err != nil { + auth.DelayBruteForce(err) + if err.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) @@ -151,9 +153,6 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return false } diff --git a/internal/playback/server_test.go b/internal/playback/server_test.go index 7d719835..116b500e 100644 --- a/internal/playback/server_test.go +++ b/internal/playback/server_test.go @@ -107,14 +107,10 @@ func TestAuthError(t *testing.T) { req, err = http.NewRequest(http.MethodGet, u.String(), nil) require.NoError(t, err) - start := time.Now() - res, err = http.DefaultClient.Do(req) require.NoError(t, err) defer res.Body.Close() - require.Greater(t, time.Since(start), 2*time.Second) - require.Equal(t, http.StatusUnauthorized, res.StatusCode) require.Equal(t, 2, n) diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index 41a9c732..e353a255 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -118,6 +118,8 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) { _, err := pp.AuthManager.Authenticate(req) if err != nil { + auth.DelayBruteForce(err) + if err.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) @@ -126,9 +128,6 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) { pp.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index e58b00a6..2ad9570a 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -224,9 +224,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) { s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } @@ -343,9 +340,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) { s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } @@ -398,9 +392,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) { create: false, }) if err != nil { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } @@ -412,9 +403,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) { sx = muxer.findSession(ctx) } if sx == nil { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index 13d47db2..f2752dc2 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -645,14 +645,10 @@ func TestAuthError(t *testing.T) { req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/stream/index.m3u8", nil) require.NoError(t, err) - start := time.Now() - res, err = http.DefaultClient.Do(req) require.NoError(t, err) defer res.Body.Close() - require.Greater(t, time.Since(start), 2*time.Second) - require.Equal(t, http.StatusUnauthorized, res.StatusCode) require.Equal(t, 2, n) diff --git a/internal/servers/moq/http_server.go b/internal/servers/moq/http_server.go index cf16eec9..862ac442 100644 --- a/internal/servers/moq/http_server.go +++ b/internal/servers/moq/http_server.go @@ -191,9 +191,6 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return false } diff --git a/internal/servers/moq/session.go b/internal/servers/moq/session.go index d54aea18..51df53d5 100644 --- a/internal/servers/moq/session.go +++ b/internal/servers/moq/session.go @@ -358,11 +358,6 @@ func (s *session) onSubscribeCatalog(wstream *webtransport.Stream, m *controlmes }, }) if err != nil { - if _, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - } - var code controlmessage.RequestErrorCode if _, ok := errors.AsType[*auth.Error](err); ok { code = controlmessage.RequestErrorCodeUnauthorized @@ -595,11 +590,6 @@ func (s *session) onPublishCatalog(wstream *webtransport.Stream, m *controlmessa }, }) if err != nil { - if _, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - } - var code controlmessage.RequestErrorCode if _, ok := errors.AsType[*auth.Error](err); ok { code = controlmessage.RequestErrorCodeUnauthorized diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go index 581e9b68..03466a09 100644 --- a/internal/servers/rtmp/conn.go +++ b/internal/servers/rtmp/conn.go @@ -171,11 +171,6 @@ func (c *conn) runRead() error { }, }) if err != nil { - if terr, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - return terr - } return err } @@ -265,11 +260,6 @@ func (c *conn) runPublish() error { }, }) if err != nil { - if terr, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - return terr - } return err } diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go index caae2e44..b4d047b3 100644 --- a/internal/servers/rtsp/conn.go +++ b/internal/servers/rtsp/conn.go @@ -151,7 +151,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, } } - res := c.pathManager.Describe(defs.PathDescribeReq{ + res, err := c.pathManager.Describe(defs.PathDescribeReq{ AccessRequest: defs.PathAccessRequest{ Name: ctx.Path, Query: ctx.Query, @@ -162,22 +162,21 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, CustomVerifyFunc: customVerifyFunc, }, }) - - if res.Err != nil { - if terr, ok := errors.AsType[*auth.Error](res.Err); ok { + if err != nil { + if terr, ok := errors.AsType[*auth.Error](err); ok { res, err2 := c.handleAuthError(terr) return res, nil, err2 } - if _, ok := errors.AsType[*defs.PathNoStreamAvailableError](res.Err); ok { + if _, ok := errors.AsType[*defs.PathNoStreamAvailableError](err); ok { return &base.Response{ StatusCode: base.StatusNotFound, - }, nil, res.Err + }, nil, err } return &base.Response{ StatusCode: base.StatusBadRequest, - }, nil, res.Err + }, nil, err } if res.Redirect != "" { @@ -208,9 +207,6 @@ func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) { }, liberrors.ErrServerAuth{} } - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - return &base.Response{ StatusCode: base.StatusUnauthorized, }, err diff --git a/internal/servers/rtsp/server.go b/internal/servers/rtsp/server.go index efb07b2e..f6f31fce 100644 --- a/internal/servers/rtsp/server.go +++ b/internal/servers/rtsp/server.go @@ -77,7 +77,7 @@ type serverMetrics interface { type serverPathManager interface { FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) - Describe(req defs.PathDescribeReq) defs.PathDescribeRes + Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) AddPublisher(_ defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) AddReader(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) } diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index f16abc4b..57fa453c 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -376,7 +376,7 @@ func TestServerRead(t *testing.T) { n := 0 pathManager := &test.PathManager{ - DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { + DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "param=value", req.AccessRequest.Query) @@ -384,7 +384,7 @@ func TestServerRead(t *testing.T) { require.Nil(t, req.AccessRequest.CustomVerifyFunc) if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { - return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}} + return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")} } require.Equal(t, "myuser", req.AccessRequest.Credentials.User) @@ -394,16 +394,15 @@ func TestServerRead(t *testing.T) { if n == 0 { require.False(t, ok) n++ - return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}} + return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")} } require.True(t, ok) } - return defs.PathDescribeRes{ + return &defs.PathDescribeRes{ Path: &dummyPath{}, Stream: strm, - Err: nil, - } + }, nil }, AddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) { require.Equal(t, "teststream", req.AccessRequest.Name) @@ -552,20 +551,20 @@ func TestServerRedirect(t *testing.T) { require.NoError(t, err) pathManager := &test.PathManager{ - DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { + DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { if req.AccessRequest.Name == "path1" { if ca == "relative" { - return defs.PathDescribeRes{ + return &defs.PathDescribeRes{ Redirect: "/path2", - } + }, nil } - return defs.PathDescribeRes{ + return &defs.PathDescribeRes{ Redirect: "rtsp://localhost:8557/path2", - } + }, nil } if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { - return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}} + return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")} } require.Equal(t, "path2", req.AccessRequest.Name) @@ -573,10 +572,10 @@ func TestServerRedirect(t *testing.T) { require.Equal(t, "myuser", req.AccessRequest.Credentials.User) require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass) - return defs.PathDescribeRes{ + return &defs.PathDescribeRes{ Path: &dummyPath{}, Stream: strm, - } + }, nil }, } @@ -616,12 +615,12 @@ func TestServerRedirect(t *testing.T) { func TestAuthError(t *testing.T) { pathManager := &test.PathManager{ - DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { + DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" { - return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}} + return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")} } - return defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf("auth error")}} + return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")} }, } diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index e1348d8f..1e197d2e 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -149,8 +149,6 @@ func (c *conn) runPublish(streamID *streamID) error { }) if err != nil { if terr, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) c.connReq.Reject(srt.REJ_PEER) return terr } @@ -275,8 +273,6 @@ func (c *conn) runRead(streamID *streamID) error { }) if err != nil { if terr, ok := errors.AsType[*auth.Error](err); ok { - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) c.connReq.Reject(srt.REJ_PEER) return terr } diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 9fc5f27d..cab2a93d 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -158,9 +158,6 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped) - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return false } @@ -223,12 +220,6 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } - - s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped) - - // wait some seconds to delay brute force attacks - <-time.After(auth.PauseAfterError) - s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error")) return } diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index a0e242a4..5d83c23f 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -10,6 +10,7 @@ import ( "net/url" "reflect" "regexp" + "sync/atomic" "testing" "time" @@ -1160,7 +1161,7 @@ func TestAuthError(t *testing.T) { "whip post", } { t.Run(ca, func(t *testing.T) { - authFailed := false + var authFailed atomic.Bool s := &Server{ Address: "127.0.0.1:8886", @@ -1175,11 +1176,13 @@ func TestAuthError(t *testing.T) { return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")} }, }, - Parent: test.Logger(func(l logger.Level, s string, i ...any) { - if l == logger.Info { - if regexp.MustCompile("failed to authenticate: auth error$").MatchString(fmt.Sprintf(s, i...)) { - authFailed = true + Parent: test.Logger(func(_ logger.Level, s string, i ...any) { + if ca == "whip post" { + if regexp.MustCompile("authentication failed: auth error$").MatchString(fmt.Sprintf(s, i...)) { + authFailed.Store(true) } + } else if regexp.MustCompile("failed to authenticate: auth error$").MatchString(fmt.Sprintf(s, i...)) { + authFailed.Store(true) } }), } @@ -1250,17 +1253,13 @@ func TestAuthError(t *testing.T) { require.NoError(t, err) - start := time.Now() - res, err = http.DefaultClient.Do(req) require.NoError(t, err) defer res.Body.Close() - require.Greater(t, time.Since(start), 2*time.Second) - require.Equal(t, http.StatusUnauthorized, res.StatusCode) - require.True(t, authFailed) + require.True(t, authFailed.Load()) }) } } diff --git a/internal/test/path_manager.go b/internal/test/path_manager.go index b4326910..f035fb3b 100644 --- a/internal/test/path_manager.go +++ b/internal/test/path_manager.go @@ -7,7 +7,7 @@ import ( // PathManager is a dummy path manager. type PathManager struct { FindPathConfImpl func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) - DescribeImpl func(req defs.PathDescribeReq) defs.PathDescribeRes + DescribeImpl func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) AddPublisherImpl func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) AddReaderImpl func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) } @@ -18,7 +18,7 @@ func (pm *PathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin } // Describe implements PathManager. -func (pm *PathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes { +func (pm *PathManager) Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) { return pm.DescribeImpl(req) }