- cmd/edgeguard-waf/: neues Binary — lädt WAF-Configs aus DB, startet SPOE-Agent auf 127.0.0.1:9000, refreshed Configs alle 30s - internal/waf/engine.go: BuildEngine() — Coraza WAF aus WafConfig bauen (SecLang-Direktiven: RuleEngine, PL, CRS-Include, Exclusions, Custom) - internal/waf/manager.go: Manager — per-Hostname Coraza-Engine-Cache (thread-safe, Lazy-Init via Reload(), Port-Strip, IPv6-Brackets) - internal/waf/spoe.go: SPOEAgent — haproxy-go SPOE-Handler (src/method/path/query/ver/host/headers aus HAProxy-Vars, Coraza-Transaction, Blocking: txn.waf.status=403 setzen) - services/waf/waf.go: ListAllWithDomain() — JOIN domains+waf_configs - go.mod: coraza/v3 v3.7.0 + dropmorepackets/haproxy-go v0.0.8 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
2.9 KiB
Go
122 lines
2.9 KiB
Go
package waf
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"github.com/corazawaf/coraza/v3"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
)
|
|
|
|
// DomainEngine bundles a Coraza WAF with its operating mode.
|
|
type DomainEngine struct {
|
|
WAF coraza.WAF
|
|
Mode string // "detection" | "blocking"
|
|
}
|
|
|
|
// Manager holds per-domain Coraza engine instances. Engines are
|
|
// created lazily on first Reload() and cached until the next reload.
|
|
// 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
|
|
}
|
|
|
|
// NewManager creates an empty Manager with the given CRS directory.
|
|
func NewManager(crsDir string) *Manager {
|
|
if crsDir == "" {
|
|
crsDir = DefaultCRSDir
|
|
}
|
|
return &Manager{
|
|
engines: make(map[string]*DomainEngine),
|
|
crsDir: crsDir,
|
|
}
|
|
}
|
|
|
|
// DomainConfig pairs a domain hostname with its WAF policy.
|
|
type DomainConfig struct {
|
|
Hostname string
|
|
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.
|
|
func (m *Manager) Reload(domains []DomainConfig) error {
|
|
engines := make(map[string]*DomainEngine, len(domains))
|
|
for _, dc := range domains {
|
|
if !dc.Config.Enabled {
|
|
engines[dc.Hostname] = nil
|
|
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",
|
|
"host", dc.Hostname,
|
|
"mode", dc.Config.Mode,
|
|
"paranoia_level", dc.Config.ParanoiaLevel,
|
|
"crs", crsAvailable(m.crsDir),
|
|
)
|
|
}
|
|
m.mu.Lock()
|
|
m.engines = engines
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// GetForHost returns the DomainEngine for the given hostname, or
|
|
// (nil, false) when the domain has no WAF or WAF is disabled.
|
|
func (m *Manager) GetForHost(host string) (*DomainEngine, bool) {
|
|
// Strip port if present (e.g. "example.com:443" → "example.com").
|
|
if i := lastColon(host); i >= 0 {
|
|
host = host[:i]
|
|
}
|
|
m.mu.RLock()
|
|
de, ok := m.engines[host]
|
|
m.mu.RUnlock()
|
|
if !ok || de == nil {
|
|
return nil, false
|
|
}
|
|
return de, true
|
|
}
|
|
|
|
// lastColon returns the index of the last ':' in s that looks like a
|
|
// port separator (after the final ']' for IPv6), or -1.
|
|
func lastColon(s string) int {
|
|
// IPv6 addresses in brackets: "[::1]:443"
|
|
if len(s) > 0 && s[0] == '[' {
|
|
if rb := lastByte(s, ']'); rb >= 0 && rb < len(s)-1 && s[rb+1] == ':' {
|
|
return rb + 1
|
|
}
|
|
return -1
|
|
}
|
|
// Plain host — only strip port if there's exactly one colon.
|
|
count := 0
|
|
idx := -1
|
|
for i, c := range s {
|
|
if c == ':' {
|
|
count++
|
|
idx = i
|
|
}
|
|
}
|
|
if count == 1 {
|
|
return idx
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func lastByte(s string, b byte) int {
|
|
for i := len(s) - 1; i >= 0; i-- {
|
|
if s[i] == b {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|