fix(waf): Engine nur bei Konfigurationsänderung neu bauen — v1.2.75

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 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-03 07:07:30 +02:00
parent 8041e3924d
commit 08119f8ccf
2 changed files with 51 additions and 15 deletions

View File

@@ -1 +1 @@
1.2.74 1.2.75

View File

@@ -17,12 +17,22 @@ type DomainEngine struct {
} }
// Manager holds per-domain Coraza engine instances. Engines are // 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. // All public methods are safe for concurrent use.
type Manager struct { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
engines map[string]*DomainEngine // hostname → engine (nil entry = disabled) engines map[string]*DomainEngine // hostname → engine (nil entry = disabled)
crsDir string 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. // NewManager creates an empty Manager with the given CRS directory.
@@ -31,8 +41,9 @@ func NewManager(crsDir string) *Manager {
crsDir = DefaultCRSDir crsDir = DefaultCRSDir
} }
return &Manager{ return &Manager{
engines: make(map[string]*DomainEngine), engines: make(map[string]*DomainEngine),
crsDir: crsDir, configKeys: make(map[string]configKey),
crsDir: crsDir,
} }
} }
@@ -42,30 +53,55 @@ type DomainConfig struct {
Config models.WafConfig Config models.WafConfig
} }
// Reload rebuilds all engine instances from the given list. Domains // Reload refreshes engines from the given list, rebuilding only when
// that are disabled get a nil entry so GetForHost returns quickly // the config has actually changed since the last call.
// without looking up a missing key.
func (m *Manager) Reload(domains []DomainConfig) error { 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 { 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 { if !dc.Config.Enabled {
engines[dc.Hostname] = nil newEngines[dc.Hostname] = nil
continue 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) waf, err := BuildEngine(dc.Config, m.crsDir)
if err != nil { if err != nil {
return fmt.Errorf("waf: build engine for %s: %w", dc.Hostname, err) return fmt.Errorf("waf: build engine for %s: %w", dc.Hostname, err)
} }
engines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode} newEngines[dc.Hostname] = &DomainEngine{WAF: waf, Mode: dc.Config.Mode}
slog.Info("waf: engine loaded", slog.Info("waf: engine (re)loaded",
"host", dc.Hostname, "host", dc.Hostname,
"mode", dc.Config.Mode, "mode", dc.Config.Mode,
"paranoia_level", dc.Config.ParanoiaLevel, "paranoia_level", dc.Config.ParanoiaLevel,
"crs", crsAvailable(m.crsDir), "crs", crsAvailable(m.crsDir),
) )
} }
m.mu.Lock() m.mu.Lock()
m.engines = engines m.engines = newEngines
m.configKeys = newKeys
m.mu.Unlock() m.mu.Unlock()
return nil return nil
} }