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
253 lines
6.1 KiB
Go
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)
|
|
)
|