Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors Synapse’s firewall stack to integrate hillock v0.3.3 and introduce config-driven 5‑tuple firewall rules dispatched across BPF (XDP/TC) and system firewalls (iptables/nftables), wired into the existing config reload + amygdala hook paths.
Changes:
- Adds global per-interface dispatcher registries for XDP and TC, caching long-lived hillock backend handles and exposing safe access helpers.
- Introduces a unified
FirewallRulesBackendtrait with BPF + iptables + nftables implementations, plus reload glue to apply config rules across backends. - Updates the amygdala bridge and sweeper to use dispatcher-based enforcement and to manage TTL expiration for iptables 5‑tuple rules.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/dispatcher_live_test.rs | Root-only live integration tests for full dispatcher chain + backend roundtrips. |
| tests/e2e/dispatcher_integration_test.rs | Non-root integration tests for config parsing and handler/dispatcher safety. |
| tests/e2e_test.rs | Registers new dispatcher test modules. |
| docs/FIREWALL_RULES_ARCHITECTURE.md | Documents end-to-end rule flow and backend architecture. |
| crates/synapse-waf/src/wirefilter.rs | Extends config initialization with firewall_rules defaults. |
| crates/synapse-utils/src/hooks.rs | Adds a config-reload hook for 5‑tuple firewall rules. |
| crates/synapse-smart-firewall/src/sweeper.rs | Integrates iptables 5‑tuple TTL sweeping into the periodic sweeper. |
| crates/synapse-smart-firewall/src/lib.rs | Exposes the new firewall-rules reload module under linux reactor builds. |
| crates/synapse-smart-firewall/src/firewall_rules_reload.rs | Applies config rulesets into BPF/iptables/nftables backends on reload. |
| crates/synapse-smart-firewall/src/bridge.rs | Switches amygdala enforcement to dispatcher-based hillock backends and tail-call chaining. |
| crates/synapse-smart-firewall/Cargo.toml | Updates hillock version/feature wiring and feature gating around BPF + reactor builds. |
| crates/synapse-security/src/lib.rs | Exposes new bpf_dispatcher and firewall_rules modules (feature-gated). |
| crates/synapse-security/src/firewall/fingerprint/mod.rs | Adds prog_fd() accessor for dispatcher tail-call registration. |
| crates/synapse-security/src/firewall_rules/nftables.rs | Implements nftables 5‑tuple rules backend via hillock. |
| crates/synapse-security/src/firewall_rules/mod.rs | Defines FirewallRulesBackend trait and re-exports hillock rule types. |
| crates/synapse-security/src/firewall_rules/iptables.rs | Implements iptables 5‑tuple rules backend + TTL side-table + sweeper function. |
| crates/synapse-security/src/firewall_rules/handler.rs | Implements dispatcher-routed BPF 5‑tuple rules handler (XDP ingress/TC egress). |
| crates/synapse-security/src/bpf_dispatcher/xdp_dispatcher.rs | Adds global per-ifindex XDP dispatcher registry + backend accessors. |
| crates/synapse-security/src/bpf_dispatcher/tc_dispatcher.rs | Adds global per-ifindex TC dispatcher registry + backend accessors. |
| crates/synapse-security/src/bpf_dispatcher/mod.rs | Exposes dispatcher modules and provides non-bpf/non-linux stubs. |
| crates/synapse-security/Cargo.toml | Updates hillock version and feature wiring for bpf/iptables/nftables. |
| crates/synapse-core/src/core/cli.rs | Adds firewall.firewall_rules config schema types. |
| crates/synapse-config/src/lib.rs | Invokes firewall-rules reload hook during config application. |
| crates/synapse-app/src/lib.rs | Registers the firewall-rules reload hook at startup. |
| crates/synapse-access-rules/src/lib.rs | Routes IP bans/unbans through the XDP dispatcher path (with legacy fallback). |
| crates/synapse-access-rules/Cargo.toml | Adds hillock dep wiring needed for the dispatcher path under bpf. |
| Cargo.toml | Updates workspace dev-deps and feature defaults for hillock/bpf-related testing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn handle_reload(cfg: Option<&FirewallRulesConfig>) { | ||
| let ifindex = BPF_IFINDEX.get().copied().unwrap_or(0); | ||
| if let Some(config) = cfg { | ||
| if config.rulesets.is_empty() { clear_all(ifindex, config); } | ||
| else { apply_from_config(ifindex, config); } |
| hillock = { version = "0.3.3", registry = "gen0sec", default-features = false, optional = true } | ||
| uuid = { version = "1", features = ["v4"], optional = true } |
| // Sweep 5-tuple firewall-rules TTL for iptables. | ||
| #[cfg(feature = "bpf")] | ||
| if let Ok(n) = std::panic::catch_unwind(|| { | ||
| synapse_security::firewall_rules::iptables::sweep_expired_ttls() | ||
| }) { | ||
| total += n; | ||
| } | ||
| } |
| fn has_xdp() -> bool { | ||
| let mut f = false; | ||
| crate::bpf_dispatcher::xdp_dispatcher::for_each_mut(|_, _| f = true); | ||
| f | ||
| } |
| fn get() -> &'static Arc<Mutex<IpTablesFirewallRules>> { | ||
| INSTANCE.get_or_init(|| { | ||
| IpTablesFirewallRules::new_hooked() | ||
| .map(|fw| Arc::new(Mutex::new(fw))) | ||
| .expect("failed to initialize iptables firewall rules") | ||
| }) |
| if count > 0 { | ||
| let mut g = ttl_map().lock().unwrap(); | ||
| for k in &expired { g.remove(k); } | ||
| synapse_log::info!("iptables firewall-rules TTL sweep: removed {count} expired"); | ||
| } |
| FirewallRule { action: match r.action { FilterAction::Allow => IpFilterAction::ACCEPT, FilterAction::Block => IpFilterAction::DROP, FilterAction::Reject => IpFilterAction::REJECT }, rule_id: r.rule_id, priority: r.priority, ip_version: r.ip_version, src_ip: r.src_ip, dst_ip: r.dst_ip, src_port: r.src_port, dst_port: r.dst_port, protocol: r.protocol, ttl: r.ttl, ext: None } | ||
| } | ||
| fn from_ip(r: IpFilterFirewallRule) -> FirewallRule<Option<String>, uuid::Uuid, FilterAction> { | ||
| FirewallRule { action: match r.action { IpFilterAction::ACCEPT => FilterAction::Allow, _ => FilterAction::Block }, rule_id: r.rule_id, priority: r.priority, ip_version: r.ip_version, src_ip: r.src_ip, dst_ip: r.dst_ip, src_port: r.src_port, dst_port: r.dst_port, protocol: r.protocol, ttl: r.ttl, ext: None } |
| # target_env above (glibc=on, musl=off), so these stay empty — that keeps the | ||
| # eBPF backends off Windows and off the musl/legacy agent. | ||
| bpf = [] | ||
| bpf = ["dep:synapse-security", "synapse-security/bpf"] |
| #[cfg(feature = "bpf")] | ||
| pub mod firewall_rules; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 30 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
crates/synapse-smart-firewall/src/firewall_rules_reload.rs:24
handle_reload(None)is documented by the hook type as meaning “clear all rules”, but this implementation does nothing whencfgisNone. Additionally, only rulesets present in the incoming config are cleared, so rulesets removed from config will remain installed (stale rules).
pub fn handle_reload(cfg: Option<&FirewallRulesConfig>) {
let ifindex = BPF_IFINDEX.get().copied().unwrap_or(0);
if let Some(config) = cfg {
if config.rulesets.is_empty() { clear_all(ifindex, config); }
else { apply_from_config(ifindex, config); }
}
crates/synapse-smart-firewall/src/firewall_rules_reload.rs:66
FirewallRuleEntry.protocolis documented as allowing "any" (wildcard), butparse_rulealways sets a concrete protocol (Some(proto)), and unknown values default to TCP. This makes "any" behave like TCP-only, which changes rule semantics.
let action = if entry.action.as_str() == "allow" { FilterAction::Allow } else { FilterAction::Block };
let proto = parse_protocol(&entry.protocol);
let mut b = FirewallRuleBuilder::new().action(action).protocol(Some(proto));
if let Some(ref src) = entry.src_ip {
crates/synapse-security/src/firewall_rules/iptables.rs:29
- The iptables rules backend initialization uses
.expect("failed to initialize iptables firewall rules"), which will panic the whole process if iptables is unavailable/misconfigured. This contradicts the rest of the handlers’ “no-op when backend missing” behavior (e.g. nftables handler returnsNoneand logs a warning). Consider making iptables initialization fallible and having the handler methods no-op (or return errors) when iptables cannot be used.
fn get() -> &'static Arc<Mutex<IpTablesFirewallRules>> {
INSTANCE.get_or_init(|| {
IpTablesFirewallRules::new_hooked()
.map(|fw| Arc::new(Mutex::new(fw)))
.expect("failed to initialize iptables firewall rules")
})
| let mut count = 0; | ||
| for (ifindex, name, ipv, dir, rule_id) in &expired { | ||
| if let Some(ruleset) = FirewallRulesHandler.get_ruleset(*ifindex, name, *ipv, *dir) { | ||
| if let Some(idx) = ruleset.iter().position(|r| r.rule_id == *rule_id) { | ||
| FirewallRulesHandler.delete_rule(*ifindex, name, idx, *ipv, *dir); | ||
| count += 1; | ||
| } | ||
| } | ||
| } |
| #[cfg(feature = "bpf")] | ||
| bpf.insert_ruleset(ifindex, name, rules.clone(), IpVersion::V4, NetworkDirection::Ingress); | ||
| ipt.insert_ruleset(0, name, rules.clone(), IpVersion::V4, NetworkDirection::Ingress); | ||
| nft.insert_ruleset(0, name, rules.clone(), IpVersion::V4, NetworkDirection::Ingress); | ||
| } |
| // Config-driven 5-tuple firewall rules → all backends (BPF + iptables + nftables) | ||
| #[cfg(all(target_os = "linux", feature = "amygdala-reactor"))] | ||
| pub mod firewall_rules_reload; |
Description:
Summary
This PR integrates hillock v0.3.3 (gen0sec registry) into synapse's firewall stack, replacing direct BPF skeleton access with a global dispatcher registry (
bpf_dispatcher) that caches long-lived hillock backend handles per-interface. It adds a config-driven 5-tuple firewall rules layer (firewall_rules) with a unifiedFirewallRulesBackendtrait and three backend implementations (BPF XDP/TC, iptables, nftables), all wired through synapse's existing config reload and amygdala hook infrastructure.Key changes
1. BPF dispatcher registry (bpf_dispatcher)
OnceLock<Mutex<HashMap<i32, XdpState/TcState>>>per-interface registriesXdpDispatcher/TcDispatcher+ long-lived hillock backends (XDPIpFilterBackend,XDPFirewallRulesBackend,TCIpFilterBackend,TCFirewallRulesBackend)ip_filter_for_each_mut,firewall_rules_for_each_mut,ip_filter_with,firewall_rules_with) hide dispatcher/ifindex details from callersinsert()eagerly creates backends at registration time;_with()accessors returnResult<(), BackendMissing>— no silent failures2. 5-tuple firewall rules (firewall_rules)
FirewallRulesBackendtrait — full CRUD + query:insert_ruleset,delete_ruleset,push_rule,insert_rule,delete_rule,pop_rule,get_rulesets,get_ruleset,clearFirewallRulesHandler— zero-sized singleton routing ingress rules to XDP (if available) or TC, egress always to TCIptablesFirewallRulesHandler/NftablesFirewallRulesHandler— system-wide singletons using hillock'sIpTablesFirewallRules/NfTablesFirewallRuleBackend(name, ip_version, direction)— same key across calls accumulates rules in the hillock backend3. Config-driven dispatch
FirewallRuleEntry/FirewallRulesConfig— YAML/JSONfirewall.firewall_rules.rulesetssection parsed into 5-tuple rulesFirewallRuleEntry→FirewallRuleviaFirewallRuleBuilder, pushing to all three backendsapply_smart_firewall_rules()insynapse-config, registered viasynapse_utils::hookshook system at startup4. Iptables TTL sweeper
OnceLock<Mutex<HashMap>>) tracking(ruleset_name, rule_id, ip_version, direction) → expiry Instantper rulesweep_expired_ttls()called every 30s from the existing amygdala sweeper — removes expired rules from the hillock iptables backend5. Dendrite fingerprint dispatch
XdpFirewall::prog_fd()accessor — dendrite's xdp_filter.bpf.c skeleton FD registered through the dispatcher's tail-call chain (add_program("fingerprint", fp_fd))ip_filter → firewall_rules → fingerprint6. Tests
FirewallRuleBuilderconstruction, trait method safety on missing backend, dispatcher no-backend no-panic#[ignore]tests: full dispatcher chain on veth + rule push/query/clear, iptables roundtrip, nftables roundtripArchitecture diagram
Files changed
28 files, +3,759 / -1,058 lines