Initial commit: Caddy SIP Guardian module
Layer 4 SIP protection with: - SIP traffic matching (REGISTER, INVITE, etc.) - Rate limiting and automatic IP banning - Attack pattern detection (sipvicious, friendly-scanner) - CIDR whitelisting - Admin API for ban management
This commit is contained in:
commit
1ba05e160c
12 changed files with 1888 additions and 0 deletions
186
l4handler.go
Normal file
186
l4handler.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package sipguardian
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"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
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ layer4.ConnMatcher = (*SIPMatcher)(nil)
|
||||
_ layer4.NextHandler = (*SIPHandler)(nil)
|
||||
_ caddy.Provisioner = (*SIPMatcher)(nil)
|
||||
_ caddy.Provisioner = (*SIPHandler)(nil)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue