From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 97A111FF0AF for ; Fri, 06 Nov 2026 13:10:29 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 31FE8215AE; Fri, 06 Nov 2026 13:10:28 +0100 (CET) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-firewall v2 2/2] service: re-apply bridge sysctls when taking over from pve-firewall Date: Fri, 6 Nov 2026 13:10:14 +0100 Message-ID: <20261106121015.188115-3-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261106121015.188115-1-s.hanreich@proxmox.com> References: <20261106121015.188115-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793967022666 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.298 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: VLQZS32TLMVDEX2O5CO2OTTGYA544OMU X-Message-ID-Hash: VLQZS32TLMVDEX2O5CO2OTTGYA544OMU X-MailFrom: s.hanreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: While active, the iptables based pve-firewall sets bridge-nf-call-ip{,6}tables to 1. Switching to nftables at runtime leaves them at that value until something re-applies the sysctl.d configuration, usually the next package upgrade. Until then br_netfilter also passes bridged guest traffic to the inet table, so the host and cluster forward rules apply to it on top of the guest rules. Re-apply the configured values once each time pve-firewall hands over by removing the force-disable flag, and on daemon start. pve-firewall only sets the values while the flag exists. Checking the flag instead of the parsed configuration means an invalid entry in any firewall config cannot keep the values at 1. Without an override by the admin, this resets them to the 0 from 10-pve.conf. Re-applying the configuration instead of writing 0 keeps such overrides. pve-firewall removes its drop-in for these sysctls before handing over, so it does not affect the re-apply. Signed-off-by: Stefan Hanreich --- proxmox-firewall/src/bin/proxmox-firewall.rs | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/proxmox-firewall/src/bin/proxmox-firewall.rs b/proxmox-firewall/src/bin/proxmox-firewall.rs index e8ed477..f0b4d15 100644 --- a/proxmox-firewall/src/bin/proxmox-firewall.rs +++ b/proxmox-firewall/src/bin/proxmox-firewall.rs @@ -46,6 +46,30 @@ fn create_firewall_instance() -> Result { Ok(Firewall::new(config)) } +/// Re-applies the sysctl.d configuration of the bridge-nf-call sysctls. +/// +/// The legacy pve-firewall sets them to 1 while it is active. After switching over, br_netfilter +/// would then keep passing bridged guest traffic to the inet table as well, until something else +/// re-applies the configuration. Re-applying it instead of writing 0 keeps overrides of the admin. +fn reapply_bridge_sysctls() -> Result<(), Error> { + let output = std::process::Command::new("/usr/lib/systemd/systemd-sysctl") + .args([ + "--prefix=net.bridge.bridge-nf-call-iptables", + "--prefix=net.bridge.bridge-nf-call-ip6tables", + ]) + .output()?; + + if !output.status.success() { + bail!( + "systemd-sysctl failed with {} - {}", + output.status, + String::from_utf8_lossy(&output.stderr).trim(), + ); + } + + Ok(()) +} + fn handle_firewall() -> Result<(), Error> { let firewall = create_firewall_instance()?; @@ -94,8 +118,12 @@ fn run_firewall() -> Result<(), Error> { // we're disabled here without the need to parse the config, avoiding log-spam errors from that let force_disable_flag = std::path::Path::new(FORCE_DISABLE_FLAG_FILE); + let mut bridge_sysctls_reapplied = false; + while !term.load(Ordering::Relaxed) { if force_disable_flag.exists() { + bridge_sysctls_reapplied = false; + if let Err(error) = remove_firewall() { log::error!("unable to disable firewall: {error:?}"); } @@ -105,6 +133,13 @@ fn run_firewall() -> Result<(), Error> { } let start = Instant::now(); + if !bridge_sysctls_reapplied { + match reapply_bridge_sysctls() { + Ok(()) => bridge_sysctls_reapplied = true, + Err(error) => log::error!("unable to re-apply bridge sysctls: {error:#}"), + } + } + if let Err(error) = handle_firewall() { log::error!("error updating firewall rules: {error:#}"); } -- 2.47.3