package sipguardian import ( "fmt" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) func init() { caddy.RegisterModule(MetricsHandler{}) httpcaddyfile.RegisterHandlerDirective("sip_guardian_metrics", parseSIPGuardianMetrics) httpcaddyfile.RegisterDirectiveOrder("sip_guardian_metrics", httpcaddyfile.Before, "respond") } // Prometheus metrics for SIP Guardian var ( sipConnectionsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "connections_total", Help: "Total number of SIP connections processed", }, []string{"status"}, // "allowed", "blocked", "suspicious" ) sipBansTotal = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "bans_total", Help: "Total number of IP bans issued", }, ) sipUnbansTotal = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "unbans_total", Help: "Total number of IP unbans (manual or expired)", }, ) sipActiveBans = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "sip_guardian", Name: "active_bans", Help: "Current number of active IP bans", }, ) sipFailuresTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "failures_total", Help: "Total number of recorded failures by reason", }, []string{"reason"}, ) sipTrackedIPs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "sip_guardian", Name: "tracked_ips", Help: "Current number of IPs being tracked for failures", }, ) sipWhitelistedConnections = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "whitelisted_connections_total", Help: "Total connections from whitelisted IPs", }, ) sipSuspiciousPatterns = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "suspicious_patterns_total", Help: "Total suspicious patterns detected by type", }, []string{"pattern"}, ) sipBanDurationSeconds = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: "sip_guardian", Name: "ban_duration_seconds", Help: "Distribution of ban durations in seconds", Buckets: []float64{60, 300, 600, 1800, 3600, 7200, 14400, 28800, 86400}, }, ) // Enumeration detection metrics sipEnumerationDetections = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "enumeration_detections_total", Help: "Total enumeration attacks detected by reason", }, []string{"reason"}, // "extension_count_exceeded", "sequential_enumeration", "rapid_fire_enumeration" ) sipEnumerationTrackedIPs = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "sip_guardian", Name: "enumeration_tracked_ips", Help: "Current number of IPs being tracked for enumeration", }, ) sipEnumerationUniqueExtensions = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: "sip_guardian", Name: "enumeration_unique_extensions", Help: "Distribution of unique extensions per IP at detection time", Buckets: []float64{5, 10, 15, 20, 30, 50, 100}, }, ) // Validation metrics sipValidationViolations = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "validation_violations_total", Help: "Total validation violations detected by rule", }, []string{"rule"}, // "null_bytes", "binary_injection", "missing_via", etc. ) sipValidationResults = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "sip_guardian", Name: "validation_results_total", Help: "Total validation results by outcome", }, []string{"result"}, // "valid", "invalid", "ban" ) sipMessageSizeBytes = prometheus.NewHistogram( prometheus.HistogramOpts{ Namespace: "sip_guardian", Name: "message_size_bytes", Help: "Distribution of SIP message sizes in bytes", Buckets: []float64{100, 500, 1000, 2000, 5000, 10000, 20000, 50000, 65535}, }, ) ) // RegisterMetrics registers all SIP Guardian metrics with Prometheus // It's safe to call multiple times - already registered metrics are silently ignored func RegisterMetrics() { collectors := []prometheus.Collector{ sipConnectionsTotal, sipBansTotal, sipUnbansTotal, sipActiveBans, sipFailuresTotal, sipTrackedIPs, sipWhitelistedConnections, sipSuspiciousPatterns, sipBanDurationSeconds, sipEnumerationDetections, sipEnumerationTrackedIPs, sipEnumerationUniqueExtensions, sipValidationViolations, sipValidationResults, sipMessageSizeBytes, } for _, collector := range collectors { if err := prometheus.Register(collector); err != nil { // Check if already registered - this is expected on config reload if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { // Unexpected error - log it but don't panic // Metrics will still work, just might not be exported continue } // Already registered is fine - metrics are global and shared } } } // Metric recording functions - called from other modules // RecordConnection records a connection event func RecordConnection(status string) { sipConnectionsTotal.WithLabelValues(status).Inc() } // RecordBan records a ban event func RecordBan() { sipBansTotal.Inc() sipActiveBans.Inc() } // RecordUnban records an unban event func RecordUnban() { sipUnbansTotal.Inc() sipActiveBans.Dec() } // RecordFailure records a failure event func RecordFailure(reason string) { sipFailuresTotal.WithLabelValues(reason).Inc() } // RecordWhitelistedConnection records a whitelisted connection func RecordWhitelistedConnection() { sipWhitelistedConnections.Inc() } // RecordSuspiciousPattern records a suspicious pattern detection func RecordSuspiciousPattern(pattern string) { sipSuspiciousPatterns.WithLabelValues(pattern).Inc() } // RecordBanDuration records the duration of a ban when it expires func RecordBanDuration(seconds float64) { sipBanDurationSeconds.Observe(seconds) } // UpdateActiveBans updates the active bans gauge func UpdateActiveBans(count int) { sipActiveBans.Set(float64(count)) } // UpdateTrackedIPs updates the tracked IPs gauge func UpdateTrackedIPs(count int) { sipTrackedIPs.Set(float64(count)) } // RecordEnumerationDetection records an enumeration attack detection func RecordEnumerationDetection(reason string) { sipEnumerationDetections.WithLabelValues(reason).Inc() } // UpdateEnumerationTrackedIPs updates the enumeration tracked IPs gauge func UpdateEnumerationTrackedIPs(count int) { sipEnumerationTrackedIPs.Set(float64(count)) } // RecordEnumerationExtensions records the number of unique extensions at detection func RecordEnumerationExtensions(count int) { sipEnumerationUniqueExtensions.Observe(float64(count)) } // RecordValidationViolation records a validation violation by rule name func RecordValidationViolation(rule string) { sipValidationViolations.WithLabelValues(rule).Inc() } // RecordValidationResult records a validation result (valid, invalid, ban) func RecordValidationResult(result string) { sipValidationResults.WithLabelValues(result).Inc() } // RecordMessageSize records the size of a SIP message in bytes func RecordMessageSize(bytes int) { sipMessageSizeBytes.Observe(float64(bytes)) } // MetricsHandler provides a Prometheus metrics endpoint for SIP Guardian type MetricsHandler struct { // Path prefix for metrics (default: /metrics) Path string `json:"path,omitempty"` // app is the SIP Guardian app instance (set during provision) app *SIPGuardianApp } func (MetricsHandler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "http.handlers.sip_guardian_metrics", New: func() caddy.Module { return new(MetricsHandler) }, } } func (h *MetricsHandler) Provision(ctx caddy.Context) error { RegisterMetrics() if h.Path == "" { h.Path = "/metrics" } // Get the SIP Guardian app from Caddy's app system appIface, err := ctx.App("sip_guardian") if err != nil { return fmt.Errorf("failed to get sip_guardian app: %w", err) } app, ok := appIface.(*SIPGuardianApp) if !ok { return fmt.Errorf("sip_guardian app has wrong type: %T", appIface) } h.app = app return nil } // ServeHTTP serves the Prometheus metrics func (h *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { // Update gauges from current state (use app, not global registry) if h.app != nil { if guardian := h.app.GetGuardian("default"); guardian != nil { stats := guardian.GetStats() if activeBans, ok := stats["active_bans"].(int); ok { UpdateActiveBans(activeBans) } if trackedFailures, ok := stats["tracked_failures"].(int); ok { UpdateTrackedIPs(trackedFailures) } } } promhttp.Handler().ServeHTTP(w, r) return nil } // parseSIPGuardianMetrics parses the sip_guardian_metrics directive func parseSIPGuardianMetrics(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { var handler MetricsHandler err := handler.UnmarshalCaddyfile(h.Dispenser) return &handler, err } // UnmarshalCaddyfile implements caddyfile.Unmarshaler for MetricsHandler. func (h *MetricsHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume directive name for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "path": if !d.NextArg() { return d.ArgErr() } h.Path = d.Val() default: return d.Errf("unknown sip_guardian_metrics directive: %s", d.Val()) } } return nil } // Interface guards var ( _ caddyhttp.MiddlewareHandler = (*MetricsHandler)(nil) _ caddy.Provisioner = (*MetricsHandler)(nil) _ caddyfile.Unmarshaler = (*MetricsHandler)(nil) )