From 08119f8ccf6f65ff1dc122bbc3d85823affaf8e1 Mon Sep 17 00:00:00 2001 From: Debian Date: Wed, 3 Jun 2026 07:07:30 +0200 Subject: [PATCH] =?UTF-8?q?fix(waf):=20Engine=20nur=20bei=20Konfigurations?= =?UTF-8?q?=C3=A4nderung=20neu=20bauen=20=E2=80=94=20v1.2.75?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manager.Reload() hat bisher bei jedem 30s-Tick alle Engines neu gebaut (BuildEngine mit CRS = 2-5s). Fix: configKey (enabled, mode, paranoia_level, updatedAt) cachen — Engine wird nur neu gebaut wenn sich der Key ändert. Spart CPU und verhindert sporadische Latenzen im SPOE-Handling. Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- internal/waf/manager.go | 64 ++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 15 deletions(-) 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 }