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: