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:
parent
95a794ba69
commit
976fdf53a5
5 changed files with 1485 additions and 1 deletions
109
l4handler.go
109
l4handler.go
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue