From 3439753379d14e783d2e6a016405ec41b941a5ae Mon Sep 17 00:00:00 2001 From: Rishikpulhani Date: Thu, 14 Aug 2025 17:08:34 +0530 Subject: [PATCH] feat: Add firewall-reload command for nftables systems On systems using nftables directly without firewalld, restarting the nftables.service would flush all of Netavark's rules, breaking container networking. This introduces a new "oneshot" command, `netavark firewall-reload`, which reads the container network state from /run/containers/netavark/ and re-applies all necessary firewall rules. To automate this, a new systemd service, `netavark-nftables-reload.service`, is added. This service is procedurally linked to `nftables.service` and triggers the reload command automatically whenever the main nftables service is started or reloaded. Fixes: #1258 Signed-off-by: Rishikpulhani --- Makefile | 4 +- .../netavark-nftables-reload.service.in | 16 ++++++ rpm/netavark.spec | 3 + src/commands/firewall_reload.rs | 56 +++++++++++++++++++ src/commands/mod.rs | 1 + src/main.rs | 5 ++ test/250-bridge-nftables.bats | 30 ++++++++++ 7 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 contrib/systemd/system/netavark-nftables-reload.service.in create mode 100644 src/commands/firewall_reload.rs diff --git a/Makefile b/Makefile index 1b5b3e208..1bfd69513 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,8 @@ docs: ## build the docs on the host $(MAKE) -C docs NV_UNIT_FILES = contrib/systemd/system/netavark-dhcp-proxy.service \ - contrib/systemd/system/netavark-firewalld-reload.service + contrib/systemd/system/netavark-firewalld-reload.service \ + contrib/systemd/system/netavark-nftables-reload.service %.service: %.service.in sed -e 's;@@NETAVARK@@;$(LIBEXECPODMAN)/netavark;g' $< >$@.tmp.$$ \ @@ -106,6 +107,7 @@ install: $(NV_UNIT_FILES) install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.socket ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.socket install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.service ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.service install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-firewalld-reload.service ${DESTDIR}${SYSTEMDDIR}/netavark-firewalld-reload.service + install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-nftables-reload.service ${DESTDIR}${SYSTEMDDIR}/netavark-nftables-reload.service .PHONY: uninstall uninstall: diff --git a/contrib/systemd/system/netavark-nftables-reload.service.in b/contrib/systemd/system/netavark-nftables-reload.service.in new file mode 100644 index 000000000..8653d23b4 --- /dev/null +++ b/contrib/systemd/system/netavark-nftables-reload.service.in @@ -0,0 +1,16 @@ +[Unit] +Description=Restore Netavark firewall rules after nftables service is reloaded +# This ensures our service is always started/restarted along with nftables. +# It also ensures this service is stopped if nftables is stopped. +PartOf=nftables.service +# This ensures nftables has finished starting before we run. +After=nftables.service + +[Service] +Type=oneshot +# This uses the placeholder, which will be replaced by the Makefile during the build. +ExecStart=@@NETAVARK@@ firewall-reload + +[Install] +# This tells systemd that this service should be enabled when nftables is enabled. +WantedBy=nftables.service diff --git a/rpm/netavark.spec b/rpm/netavark.spec index 521f7642f..f7795a123 100644 --- a/rpm/netavark.spec +++ b/rpm/netavark.spec @@ -118,10 +118,12 @@ cd docs %preun %systemd_preun %{name}-dhcp-proxy.service %systemd_preun %{name}-firewalld-reload.service +%systemd_preun %{name}-nftables-reload.service %postun %systemd_postun %{name}-dhcp-proxy.service %systemd_postun %{name}-firewalld-reload.service +%systemd_postun %{name}-nftables-reload.service %files %license LICENSE @@ -136,6 +138,7 @@ cd docs %{_unitdir}/%{name}-dhcp-proxy.service %{_unitdir}/%{name}-dhcp-proxy.socket %{_unitdir}/%{name}-firewalld-reload.service +%{_unitdir}/%{name}-nftables-reload.service %changelog %autochangelog diff --git a/src/commands/firewall_reload.rs b/src/commands/firewall_reload.rs new file mode 100644 index 000000000..bf1128acc --- /dev/null +++ b/src/commands/firewall_reload.rs @@ -0,0 +1,56 @@ +use crate::{ + error::{ErrorWrap, NetavarkResult}, + firewall::{get_supported_firewall_driver, state::read_fw_config}, + network::constants, +}; +use std::{ + ffi::{OsStr, OsString}, + path::Path, +}; +use zbus::blocking::Connection; + +pub fn firewall_reload(config_dir: Option) -> NetavarkResult<()> { + // Set the path to the directory where Podman stores the container network state. + let config_dir = Path::new( + config_dir + .as_deref() + .unwrap_or(OsStr::new(constants::DEFAULT_CONFIG_DIR)), // path to the config dir mainatined by podman + ); + log::debug!("looking for firewall configs in {config_dir:?}"); + + let conn = Connection::system().ok(); + + reload_rules(config_dir, &conn)?; + + Ok(()) +} + +// This function is copied directly from firewalld_reload.rs. +fn reload_rules(config_dir: &Path, conn: &Option) -> NetavarkResult<()> { + reload_rules_inner(config_dir, conn)?; + Ok(()) +} + +// This is the core logic, also copied directly. +fn reload_rules_inner(config_dir: &Path, conn: &Option) -> NetavarkResult<()> { + // read_fw_config reads all the JSON files from `/run/containers/netavark/` + let conf = read_fw_config(config_dir).wrap("read firewall config")?; + + // If there are no config files, there are no running containers, so we do nothing. + if let Some(conf) = conf { + // Get the appropriate firewall driver + let fw_driver = get_supported_firewall_driver(Some(conf.driver))?; + + // Loop through each network configuration and restore its rules. + for net in conf.net_confs { + fw_driver.setup_network(net, conn)?; + } + // Loop through each container's port mappings and restore them. + for port in &conf.port_confs { + fw_driver.setup_port_forward(port.into(), conn)?; + } + log::info!("Successfully reloaded firewall rules"); + } + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 46ae2ac6d..c6ab69579 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,6 +3,7 @@ use std::ffi::OsString; use crate::error::{NetavarkError, NetavarkResult}; pub mod dhcp_proxy; +pub mod firewall_reload; pub mod firewalld_reload; pub mod setup; pub mod teardown; diff --git a/src/main.rs b/src/main.rs index 4ee23416d..b13f34c24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::ffi::OsString; use clap::{Parser, Subcommand}; use netavark::commands::dhcp_proxy; +use netavark::commands::firewall_reload; use netavark::commands::firewalld_reload; use netavark::commands::setup; use netavark::commands::teardown; @@ -51,6 +52,9 @@ enum SubCommand { /// Listen for the firewalld reload event and reload fw rules #[command(name = "firewalld-reload")] FirewallDReload, + // Re-applies firewall rules for all networks. + #[command(name = "firewall-reload")] + FirewallReload, } fn main() { @@ -84,6 +88,7 @@ fn main() { SubCommand::Version(version) => version.exec(), SubCommand::DHCPProxy(proxy) => dhcp_proxy::serve(proxy), SubCommand::FirewallDReload => firewalld_reload::listen(config), + SubCommand::FirewallReload => firewall_reload::firewall_reload(config), }; match result { diff --git a/test/250-bridge-nftables.bats b/test/250-bridge-nftables.bats index 7d8f8a054..ba0ff8b68 100644 --- a/test/250-bridge-nftables.bats +++ b/test/250-bridge-nftables.bats @@ -985,6 +985,36 @@ net/ipv4/conf/podman1/rp_filter = 2" @test "$fw_driver - port forwarding ipv4 - tcp with firewalld reload" { test_port_fw firewalld_reload=true } +@test "$fw_driver - test firewall-reload" { + # setup a simple bridge network + run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) + + # verify the rules are there initially + check_simple_bridge_nftables + + # check that the firewall config files exist + net_id=$(jq -r '.network_info.podman.id' < "${TESTSDIR}/testfiles/simplebridge.json") + config_file="$NETAVARK_TMPDIR/config/firewall/networks/$net_id" + run_helper test -f "$config_file" + assert "$status" == "0" "network config file $config_file should exist" + # flush all nftables rules + run_in_host_netns nft flush ruleset + + # verify the netavark table is gone + expected_rc=1 run_in_host_netns nft list table inet netavark + assert "$output" =~ "Error: No such file or directory" "netavark table should be gone" + + # run firewall-reload to restore the rules + RUST_LOG=netavark=debug run_netavark firewall-reload + assert "$output" =~ "\[INFO netavark::commands::firewall_reload\] Successfully reloaded firewall rules" "firewall-reload success message" + + # check that the rules are back + check_simple_bridge_nftables + + # teardown the network + run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path) +} + function check_simple_bridge_nftables() { # check nftables POSTROUTING chain