Skip to content

Integrate hillock XDP/TC dispatchers, 5-tuple firewall rules, and iptables/nftables/BPF backends via config-driven dispatch#458

Open
Gepsonka wants to merge 2 commits into
mainfrom
fi
Open

Integrate hillock XDP/TC dispatchers, 5-tuple firewall rules, and iptables/nftables/BPF backends via config-driven dispatch#458
Gepsonka wants to merge 2 commits into
mainfrom
fi

Conversation

@Gepsonka

Copy link
Copy Markdown
Collaborator

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 unified FirewallRulesBackend trait 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)

  • xdp_dispatcher.rs / tc_dispatcher.rs — global OnceLock<Mutex<HashMap<i32, XdpState/TcState>>> per-interface registries
  • Each state struct holds an XdpDispatcher/TcDispatcher + long-lived hillock backends (XDPIpFilterBackend, XDPFirewallRulesBackend, TCIpFilterBackend, TCFirewallRulesBackend)
  • Backend-level APIs (ip_filter_for_each_mut, firewall_rules_for_each_mut, ip_filter_with, firewall_rules_with) hide dispatcher/ifindex details from callers
  • insert() eagerly creates backends at registration time; _with() accessors return Result<(), BackendMissing> — no silent failures

2. 5-tuple firewall rules (firewall_rules)

  • FirewallRulesBackend trait — full CRUD + query: insert_ruleset, delete_ruleset, push_rule, insert_rule, delete_rule, pop_rule, get_rulesets, get_ruleset, clear
  • FirewallRulesHandler — zero-sized singleton routing ingress rules to XDP (if available) or TC, egress always to TC
  • IptablesFirewallRulesHandler / NftablesFirewallRulesHandler — system-wide singletons using hillock's IpTablesFirewallRules / NfTablesFirewallRuleBackend
  • Rulesets identified by deterministic UUIDs from (name, ip_version, direction) — same key across calls accumulates rules in the hillock backend

3. Config-driven dispatch

  • FirewallRuleEntry / FirewallRulesConfig — YAML/JSON firewall.firewall_rules.rulesets section parsed into 5-tuple rules
  • firewall_rules_reload.rs — hook handler parsing FirewallRuleEntryFirewallRule via FirewallRuleBuilder, pushing to all three backends
  • Wired into apply_smart_firewall_rules() in synapse-config, registered via synapse_utils::hooks hook system at startup

4. Iptables TTL sweeper

  • TTL side-table (OnceLock<Mutex<HashMap>>) tracking (ruleset_name, rule_id, ip_version, direction) → expiry Instant per rule
  • sweep_expired_ttls() called every 30s from the existing amygdala sweeper — removes expired rules from the hillock iptables backend
  • Complements hillock's own per-rule TTL threads (catches leaked rules if a thread panics)

5. 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))
  • Tail-call chain order: ip_filter → firewall_rules → fingerprint

6. Tests

  • dispatcher_integration_test.rs — 18 tests: config JSON parsing, FirewallRuleBuilder construction, trait method safety on missing backend, dispatcher no-backend no-panic
  • dispatcher_live_test.rs — 3 root-only #[ignore] tests: full dispatcher chain on veth + rule push/query/clear, iptables roundtrip, nftables roundtrip
  • All existing e2e (51 passed) and workspace tests pass; 0 new compiler warnings

Architecture diagram

config.yaml / API JSON
  firewall_rules.rulesets.{block-ssh, allow-admin, ...}
    ↓ serde deserialise
synapse_config::Config
    ↓ set_global_config() → apply_smart_firewall_rules()
synapse_utils::hooks::invoke_firewall_rules_reload()
    ↓ hook registered at startup
firewall_rules_reload::handle_reload()
    ├── FirewallRulesHandler (BPF XDP/TC, per-ifindex)
    ├── IptablesFirewallRulesHandler (filter table, system-wide)
    └── NftablesFirewallRulesHandler (inet table, system-wide)

amygdala::ReactorAction::BlockIpSrcPort
    → HillockAdapter::block_ip_src_port()
        → FirewallRulesHandler::new().push_rule(ifindex, ...)

Configured backends cached in:
    bpf_dispatcher::{xdp_disp, tc_disp}::XdpState/TcState

Files changed

28 files, +3,759 / -1,058 lines

@Gepsonka Gepsonka self-assigned this Jul 22, 2026
Copilot AI review requested due to automatic review settings July 22, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FirewallRulesBackend trait 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.

Comment on lines +19 to +23
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); }
Comment on lines +50 to 51
hillock = { version = "0.3.3", registry = "gen0sec", default-features = false, optional = true }
uuid = { version = "1", features = ["v4"], optional = true }
Comment on lines +116 to +123
// 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;
}
}
Comment on lines +392 to +396
fn has_xdp() -> bool {
let mut f = false;
crate::bpf_dispatcher::xdp_dispatcher::for_each_mut(|_, _| f = true);
f
}
Comment on lines +24 to +29
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")
})
Comment on lines +119 to +123
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"]
Comment on lines +24 to +25
#[cfg(feature = "bpf")]
pub mod firewall_rules;
Copilot AI review requested due to automatic review settings July 23, 2026 23:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when cfg is None. 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.protocol is documented as allowing "any" (wildcard), but parse_rule always 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 returns None and 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")
    })

Comment on lines +95 to +103
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;
}
}
}
Comment on lines +43 to +47
#[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);
}
Comment on lines +26 to +28
// Config-driven 5-tuple firewall rules → all backends (BPF + iptables + nftables)
#[cfg(all(target_os = "linux", feature = "amygdala-reactor"))]
pub mod firewall_rules_reload;
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.

2 participants