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.
This commit is contained in:
Ryan Malloy 2025-12-07 15:57:26 -07:00
parent 95a794ba69
commit 976fdf53a5
5 changed files with 1485 additions and 1 deletions

View file

@ -153,7 +153,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
// Read data from the connection for suspicious pattern detection
// caddy-l4 replays prefetched data on read, so we can read the full message here
buf := make([]byte, 1024)
buf := make([]byte, 4096) // Larger buffer for validation
n, err := cx.Read(buf)
if n > 0 {
buf = buf[:n]
@ -162,6 +162,61 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
zap.Int("bytes", n),
)
// Record message size metric
if enableMetrics {
RecordMessageSize(n)
}
// Validate SIP message structure and content
validator := GetValidator(h.logger)
if validator.IsEnabled() {
validationResult := validator.Validate(buf)
// Record metrics for violations
if enableMetrics {
for _, v := range validationResult.Violations {
RecordValidationViolation(v.Rule)
}
if validationResult.Valid {
RecordValidationResult("valid")
} else if validationResult.ShouldBan {
RecordValidationResult("ban")
} else {
RecordValidationResult("invalid")
}
}
if !validationResult.Valid {
h.logger.Warn("SIP validation failed",
zap.String("ip", host),
zap.Int("violation_count", len(validationResult.Violations)),
zap.Bool("should_ban", validationResult.ShouldBan),
)
// Log individual violations at debug level
for _, v := range validationResult.Violations {
h.logger.Debug("Validation violation",
zap.String("ip", host),
zap.String("rule", v.Rule),
zap.String("severity", string(v.Severity)),
zap.String("message", v.Message),
)
}
if validationResult.ShouldBan {
if enableMetrics {
RecordConnection("validation_blocked")
}
h.guardian.RecordFailure(host, validationResult.BanReason)
return cx.Close()
}
// In strict/paranoid mode, any violation rejects the message
// but we already counted it toward ban threshold above via RecordFailure
// For now in permissive mode, log and continue
}
}
// Extract SIP method for rate limiting
method := ExtractSIPMethod(buf)
if method != "" {
@ -362,6 +417,14 @@ func (m *SIPMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// ban_time 2h
// exempt_extensions 100 200 9999
// }
// validation {
// enabled true
// mode permissive # permissive, strict, paranoid
// max_message_size 65535
// ban_on_null_bytes true
// ban_on_binary_injection true
// disabled_rules via_invalid_branch cseq_out_of_range
// }
// webhook http://example.com/hook { ... }
// }
//
@ -507,6 +570,50 @@ func (h *SIPHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}
case "validation":
h.Validation = &ValidationConfig{}
for innerNesting := d.Nesting(); d.NextBlock(innerNesting); {
switch d.Val() {
case "enabled":
if !d.NextArg() {
return d.ArgErr()
}
h.Validation.Enabled = d.Val() == "true" || d.Val() == "yes" || d.Val() == "on"
case "mode":
if !d.NextArg() {
return d.ArgErr()
}
mode := ValidationMode(d.Val())
if mode != ValidationModePermissive && mode != ValidationModeStrict && mode != ValidationModeParanoid {
return d.Errf("invalid validation mode: %s (must be permissive, strict, or paranoid)", d.Val())
}
h.Validation.Mode = mode
case "max_message_size":
if !d.NextArg() {
return d.ArgErr()
}
var val int
if _, err := fmt.Sscanf(d.Val(), "%d", &val); err != nil {
return d.Errf("invalid max_message_size: %v", err)
}
h.Validation.MaxMessageSize = val
case "ban_on_null_bytes":
if !d.NextArg() {
return d.ArgErr()
}
h.Validation.BanOnNullBytes = d.Val() == "true" || d.Val() == "yes" || d.Val() == "on"
case "ban_on_binary_injection":
if !d.NextArg() {
return d.ArgErr()
}
h.Validation.BanOnBinaryInjection = d.Val() == "true" || d.Val() == "yes" || d.Val() == "on"
case "disabled_rules":
h.Validation.DisabledRules = d.RemainingArgs()
default:
return d.Errf("unknown validation directive: %s", d.Val())
}
}
case "webhook":
if !d.NextArg() {
return d.ArgErr()