caddy-sip-guardian/l4handler.go
Ryan Malloy b5fa007d6e Add Caddyfile unmarshaler support for SIPMatcher and SIPHandler
The layer4 matchers and handlers must implement caddyfile.Unmarshaler
to be usable in Caddyfile syntax. This enables proper parsing of:
- @sip sip { methods ... } matchers
- sip_guardian { ... } handlers
2025-12-07 10:23:38 -07:00

253 lines
6.1 KiB
Go

package sipguardian
import (
"bytes"
"io"
"net"
"regexp"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/mholt/caddy-l4/layer4"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(SIPMatcher{})
caddy.RegisterModule(SIPHandler{})
}
// SIPMatcher matches SIP traffic by inspecting the first bytes
type SIPMatcher struct {
// Match specific SIP methods (REGISTER, INVITE, OPTIONS, etc.)
Methods []string `json:"methods,omitempty"`
methodRegex *regexp.Regexp
}
func (SIPMatcher) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "layer4.matchers.sip",
New: func() caddy.Module { return new(SIPMatcher) },
}
}
func (m *SIPMatcher) Provision(ctx caddy.Context) error {
if len(m.Methods) == 0 {
// Default: match common SIP methods
m.Methods = []string{"REGISTER", "INVITE", "OPTIONS", "ACK", "BYE", "CANCEL", "INFO", "NOTIFY", "SUBSCRIBE", "MESSAGE"}
}
// Build regex for matching SIP methods
pattern := "^(" + strings.Join(m.Methods, "|") + ") sip:"
m.methodRegex = regexp.MustCompile("(?i)" + pattern)
return nil
}
// Match returns true if the connection appears to be SIP traffic
func (m *SIPMatcher) Match(cx *layer4.Connection) (bool, error) {
// Peek at first 64 bytes to check for SIP signature
buf := make([]byte, 64)
n, err := io.ReadAtLeast(cx, buf, 8)
if err != nil && err != io.ErrUnexpectedEOF {
return false, nil
}
buf = buf[:n]
// Check if it matches a SIP method
if m.methodRegex.Match(buf) {
// Rewind the buffer for the handler
cx.SetVar("sip_peek", buf)
return true, nil
}
// Check for SIP response (starts with "SIP/2.0")
if bytes.HasPrefix(buf, []byte("SIP/2.0")) {
cx.SetVar("sip_peek", buf)
return true, nil
}
return false, nil
}
// SIPHandler is a Layer 4 handler that enforces SIP Guardian rules
type SIPHandler struct {
// Guardian reference (shared across handlers)
GuardianRef string `json:"guardian,omitempty"`
// Upstream address to proxy to
Upstream string `json:"upstream,omitempty"`
logger *zap.Logger
guardian *SIPGuardian
}
func (SIPHandler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "layer4.handlers.sip_guardian",
New: func() caddy.Module { return new(SIPHandler) },
}
}
func (h *SIPHandler) Provision(ctx caddy.Context) error {
h.logger = ctx.Logger()
// Get or create the guardian instance
// In a real implementation, this would use Caddy's module loading
// For now, we'll create a default instance
h.guardian = &SIPGuardian{}
if err := h.guardian.Provision(ctx); err != nil {
return err
}
return nil
}
// Handle processes the connection with SIP-aware protection
func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
remoteAddr := cx.RemoteAddr().String()
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
host = remoteAddr
}
// Check if IP is banned
if h.guardian.IsBanned(host) {
h.logger.Debug("Blocked banned IP", zap.String("ip", host))
return cx.Close()
}
// Check if IP is whitelisted - skip further checks
if h.guardian.IsWhitelisted(host) {
return next.Handle(cx)
}
// Get the peeked SIP data if available
if peekData := cx.GetVar("sip_peek"); peekData != nil {
buf := peekData.([]byte)
// Check for suspicious patterns
if isSuspiciousSIP(buf) {
h.logger.Warn("Suspicious SIP traffic detected",
zap.String("ip", host),
zap.ByteString("sample", buf[:min(32, len(buf))]),
)
banned := h.guardian.RecordFailure(host, "suspicious_sip_pattern")
if banned {
return cx.Close()
}
}
}
// Continue to next handler
return next.Handle(cx)
}
// isSuspiciousSIP checks for common attack patterns in SIP traffic
func isSuspiciousSIP(data []byte) bool {
s := string(data)
// Common scanner/attack patterns
suspiciousPatterns := []string{
"sipvicious",
"friendly-scanner",
"sipcli",
"sip-scan",
"User-Agent: Zoiper", // Often spoofed
"From: <sip:100@", // Common test extension
"From: <sip:1000@",
"To: <sip:100@",
}
lower := strings.ToLower(s)
for _, pattern := range suspiciousPatterns {
if strings.Contains(lower, strings.ToLower(pattern)) {
return true
}
}
return false
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler for SIPMatcher.
// Usage in Caddyfile:
//
// @sip sip {
// methods REGISTER INVITE OPTIONS
// }
//
// Or simply: @sip sip
func (m *SIPMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Move past "sip" token
d.Next()
// Check for block
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "methods":
m.Methods = d.RemainingArgs()
if len(m.Methods) == 0 {
return d.ArgErr()
}
default:
return d.Errf("unknown sip matcher directive: %s", d.Val())
}
}
return nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler for SIPHandler.
// Usage in Caddyfile:
//
// sip_guardian {
// max_failures 5
// find_time 10m
// ban_time 1h
// whitelist 10.0.0.0/8 172.16.0.0/12
// }
//
// Or simply: sip_guardian (uses defaults)
func (h *SIPHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Move past "sip_guardian" token
d.Next()
// The handler doesn't have its own configuration - it uses the shared SIPGuardian
// But we need to parse any configuration blocks that might be present
// Check for inline args or block
for nesting := d.Nesting(); d.NextBlock(nesting); {
// For now, SIPHandler delegates to SIPGuardian
// In future, handler-specific config could go here
switch d.Val() {
case "max_failures", "find_time", "ban_time", "whitelist":
// These are handled by the embedded SIPGuardian
// Skip to allow flexibility in config placement
d.RemainingArgs()
default:
return d.Errf("unknown sip_guardian directive: %s", d.Val())
}
}
return nil
}
// Interface guards
var (
_ layer4.ConnMatcher = (*SIPMatcher)(nil)
_ layer4.NextHandler = (*SIPHandler)(nil)
_ caddy.Provisioner = (*SIPMatcher)(nil)
_ caddy.Provisioner = (*SIPHandler)(nil)
_ caddyfile.Unmarshaler = (*SIPMatcher)(nil)
_ caddyfile.Unmarshaler = (*SIPHandler)(nil)
)