diff --git a/VERSION b/VERSION index a366a08..eae86f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.74 +1.2.75 diff --git a/internal/waf/manager.go b/internal/waf/manager.go index f35eddf..7caaee3 100644 --- a/internal/waf/manager.go +++ b/internal/waf/manager.go @@ -17,12 +17,22 @@ type DomainEngine struct { } // Manager holds per-domain Coraza engine instances. Engines are -// created lazily on first Reload() and cached until the next reload. +// rebuilt only when their configuration changes (UpdatedAt differs). // All public methods are safe for concurrent use. type Manager struct { - mu sync.RWMutex - engines map[string]*DomainEngine // hostname → engine (nil entry = disabled) - crsDir string + mu sync.RWMutex + engines map[string]*DomainEngine // hostname → engine (nil entry = disabled) + configKeys map[string]configKey // hostname → last-seen config fingerprint + crsDir string +} + +// configKey identifies a specific WAF config snapshot so we only +// rebuild the engine when something actually changed. +type configKey struct { + enabled bool + mode string + paranoiaLevel int + updatedAt int64 // unix nano } // NewManager creates an empty Manager with the given CRS directory. @@ -31,8 +41,9 @@ func NewManager(crsDir string) *Manager { crsDir = DefaultCRSDir } return &Manager{ - engines: make(map[string]*DomainEngine), - crsDir: crsDir, + engines: make(map[string]*DomainEngine), + configKeys: make(map[string]configKey), + crsDir: crsDir, } } @@ -42,30 +53,55 @@ type DomainConfig struct { Config models.WafConfig } -// Reload rebuilds all engine instances from the given list. Domains -// that are disabled get a nil entry so GetForHost returns quickly -// without looking up a missing key. +// Reload refreshes engines from the given list, rebuilding only when +// the config has actually changed since the last call. func (m *Manager) Reload(domains []DomainConfig) error { - engines := make(map[string]*DomainEngine, len(domains)) + m.mu.RLock() + prevEngines := m.engines + prevKeys := m.configKeys + m.mu.RUnlock() + + newEngines := make(map[string]*DomainEngine, len(domains)) + newKeys := make(map[string]configKey, len(domains)) + for _, dc := range domains { + ck := configKey{ + enabled: dc.Config.Enabled, + mode: dc.Config.Mode, + paranoiaLevel: dc.Config.ParanoiaLevel, + updatedAt: dc.Config.UpdatedAt.UnixNano(), + } + newKeys[dc.Hostname] = ck + if !dc.Config.Enabled { - engines[dc.Hostname] = nil + newEngines[dc.Hostname] = nil continue } + + // Reuse existing engine if config hasn't changed. + if prev, ok := prevKeys[dc.Hostname]; ok && prev == ck { + if existing := prevEngines[dc.Hostname]; existing != nil { + newEngines[dc.Hostname] = existing + continue + } + } + waf, err := BuildEngine(dc.Config, m.crsDir) if err != nil { return fmt.Errorf("waf: build engine for %s: %w", dc.Hostname, err) } - engines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode} - slog.Info("waf: engine loaded", + newEngines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode} + slog.Info("waf: engine (re)loaded", "host", dc.Hostname, "mode", dc.Config.Mode, "paranoia_level", dc.Config.ParanoiaLevel, "crs", crsAvailable(m.crsDir), ) } + m.mu.Lock() - m.engines = engines + m.engines = newEngines + m.configKeys = newKeys m.mu.Unlock() return nil }