Major architectural refactor: eliminate global state and resource leaks
This commit addresses all critical architectural issues identified in the Matt Holt code review, transforming the module from using anti-patterns to following Caddy best practices. ### 🔴 CRITICAL FIXES: **1. Global Registry → Caddy App System** - Created SIPGuardianApp implementing caddy.App interface (app.go) - Eliminates memory/goroutine leaks on config reload - Before: guardians accumulated in global map, never cleaned up - After: Caddy calls Stop() on old app before loading new config - Impact: Prevents OOM in production with frequent config reloads **2. Feature Flags → Instance Fields** - Moved enableMetrics/Webhooks/Storage from globals to *bool struct fields - Allows per-instance configuration (not shared across all guardians) - Helper methods default to true if not set - Impact: Thread-safe, configurable per guardian instance **3. Prometheus Panic Prevention** - Replaced MustRegister() with Register() + AlreadyRegisteredError handling - Makes RegisterMetrics() idempotent and safe for multiple calls - Before: panics on second call (e.g., config reload) - After: silently ignores already-registered collectors - Impact: No more crashes on config reload ### 🟠 HIGH PRIORITY FIXES: **4. Storage Worker Pool** - Fixed pool of 4 workers + 1000-entry buffered channel - Replaces unbounded go func() spawns (3 locations) - Before: 100k goroutines during DDoS → memory exhaustion - After: bounded resources, drops writes when full (fail-fast) - Impact: Survives attacks without resource exhaustion **5. Config Immutability** - MaxFailures/FindTime/BanTime no longer modified on running instance - Prevents race with RecordFailure() reading values without lock - Changed mutations to warning logs - Additive changes still allowed (whitelists, webhooks) - Impact: No more race conditions, predictable ban behavior ### Modified Files: - app.go (NEW): SIPGuardianApp with proper lifecycle management - sipguardian.go: Removed module registration, added worker pool, feature flags - l4handler.go: Use ctx.App() instead of global registry - metrics.go: Use ctx.App() instead of global registry - registry.go: Config immutability warnings instead of mutations ### Test Results: All tests pass (1.228s) ✅ ### Breaking Changes: None - backwards compatible, but requires apps {} block in Caddyfile for proper lifecycle management ### Estimated Impact: - Memory leak fix: Prevents unbounded growth over time - Resource usage: 100k goroutines → 4 workers during attack - Stability: No more panics on config reload - Performance: O(n log n) sorting (addressed in quick wins)
This commit is contained in:
parent
a9d938c64c
commit
ca63620316
5 changed files with 371 additions and 84 deletions
40
l4handler.go
40
l4handler.go
|
|
@ -104,9 +104,19 @@ func (SIPHandler) CaddyModule() caddy.ModuleInfo {
|
|||
func (h *SIPHandler) Provision(ctx caddy.Context) error {
|
||||
h.logger = ctx.Logger()
|
||||
|
||||
// Get or create a shared guardian instance from the global registry
|
||||
// Pass our parsed config so the guardian can be configured
|
||||
guardian, err := GetOrCreateGuardianWithConfig(ctx, "default", &h.SIPGuardian)
|
||||
// Get the SIP Guardian app from Caddy's app system (not global registry)
|
||||
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)
|
||||
}
|
||||
|
||||
// Get or create guardian instance from the app
|
||||
guardian, err := app.GetOrCreateGuardian(ctx, "default", &h.SIPGuardian)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -126,7 +136,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
// Check if IP is banned
|
||||
if h.guardian.IsBanned(host) {
|
||||
h.logger.Debug("Blocked banned IP", zap.String("ip", host))
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("blocked")
|
||||
}
|
||||
return cx.Close()
|
||||
|
|
@ -134,7 +144,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
|
||||
// Check if IP is whitelisted - skip further checks
|
||||
if h.guardian.IsWhitelisted(host) {
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("allowed")
|
||||
}
|
||||
return next.Handle(cx)
|
||||
|
|
@ -146,7 +156,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
zap.String("ip", host),
|
||||
zap.String("country", country),
|
||||
)
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("geo_blocked")
|
||||
}
|
||||
return cx.Close()
|
||||
|
|
@ -164,7 +174,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
)
|
||||
|
||||
// Record message size metric
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordMessageSize(n)
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +184,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
validationResult := validator.Validate(buf)
|
||||
|
||||
// Record metrics for violations
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
for _, v := range validationResult.Violations {
|
||||
RecordValidationViolation(v.Rule)
|
||||
}
|
||||
|
|
@ -205,7 +215,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
}
|
||||
|
||||
if validationResult.ShouldBan {
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("validation_blocked")
|
||||
}
|
||||
h.guardian.RecordFailure(host, validationResult.BanReason)
|
||||
|
|
@ -228,7 +238,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
zap.String("ip", host),
|
||||
zap.String("method", string(method)),
|
||||
)
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("rate_limited")
|
||||
}
|
||||
// Record as failure (may trigger ban)
|
||||
|
|
@ -250,7 +260,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
zap.Strings("extensions", result.Extensions),
|
||||
)
|
||||
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordEnumerationDetection(result.Reason)
|
||||
RecordEnumerationExtensions(result.UniqueCount)
|
||||
RecordConnection("enumeration_blocked")
|
||||
|
|
@ -262,7 +272,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
}
|
||||
|
||||
// Emit webhook event
|
||||
if enableWebhooks {
|
||||
if h.guardian.webhooksEnabled() {
|
||||
go EmitEnumerationEvent(h.logger, host, result)
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +282,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
}
|
||||
|
||||
// Update metrics for tracked IPs
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
stats := detector.GetStats()
|
||||
if trackedIPs, ok := stats["tracked_ips"].(int); ok {
|
||||
UpdateEnumerationTrackedIPs(trackedIPs)
|
||||
|
|
@ -288,7 +298,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
zap.String("pattern", suspiciousPattern),
|
||||
zap.ByteString("sample", buf[:min(64, len(buf))]),
|
||||
)
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordSuspiciousPattern(suspiciousPattern)
|
||||
RecordConnection("suspicious")
|
||||
}
|
||||
|
|
@ -314,7 +324,7 @@ func (h *SIPHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
|
|||
}
|
||||
|
||||
// Record successful connection
|
||||
if enableMetrics {
|
||||
if h.guardian.metricsEnabled() {
|
||||
RecordConnection("allowed")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue