package sipguardian import ( "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}, }, ) ) // metricsRegistered tracks if we've registered with Prometheus var metricsRegistered bool // RegisterMetrics registers all SIP Guardian metrics with Prometheus func RegisterMetrics() { if metricsRegistered { return } metricsRegistered = true prometheus.MustRegister( sipConnectionsTotal, sipBansTotal, sipUnbansTotal, sipActiveBans, sipFailuresTotal, sipTrackedIPs, sipWhitelistedConnections, sipSuspiciousPatterns, sipBanDurationSeconds, sipEnumerationDetections, sipEnumerationTrackedIPs, sipEnumerationUniqueExtensions, ) } // 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)) } // MetricsHandler provides a Prometheus metrics endpoint for SIP Guardian type MetricsHandler struct { // Path prefix for metrics (default: /metrics) Path string `json:"path,omitempty"` } 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" } 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 if guardian := 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) )