Skip to content

feat(smart_firewall): wirefilter Phase 3 + container-friendly logging#384

Merged
pigri merged 16 commits into
mainfrom
feat/wirefilter-phase3-and-log-format
Jun 11, 2026
Merged

feat(smart_firewall): wirefilter Phase 3 + container-friendly logging#384
pigri merged 16 commits into
mainfrom
feat/wirefilter-phase3-and-log-format

Conversation

@pigri

@pigri pigri commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • smart_firewall_rules wirefilter expressions now compile + evaluate per-flow (was silently no-op'd as "Phase 3 TODO"). Platform wire shape {rules:[...]} accepted via serde alias.
  • Container-friendly logging: blocked.log + eventbridge_log accept "stdout"/"stderr" sentinels; SYNAPSE_LOG_FORMAT=json for structured stdout output.
  • Fixes a duplicate-compile log on every SSE push (SSE handler was calling apply twice).

Test plan

  • Rolled to 6-node Hetzner cluster (Ubuntu 24.04, kernels 6.11 + 6.17). Per-pod log shows wirefilter compile — installed: 1, failed: 0 exactly once per Config applied from SSE push.
  • Bad expression (threat.score ge "90") surfaced as WARN smart_firewall_rules: skipping wirefilter rule '...' — compile failed: Filter parsing error (1:18) and installed: 0, failed: 1 instead of being silently dropped.
  • logging.blocked_log_file: stdout + eventbridge_log.file: stdout routes both event streams to pod stdout (visible via kubectl logs).
  • SYNAPSE_LOG_FORMAT=json emits one JSON object per line; default text format unchanged.
  • HTTP filter not initialized no longer spams WARN on every config push in agent mode.

Added in this push — logging hardening, block telemetry, dendrite

  • dendrite 0.1.2 → 0.1.3 (gen0sec-registry crate).
  • Proxy JSON logging fixed: console-only (no-file) logging now uses log4rs init_console_logging instead of the legacy env_logger path (no JSON mode), so SYNAPSE_LOG_FORMAT=json is honored for proxy-style deploys. Dropped the now-unused env_logger dep.
  • No double-encoding: already-JSON access logs route to a dedicated plain {m}{n} appender.
  • Compact JSON: custom encoder emits only {time, level, target, message}; built via serde_json::Value, not the json! macro (no-unwrap clippy lint).
  • Block telemetry: userland access-rules block emits a BlockEvent (layer=AccessRules); synapse-blocking-log emits block events as structured JSON on the stdout path, routed to the plain appender.

Wellness (local, before push): fmt / clippy (+classifier) / test / doc / machete — all green.

pigri and others added 13 commits June 10, 2026 11:06
Smart-firewall wirefilter rules in `smart_firewall_rules.{wirefilter,rules}`
now compile against the amygdala scheme at apply time and evaluate per-flow
against threat-intel + GeoIP enrichment. Previously: silently dropped.

Phase 3a — compile + global RuleSet:
- synapse-config accepts the platform wire shape `{rules: [...]}` via
  `#[serde(alias = "rules")]` on `smart_firewall_rules.wirefilter`.
- synapse-utils gets a `WirefilterReloadHook` parallel to the existing
  amygdala/WAF reload hooks. Payload is `(id, expression, action)`
  tuples so synapse-utils stays free of an amygdala dep.
- synapse-smart-firewall::wirefilter_apply compiles each rule via
  amygdala::RuleSet::compile, logs per-rule failures at WARN with the
  parse error + offending expression, installs the successful subset
  in a process-global `Arc<RuleSet>`. Re-exports amygdala's
  `Packet`/`Enrichment`/`Action` so callers don't take a direct dep.
- synapse-app registers the hook at startup alongside the others.

Phase 3b — per-flow eval call site:
- synapse-app's per-flow enrichment loop calls
  `wirefilter_apply::evaluate(&packet)` after threat-intel + GeoIP
  resolution and installs an IP block via the existing
  `access_rules::block_ip_runtime` path on Drop verdicts. Caches
  (score, advice) together so `threat.advice` is available at eval
  time (previously only score was cached).

smart_firewall_rules count + double-fire fix:
- Add `smart_firewall_rules: N` to the four info-level config-apply
  logs alongside waf_rules / access_rules counts.
- SSE-push handler no longer calls apply_smart_firewall_rules a
  second time after set_global_config (which already dispatches it
  per its single-chokepoint docstring) — fixes the duplicate compile
  log on every push.

Container-friendly logging:
- synapse-blocking-log and synapse-core::logger::eventbridge_log
  accept the sentinels "stdout" / "stderr" / "-" as path. Routes the
  appender to the corresponding standard stream instead of opening a
  file — events surface via `kubectl logs` without a writable volume.
- New `logging.blocked_log_file: Option<String>` config field
  overrides the historical `{log_directory}/blocked.log` path.
- New env var `SYNAPSE_LOG_FORMAT=json` swaps every PatternEncoder
  for log4rs::encode::json::JsonEncoder. Default stays the legacy
  text pattern so existing dashboards keep working.

WAF noise:
- `HTTP filter not initialized, cannot update` (fires on every config
  push in agent mode, where there's no HTTP listener) demoted from
  WARN to DEBUG. The fall-through is already Ok(()); the warn was
  misleading "looks like an error but isn't".

Phase 3c (BPF map lowering for kernel-evaluable subexpressions) is
deferred — Phase 3b's userspace path covers the agent-mode use case
today.
Apply rustfmt across wirefilter changes (fixes Formatting + Windows CI,
which runs `cargo fmt --check` first). Also drop debug formatting on the
threat_mmdb logs so the version prints bare instead of Some("...") and the
path prints without surrounding quotes.
`fetch_config_conditional` and `fetch_config_immutable` were calling
`set_global_config(body.config.clone())` themselves, which fires the
reload hook chain (including `apply_smart_firewall_rules` → wirefilter
compile). The SSE push handler / periodic poller then ALSO called
`set_global_config` on the returned body, firing the hook chain a
second time per push. Visible as duplicate `wirefilter compile —
installed: N, failed: M` log lines on every push.

Move the promotion responsibility entirely to the worker layer.
Fetch primitives now just return the parsed body; the worker (which
already calls `set_global_config` immediately after Fresh) is the
single chokepoint.
The startup-load debug log and the version-file write-failure warning still
debug-printed the PathBuf, producing quoted paths. Switch them to display()
so every threat_mmdb log line renders paths consistently.
The GeoIP MMDB worker is the sibling of threat_mmdb and carried the same
debug-format warts: version logged as Some("...") and paths printed quoted.
Match the threat_mmdb cleanup so all MMDB worker logs render consistently.
…ter eval

- Drop redundant u32->u32 cast on src_asn (pkt_event.asn is already Option<u32>).
- Collapse nested if-let into a let-chain (clippy::collapsible_if).
- Mark threat_advice allow(unused_variables) off linux+amygdala-reactor, where
  the binding is kept only for its threat_score side effect (fixes Windows -D warnings).
Bumps the gen0sec-registry dendrite crate (and dendrite-core/-linux/-windows)
across all synapse crates. cargo check --workspace passes clean (0 warnings).
…ockEvent

- logging: console-only (no file, non-terminal) path now uses log4rs
  init_console_logging instead of legacy env_logger, so SYNAPSE_LOG_FORMAT=json
  is honored for proxy-style deploys; access_log=Info parity preserved.
- telemetry: userland access-rules block now emits a synapse_blocking_log
  BlockEvent (layer=AccessRules) so it lands in block_events, not just the
  terminal stream.
init_console_logging now routes the access_log target to a dedicated
plain ({m}{n}) console appender with additive=false, so the already-JSON
access-log payload isn't wrapped again by the JSON encoder. Mirrors the
access.log file appender in init_file_logging.
Replace log4rs default JsonEncoder with CompactJsonEncoder that emits only
ops-relevant fields, dropping module_path/file/line/thread_id/mdc noise.
Used by both file and console log paths; access logs unaffected (plain {m}{n}).
The console-only logging path now uses log4rs (init_console_logging), so
env_logger is no longer used by synapse-app — remove the direct dependency
(keeps cargo-machete green) and the stale comment.
- compact encoder: build the JSON line via serde_json::Value instead of the
  json! macro (which expands to .unwrap(), banned by workspace clippy).
- block events: synapse-blocking-log now emits the BlockEvent as structured
  JSON on the no-file-sink (stdout) path instead of a text to_log_line summary;
  the app logger routes the synapse_blocking_log target to the plain appender
  so the JSON isn't re-wrapped as a string by the JSON encoder.
pigri added 3 commits June 10, 2026 20:46
When a wirefilter smart-firewall rule matches a flow (synapse-app per-flow
eval), emit a synapse_blocking_log BlockEvent (layer=SmartFirewall,
source=FingerprintRule) carrying the matched rule's id (EvalMatch.rule_name,
= WirefilterRule.id) so block_events attributes the drop to the specific rule
instead of only a terminal HttpEvent. Also fixes the block_source mislabel
(was ThreatIntel). Full UUID requires config-generator stamping the DB id;
the reactor/bridge kind-keyed path is a separate change.
Guards the rule-id telemetry plumbing: a wirefilter rule's id (config UUID)
must become the compiled rule's name (which evaluate() surfaces as
EvalMatch.rule_name and synapse-app emits as the BlockEvent rule_id), with
empty id falling back to a stable wf-<hash>. Uses RuleSet::iter() so no
packet construction / dendrite dev-dep is needed.
Mirror the WAF taxonomy for smart-firewall wirefilter blocks: classify the
block-event source from the matched rule's expression (threat.* -> threat_intel,
signal.classifier -> classifier, ja4* -> fingerprint_rule, ip.src.country/asn ->
geoip, else fingerprint_rule) instead of a hard-coded FingerprintRule. The
source is computed at install time (amygdala::CompiledRule drops the expression)
and threaded through EvalMatch to the synapse-app emit; the terminal HttpEvent
block_source is mapped likewise. Tests cover the classification.
@pigri pigri merged commit 8894017 into main Jun 11, 2026
28 checks passed
@pigri pigri deleted the feat/wirefilter-phase3-and-log-format branch June 11, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant