caddy-sip-guardian/metrics.go
Ryan Malloy 976fdf53a5 Add SIP message validation feature
Implements RFC 3261 compliance checking and security validation:

- Three validation modes: permissive (default), strict, paranoid
- Critical checks: null bytes, binary injection (immediate ban)
- RFC compliance: required headers (Via, From, To, Call-ID, CSeq, Max-Forwards)
- Format validation: CSeq range, Content-Length, Via branch format
- Paranoid mode: SQL injection patterns, excessive headers, long values
- Compact header form support (v, f, t, i, l, etc.)

Caddyfile configuration:
  validation {
      enabled true
      mode permissive
      max_message_size 65535
      ban_on_null_bytes true
      ban_on_binary_injection true
      disabled_rules via_invalid_branch
  }

New Prometheus metrics:
- sip_guardian_validation_violations_total{rule}
- sip_guardian_validation_results_total{result}
- sip_guardian_message_size_bytes (histogram)

Includes comprehensive unit tests covering all validation scenarios.
2025-12-07 15:57:26 -07:00

334 lines
9.1 KiB
Go

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},
},
)
// 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},
},
)
)
// 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,
sipValidationViolations,
sipValidationResults,
sipMessageSizeBytes,
)
}
// 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"`
}
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)
)