fix: increment limit, added integration tests

This commit is contained in:
Brendan Le Glaunec
2026-01-28 21:22:57 +01:00
parent 1f06a075e4
commit d589b610b2
3 changed files with 146 additions and 59 deletions
+2 -1
View File
@@ -420,6 +420,8 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context,
case <-time.After(a.attackInterval): case <-time.After(a.attackInterval):
} }
attempts++
nextRoute := buildIncrementedRoute(match, nextNumber) nextRoute := buildIncrementedRoute(match, nextNumber)
if slices.Contains(target.Routes, nextRoute) { if slices.Contains(target.Routes, nextRoute) {
if !match.isChannel { if !match.isChannel {
@@ -443,7 +445,6 @@ func (a Attacker) tryIncrementalRoutes(ctx context.Context,
)) ))
return target, nil return target, nil
} }
attempts++
if !ok { if !ok {
return target, nil return target, nil
} }
+109 -32
View File
@@ -1,6 +1,7 @@
package attack_test package attack_test
import ( import (
"fmt"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -50,11 +51,11 @@ func TestNew(t *testing.T) {
func TestAttacker_Attack_BasicAuth(t *testing.T) { func TestAttacker_Attack_BasicAuth(t *testing.T) {
addr, port := startRTSPServer(t, rtspServerConfig{ addr, port := startRTSPServer(t, rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: true, requireAuth: true,
username: "user", username: "user",
password: "pass", password: "pass",
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
}) })
dict := testDictionary{ dict := testDictionary{
@@ -101,9 +102,9 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) {
{ {
name: "no authentication", name: "no authentication",
config: rtspServerConfig{ config: rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: false, requireAuth: false,
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
}, },
dict: testDictionary{ dict: testDictionary{
routes: []string{"stream"}, routes: []string{"stream"},
@@ -117,11 +118,11 @@ func TestAttacker_Attack_AuthVariants(t *testing.T) {
{ {
name: "digest authentication", name: "digest authentication",
config: rtspServerConfig{ config: rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: true, requireAuth: true,
username: "user", username: "user",
password: "pass", password: "pass",
authMethod: headers.AuthMethodDigest, authMethod: headers.AuthMethodDigest,
}, },
dict: testDictionary{ dict: testDictionary{
routes: []string{"stream"}, routes: []string{"stream"},
@@ -193,9 +194,9 @@ func TestAttacker_Attack_ValidationErrors(t *testing.T) {
func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) { func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
addr, port := startRTSPServer(t, rtspServerConfig{ addr, port := startRTSPServer(t, rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: false, requireAuth: false,
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
}) })
dict := testDictionary{ dict := testDictionary{
@@ -221,11 +222,11 @@ func TestAttacker_Attack_ReturnsErrorWhenRouteMissing(t *testing.T) {
func TestAttacker_Attack_ReturnsErrorWhenCredentialsMissing(t *testing.T) { func TestAttacker_Attack_ReturnsErrorWhenCredentialsMissing(t *testing.T) {
addr, port := startRTSPServer(t, rtspServerConfig{ addr, port := startRTSPServer(t, rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: true, requireAuth: true,
username: "user", username: "user",
password: "pass", password: "pass",
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
}) })
dict := testDictionary{ dict := testDictionary{
@@ -254,12 +255,12 @@ func TestAttacker_Attack_CredentialAttemptFails(t *testing.T) {
reporter := &recordingReporter{} reporter := &recordingReporter{}
addr, port := startRTSPServer(t, rtspServerConfig{ addr, port := startRTSPServer(t, rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: true, requireAuth: true,
username: "user", username: "user",
password: "pass", password: "pass",
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
failOnAuth: true, failOnAuth: true,
}) })
dict := testDictionary{ dict := testDictionary{
@@ -310,10 +311,10 @@ func TestAttacker_Attack_AllowsDummyRoute(t *testing.T) {
func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) { func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) {
addr, port := startRTSPServer(t, rtspServerConfig{ addr, port := startRTSPServer(t, rtspServerConfig{
allowedRoute: "stream", allowRoutes: []string{"stream"},
requireAuth: false, requireAuth: false,
authMethod: headers.AuthMethodBasic, authMethod: headers.AuthMethodBasic,
setupStatus: base.StatusUnsupportedTransport, setupStatus: base.StatusUnsupportedTransport,
}) })
dict := testDictionary{ dict := testDictionary{
@@ -335,6 +336,71 @@ func TestAttacker_Attack_ValidationFailsWhenSetupErrors(t *testing.T) {
assert.True(t, got[0].RouteFound) assert.True(t, got[0].RouteFound)
} }
func TestAttacker_Attack_IncrementalRoutesStopsOnFirstMissAndAvoidsDuplicates(t *testing.T) {
addr, port := startRTSPServer(t, rtspServerConfig{
allowRoutes: []string{"channel1", "channel2"},
requireAuth: false,
authMethod: headers.AuthMethodBasic,
})
dict := testDictionary{
routes: []string{"channel1", "channel2"},
usernames: []string{"user"},
passwords: []string{"pass"},
}
attacker, err := attack.New(dict, 0, time.Second, ui.NopReporter{})
require.NoError(t, err)
streams := []cameradar.Stream{{
Address: addr,
Port: port,
}}
got, err := attacker.Attack(t.Context(), streams)
require.NoError(t, err)
require.Len(t, got, 1)
assert.ElementsMatch(t, []string{"channel1", "channel2"}, got[0].Routes)
assert.Equal(t, 1, countRoute(got[0].Routes, "channel2"))
}
func TestAttacker_Attack_IncrementalRoutesStopsAtCap(t *testing.T) {
allowedRoutes := make([]string, 0, 50)
for i := 1; i <= 50; i++ {
allowedRoutes = append(allowedRoutes, fmt.Sprintf("channel%d", i))
}
addr, port := startRTSPServer(t, rtspServerConfig{
allowRoutes: allowedRoutes,
requireAuth: false,
authMethod: headers.AuthMethodBasic,
})
dict := testDictionary{
routes: []string{"channel1"},
usernames: []string{"user"},
passwords: []string{"pass"},
}
attacker, err := attack.New(dict, 0, time.Second, ui.NopReporter{})
require.NoError(t, err)
streams := []cameradar.Stream{{
Address: addr,
Port: port,
}}
got, err := attacker.Attack(t.Context(), streams)
require.NoError(t, err)
require.Len(t, got, 1)
const expectedRoutes = 33 // channel1 + 32 incremental attempts
assert.Len(t, got[0].Routes, expectedRoutes)
assert.Contains(t, got[0].Routes, "channel33")
assert.NotContains(t, got[0].Routes, "channel34")
}
type testDictionary struct { type testDictionary struct {
routes []string routes []string
usernames []string usernames []string
@@ -376,9 +442,10 @@ func (r *recordingReporter) Summary([]cameradar.Stream, error) {}
func (r *recordingReporter) Close() {} func (r *recordingReporter) Close() {}
func (r *recordingReporter) HasDebugContaining(value string) bool { func (r *recordingReporter) ContainsDebug(value string) bool {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
for _, message := range r.debugMessages { for _, message := range r.debugMessages {
if strings.Contains(message, value) { if strings.Contains(message, value) {
return true return true
@@ -386,3 +453,13 @@ func (r *recordingReporter) HasDebugContaining(value string) bool {
} }
return false return false
} }
func countRoute(routes []string, route string) int {
count := 0
for _, value := range routes {
if value == route {
count++
}
}
return count
}
+35 -26
View File
@@ -18,27 +18,27 @@ import (
) )
type rtspServerConfig struct { type rtspServerConfig struct {
allowAll bool allowAll bool
allowedRoute string allowRoutes []string
requireAuth bool requireAuth bool
username string username string
password string password string
authMethod headers.AuthMethod authMethod headers.AuthMethod
authHeader base.HeaderValue authHeader base.HeaderValue
failOnAuth bool failOnAuth bool
setupStatus base.StatusCode setupStatus base.StatusCode
} }
type testServerHandler struct { type testServerHandler struct {
stream *gortsplib.ServerStream stream *gortsplib.ServerStream
allowAll bool allowAll bool
allowedRoute string allowRoutes []string
requireAuth bool requireAuth bool
username string username string
password string password string
authHeader base.HeaderValue authHeader base.HeaderValue
failOnAuth bool failOnAuth bool
setupStatus base.StatusCode setupStatus base.StatusCode
} }
func (h *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { func (h *testServerHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
@@ -86,20 +86,29 @@ func (h *testServerHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*ba
func (h *testServerHandler) routeAllowed(path string) bool { func (h *testServerHandler) routeAllowed(path string) bool {
path = strings.TrimLeft(path, "/") path = strings.TrimLeft(path, "/")
return h.allowAll || path == h.allowedRoute if h.allowAll {
return true
}
for _, route := range h.allowRoutes {
if path == route {
return true
}
}
return false
} }
func startRTSPServer(t *testing.T, cfg rtspServerConfig) (netip.Addr, uint16) { func startRTSPServer(t *testing.T, cfg rtspServerConfig) (netip.Addr, uint16) {
t.Helper() t.Helper()
handler := &testServerHandler{ handler := &testServerHandler{
allowAll: cfg.allowAll, allowAll: cfg.allowAll,
allowedRoute: cfg.allowedRoute, allowRoutes: cfg.allowRoutes,
requireAuth: cfg.requireAuth, requireAuth: cfg.requireAuth,
username: cfg.username, username: cfg.username,
password: cfg.password, password: cfg.password,
failOnAuth: cfg.failOnAuth, failOnAuth: cfg.failOnAuth,
setupStatus: cfg.setupStatus, setupStatus: cfg.setupStatus,
} }
if len(cfg.authHeader) > 0 { if len(cfg.authHeader) > 0 {