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.
597 lines
18 KiB
Go
597 lines
18 KiB
Go
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")
|
|
}
|
|
}
|