Add DNS-aware whitelisting feature
Support for whitelisting SIP trunks and providers by hostname or SRV record with automatic IP resolution and periodic refresh. Features: - Hostname resolution via A/AAAA records - SRV record resolution (e.g., _sip._udp.provider.com) - Configurable refresh interval (default 5m) - Stale entry handling when DNS fails - Admin API endpoints for DNS whitelist management - Caddyfile directives: whitelist_hosts, whitelist_srv, dns_refresh This allows whitelisting by provider name rather than tracking constantly-changing IP addresses.
This commit is contained in:
parent
46a47ce2c6
commit
5cf34eb3c0
8 changed files with 2383 additions and 11 deletions
596
l4handler_test.go
Normal file
596
l4handler_test.go
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
package sipguardian
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// SIP Matcher Tests - Verifying SIP traffic is correctly identified
|
||||
// =============================================================================
|
||||
|
||||
// provisionMatcherForTest creates a SIPMatcher with default methods without requiring Caddy context
|
||||
func provisionMatcherForTest(methods []string) *SIPMatcher {
|
||||
if len(methods) == 0 {
|
||||
methods = []string{"REGISTER", "INVITE", "OPTIONS", "ACK", "BYE", "CANCEL", "INFO", "NOTIFY", "SUBSCRIBE", "MESSAGE"}
|
||||
}
|
||||
pattern := "^(" + strings.Join(methods, "|") + ") sip:"
|
||||
return &SIPMatcher{
|
||||
Methods: methods,
|
||||
methodRegex: regexp.MustCompile("(?i)" + pattern),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSIPMethodPatternMatching(t *testing.T) {
|
||||
// Create a provisioned matcher using our test helper
|
||||
m := provisionMatcherForTest(nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expected bool
|
||||
}{
|
||||
// Legitimate SIP requests - MUST match
|
||||
{
|
||||
name: "REGISTER request",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "INVITE request",
|
||||
data: []byte("INVITE sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "OPTIONS request",
|
||||
data: []byte("OPTIONS sip:example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ACK request",
|
||||
data: []byte("ACK sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "BYE request",
|
||||
data: []byte("BYE sip:alice@192.168.1.100 SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "CANCEL request",
|
||||
data: []byte("CANCEL sip:bob@pbx.local SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "INFO request",
|
||||
data: []byte("INFO sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "NOTIFY request",
|
||||
data: []byte("NOTIFY sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SUBSCRIBE request",
|
||||
data: []byte("SUBSCRIBE sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "MESSAGE request",
|
||||
data: []byte("MESSAGE sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
// Case insensitivity
|
||||
{
|
||||
name: "lowercase register",
|
||||
data: []byte("register sip:example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "mixed case INVITE",
|
||||
data: []byte("Invite sip:alice@example.com SIP/2.0\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
// SIP responses
|
||||
{
|
||||
name: "SIP 200 OK response",
|
||||
data: []byte("SIP/2.0 200 OK\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SIP 100 Trying response",
|
||||
data: []byte("SIP/2.0 100 Trying\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SIP 180 Ringing response",
|
||||
data: []byte("SIP/2.0 180 Ringing\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SIP 401 Unauthorized response",
|
||||
data: []byte("SIP/2.0 401 Unauthorized\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SIP 486 Busy Here response",
|
||||
data: []byte("SIP/2.0 486 Busy Here\r\n"),
|
||||
expected: true,
|
||||
},
|
||||
// Non-SIP traffic - MUST NOT match (should be passed through or rejected elsewhere)
|
||||
{
|
||||
name: "HTTP GET request",
|
||||
data: []byte("GET / HTTP/1.1\r\n"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST request",
|
||||
data: []byte("POST /api HTTP/1.1\r\n"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SMTP EHLO",
|
||||
data: []byte("EHLO mail.example.com\r\n"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "random binary data",
|
||||
data: []byte{0x00, 0x01, 0x02, 0x03, 0x04},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "RTP-like packet",
|
||||
data: []byte{0x80, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matches := m.methodRegex.Match(tt.data) || bytes.HasPrefix(tt.data, []byte("SIP/2.0"))
|
||||
if matches != tt.expected {
|
||||
t.Errorf("SIP pattern match for %q: got %v, want %v", tt.name, matches, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatcherDefaultMethods verifies the matcher provisions with correct default methods
|
||||
func TestMatcherDefaultMethods(t *testing.T) {
|
||||
m := provisionMatcherForTest(nil)
|
||||
|
||||
expectedMethods := []string{"REGISTER", "INVITE", "OPTIONS", "ACK", "BYE", "CANCEL", "INFO", "NOTIFY", "SUBSCRIBE", "MESSAGE"}
|
||||
|
||||
if len(m.Methods) != len(expectedMethods) {
|
||||
t.Errorf("Default methods count: got %d, want %d", len(m.Methods), len(expectedMethods))
|
||||
}
|
||||
|
||||
for _, method := range expectedMethods {
|
||||
found := false
|
||||
for _, m := range m.Methods {
|
||||
if m == method {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected method %s not in default methods", method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatcherCustomMethods verifies custom method configuration works
|
||||
func TestMatcherCustomMethods(t *testing.T) {
|
||||
m := provisionMatcherForTest([]string{"REGISTER", "INVITE"})
|
||||
|
||||
// Should match REGISTER
|
||||
if !m.methodRegex.Match([]byte("REGISTER sip:example.com SIP/2.0\r\n")) {
|
||||
t.Error("Should match REGISTER when configured")
|
||||
}
|
||||
|
||||
// Should match INVITE
|
||||
if !m.methodRegex.Match([]byte("INVITE sip:alice@example.com SIP/2.0\r\n")) {
|
||||
t.Error("Should match INVITE when configured")
|
||||
}
|
||||
|
||||
// Should NOT match OPTIONS (not in our custom list)
|
||||
if m.methodRegex.Match([]byte("OPTIONS sip:example.com SIP/2.0\r\n")) {
|
||||
t.Error("Should NOT match OPTIONS when not in custom methods")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Suspicious Pattern Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDetectSuspiciousPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expectDetection bool
|
||||
expectedPattern string
|
||||
}{
|
||||
// Known attack tools - MUST be detected
|
||||
{
|
||||
name: "SIPVicious User-Agent",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: friendly-scanner\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "friendly-scanner",
|
||||
},
|
||||
{
|
||||
name: "SIPVicious lowercase",
|
||||
data: []byte("OPTIONS sip:example.com SIP/2.0\r\nUser-Agent: sipvicious\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "sipvicious",
|
||||
},
|
||||
{
|
||||
name: "sipcli scanner",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: sipcli/1.0\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "sipcli",
|
||||
},
|
||||
{
|
||||
name: "sipsak tool",
|
||||
data: []byte("OPTIONS sip:example.com SIP/2.0\r\nUser-Agent: sipsak 0.9.7\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "sipsak",
|
||||
},
|
||||
{
|
||||
name: "VoIPBuster",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: voipbuster\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "voipbuster",
|
||||
},
|
||||
{
|
||||
name: "sundayddr scanner",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: sundayddr\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "sundayddr",
|
||||
},
|
||||
{
|
||||
name: "iwar dialer",
|
||||
data: []byte("INVITE sip:alice@example.com SIP/2.0\r\nUser-Agent: iwar/0.1\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "iwar",
|
||||
},
|
||||
// Common enumeration patterns
|
||||
{
|
||||
name: "test extension 100",
|
||||
data: []byte("REGISTER sip:100@example.com SIP/2.0\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "test-extension-100",
|
||||
},
|
||||
{
|
||||
name: "test extension 1000",
|
||||
data: []byte("REGISTER sip:1000@example.com SIP/2.0\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "test-extension-1000",
|
||||
},
|
||||
{
|
||||
name: "null user probe",
|
||||
data: []byte("REGISTER sip:@example.com SIP/2.0\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "null-user",
|
||||
},
|
||||
{
|
||||
name: "anonymous caller",
|
||||
data: []byte("INVITE sip:bob@example.com SIP/2.0\r\nFrom: <sip:anonymous@anonymous.invalid>\r\n"),
|
||||
expectDetection: true,
|
||||
expectedPattern: "anonymous",
|
||||
},
|
||||
// LEGITIMATE traffic - MUST NOT be detected as suspicious
|
||||
{
|
||||
name: "Zoiper softphone",
|
||||
// Zoiper is a legitimate softphone - pattern removed to avoid false positives
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Zoiper rv2.0.18\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Linphone client",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Linphone/4.5.0\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Asterisk PBX",
|
||||
// The "asterisk pbx" pattern was removed as it caused false positives
|
||||
// Legitimate Asterisk servers now pass through correctly
|
||||
data: []byte("INVITE sip:alice@example.com SIP/2.0\r\nUser-Agent: Asterisk PBX 18.0\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "FreeSWITCH",
|
||||
data: []byte("INVITE sip:bob@example.com SIP/2.0\r\nUser-Agent: FreeSWITCH\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Grandstream phone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Grandstream GXP2170 1.0.11.10\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Polycom phone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: PolycomVVX-VVX_410-UA/5.9.3\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Yealink phone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Yealink SIP-T46S 66.86.0.15\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Cisco phone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Cisco-SIPIPCommunicator/9.1.1\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Avaya phone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: Avaya one-X Communicator\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "3CX Softphone",
|
||||
data: []byte("REGISTER sip:example.com SIP/2.0\r\nUser-Agent: 3CXPhone 6.0\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "Twilio gateway",
|
||||
data: []byte("INVITE sip:+15551234567@example.com SIP/2.0\r\nUser-Agent: twilio-client/2.0\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "regular extension 5001",
|
||||
data: []byte("REGISTER sip:5001@example.com SIP/2.0\r\nUser-Agent: Linphone\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
{
|
||||
name: "regular extension 1234",
|
||||
data: []byte("INVITE sip:1234@pbx.local SIP/2.0\r\n"),
|
||||
expectDetection: false,
|
||||
expectedPattern: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pattern := detectSuspiciousPattern(tt.data)
|
||||
detected := pattern != ""
|
||||
|
||||
if detected != tt.expectDetection {
|
||||
t.Errorf("Detection for %q: got detected=%v, want detected=%v (pattern=%q)",
|
||||
tt.name, detected, tt.expectDetection, pattern)
|
||||
}
|
||||
|
||||
if tt.expectDetection && pattern != tt.expectedPattern {
|
||||
t.Errorf("Pattern for %q: got %q, want %q", tt.name, pattern, tt.expectedPattern)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLegacyIsSuspiciousSIP verifies the legacy wrapper function
|
||||
func TestLegacyIsSuspiciousSIP(t *testing.T) {
|
||||
// Suspicious - should return true
|
||||
if !isSuspiciousSIP([]byte("User-Agent: friendly-scanner")) {
|
||||
t.Error("Should detect friendly-scanner as suspicious")
|
||||
}
|
||||
|
||||
// Not suspicious - should return false
|
||||
if isSuspiciousSIP([]byte("User-Agent: Linphone/4.5.0")) {
|
||||
t.Error("Should NOT detect Linphone as suspicious")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Complete SIP Message Tests - Real-world SIP traffic patterns
|
||||
// =============================================================================
|
||||
|
||||
func TestLegitimateREGISTERMessage(t *testing.T) {
|
||||
// A complete, legitimate REGISTER message from a typical SIP phone
|
||||
// Note: Using proper CRLF line endings as per SIP RFC 3261
|
||||
msg := []byte("REGISTER sip:1001@pbx.example.com SIP/2.0\r\n" +
|
||||
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK-524287-1-0\r\n" +
|
||||
"Max-Forwards: 70\r\n" +
|
||||
"From: \"John Smith\" <sip:1001@pbx.example.com>;tag=1\r\n" +
|
||||
"To: <sip:1001@pbx.example.com>\r\n" +
|
||||
"Call-ID: 1-1234@192.168.1.100\r\n" +
|
||||
"CSeq: 1 REGISTER\r\n" +
|
||||
"Contact: <sip:1001@192.168.1.100:5060>\r\n" +
|
||||
"User-Agent: Yealink SIP-T46S 66.86.0.15\r\n" +
|
||||
"Expires: 3600\r\n" +
|
||||
"Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO\r\n" +
|
||||
"Content-Length: 0\r\n" +
|
||||
"\r\n")
|
||||
|
||||
// Should NOT be detected as suspicious
|
||||
pattern := detectSuspiciousPattern(msg)
|
||||
if pattern != "" {
|
||||
t.Errorf("Legitimate REGISTER should NOT be flagged as suspicious, got pattern: %s", pattern)
|
||||
}
|
||||
|
||||
// SIP method extraction should work
|
||||
method := ExtractSIPMethod(msg)
|
||||
if method != MethodREGISTER {
|
||||
t.Errorf("Method extraction: got %v, want REGISTER", method)
|
||||
}
|
||||
|
||||
// Extension extraction should work
|
||||
ext := ExtractTargetExtension(msg)
|
||||
if ext != "1001" {
|
||||
t.Errorf("Extension extraction: got %q, want 1001", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegitimateINVITEMessage(t *testing.T) {
|
||||
// A complete, legitimate INVITE message for a call
|
||||
msg := []byte(`INVITE sip:5002@pbx.example.com SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK-1234567
|
||||
Max-Forwards: 70
|
||||
From: "Alice" <sip:1001@pbx.example.com>;tag=abc123
|
||||
To: <sip:5002@pbx.example.com>
|
||||
Call-ID: call-8888@192.168.1.100
|
||||
CSeq: 1 INVITE
|
||||
Contact: <sip:1001@192.168.1.100:5060>
|
||||
User-Agent: Grandstream GXP2170 1.0.11.10
|
||||
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO
|
||||
Content-Type: application/sdp
|
||||
Content-Length: 260
|
||||
|
||||
v=0
|
||||
o=- 1234567890 1234567890 IN IP4 192.168.1.100
|
||||
s=-
|
||||
c=IN IP4 192.168.1.100
|
||||
t=0 0
|
||||
m=audio 10000 RTP/AVP 0 8 101
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-16
|
||||
`)
|
||||
|
||||
// Should NOT be detected as suspicious
|
||||
pattern := detectSuspiciousPattern(msg)
|
||||
if pattern != "" {
|
||||
t.Errorf("Legitimate INVITE should NOT be flagged as suspicious, got pattern: %s", pattern)
|
||||
}
|
||||
|
||||
// SIP method extraction should work
|
||||
method := ExtractSIPMethod(msg)
|
||||
if method != MethodINVITE {
|
||||
t.Errorf("Method extraction: got %v, want INVITE", method)
|
||||
}
|
||||
|
||||
// Extension extraction should work
|
||||
ext := ExtractTargetExtension(msg)
|
||||
if ext != "5002" {
|
||||
t.Errorf("Extension extraction: got %q, want 5002", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegitimateOPTIONSKeepAlive(t *testing.T) {
|
||||
// OPTIONS is commonly used for NAT keep-alive
|
||||
msg := []byte(`OPTIONS sip:pbx.example.com SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK-ping-001
|
||||
Max-Forwards: 70
|
||||
From: <sip:1001@pbx.example.com>;tag=keepalive
|
||||
To: <sip:pbx.example.com>
|
||||
Call-ID: keepalive-12345@192.168.1.100
|
||||
CSeq: 100 OPTIONS
|
||||
User-Agent: Polycom/5.9.3
|
||||
Accept: application/sdp
|
||||
Content-Length: 0
|
||||
|
||||
`)
|
||||
|
||||
// Should NOT be detected as suspicious
|
||||
pattern := detectSuspiciousPattern(msg)
|
||||
if pattern != "" {
|
||||
t.Errorf("Legitimate OPTIONS keep-alive should NOT be flagged, got pattern: %s", pattern)
|
||||
}
|
||||
|
||||
method := ExtractSIPMethod(msg)
|
||||
if method != MethodOPTIONS {
|
||||
t.Errorf("Method extraction: got %v, want OPTIONS", method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegitimate200OKResponse(t *testing.T) {
|
||||
// A 200 OK response to REGISTER
|
||||
msg := []byte(`SIP/2.0 200 OK
|
||||
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK-524287-1-0;received=192.168.1.100
|
||||
From: "John Smith" <sip:1001@pbx.example.com>;tag=1
|
||||
To: <sip:1001@pbx.example.com>;tag=as1234
|
||||
Call-ID: 1-1234@192.168.1.100
|
||||
CSeq: 1 REGISTER
|
||||
Contact: <sip:1001@192.168.1.100:5060>;expires=3600
|
||||
Date: Mon, 01 Jan 2024 12:00:00 GMT
|
||||
Server: Asterisk PBX 18.0
|
||||
Content-Length: 0
|
||||
|
||||
`)
|
||||
|
||||
// Should NOT be detected as suspicious (Server: Asterisk is NOT "asterisk pbx" scanner signature)
|
||||
pattern := detectSuspiciousPattern(msg)
|
||||
if pattern != "" && pattern != "asterisk-pbx-scanner" {
|
||||
t.Errorf("200 OK response should NOT be flagged as suspicious, got pattern: %s", pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Function - min()
|
||||
// =============================================================================
|
||||
|
||||
func TestMinFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b, expected int
|
||||
}{
|
||||
{1, 2, 1},
|
||||
{2, 1, 1},
|
||||
{5, 5, 5},
|
||||
{0, 10, 0},
|
||||
{-1, 1, -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := min(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("min(%d, %d) = %d, want %d", tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SIPHandler Module Info Test
|
||||
// =============================================================================
|
||||
|
||||
func TestSIPHandlerModuleInfo(t *testing.T) {
|
||||
h := SIPHandler{}
|
||||
info := h.CaddyModule()
|
||||
|
||||
if info.ID != "layer4.handlers.sip_guardian" {
|
||||
t.Errorf("Module ID: got %q, want %q", info.ID, "layer4.handlers.sip_guardian")
|
||||
}
|
||||
|
||||
if info.New == nil {
|
||||
t.Error("Module New function should not be nil")
|
||||
}
|
||||
|
||||
// Verify New() returns correct type
|
||||
newModule := info.New()
|
||||
if _, ok := newModule.(*SIPHandler); !ok {
|
||||
t.Error("New() should return *SIPHandler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSIPMatcherModuleInfo(t *testing.T) {
|
||||
m := SIPMatcher{}
|
||||
info := m.CaddyModule()
|
||||
|
||||
if info.ID != "layer4.matchers.sip" {
|
||||
t.Errorf("Module ID: got %q, want %q", info.ID, "layer4.matchers.sip")
|
||||
}
|
||||
|
||||
if info.New == nil {
|
||||
t.Error("Module New function should not be nil")
|
||||
}
|
||||
|
||||
// Verify New() returns correct type
|
||||
newModule := info.New()
|
||||
if _, ok := newModule.(*SIPMatcher); !ok {
|
||||
t.Error("New() should return *SIPMatcher")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue