feat(smart_firewall): wirefilter Phase 3 + container-friendly logging#384
Merged
Conversation
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.
This was referenced Jun 10, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
smart_firewall_ruleswirefilter expressions now compile + evaluate per-flow (was silently no-op'd as "Phase 3 TODO"). Platform wire shape{rules:[...]}accepted via serde alias."stdout"/"stderr"sentinels;SYNAPSE_LOG_FORMAT=jsonfor structured stdout output.Test plan
wirefilter compile — installed: 1, failed: 0exactly once perConfig applied from SSE push.threat.score ge "90") surfaced asWARN smart_firewall_rules: skipping wirefilter rule '...' — compile failed: Filter parsing error (1:18)andinstalled: 0, failed: 1instead of being silently dropped.logging.blocked_log_file: stdout+eventbridge_log.file: stdoutroutes both event streams to pod stdout (visible viakubectl logs).SYNAPSE_LOG_FORMAT=jsonemits one JSON object per line; default text format unchanged.HTTP filter not initializedno longer spams WARN on every config push in agent mode.Added in this push — logging hardening, block telemetry, dendrite
init_console_logginginstead of the legacyenv_loggerpath (no JSON mode), soSYNAPSE_LOG_FORMAT=jsonis honored for proxy-style deploys. Dropped the now-unusedenv_loggerdep.{m}{n}appender.{time, level, target, message}; built viaserde_json::Value, not thejson!macro (no-unwrapclippy lint).BlockEvent(layer=AccessRules);synapse-blocking-logemits 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.