public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-ve-rs 2/2] config: firewall: add support for legacy ipset names
Date: Fri, 12 Sep 2025 18:11:17 +0200	[thread overview]
Message-ID: <20250912161121.229819-3-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20250912161121.229819-1-s.hanreich@proxmox.com>

Proxmox VE 8.0 introduced scopes for ipsets and aliases in the
firewall configuration. Since existing firewall configuration was not
converted to this new syntax, there are still many systems that have
at least one alias / ipset name in their firewall ruleset that does
not contain any scope. proxmox-firewall failed to parse such rules and
in turn failed to create a new ruleset, which was a common occurence
when users switched from pve-firewall to proxmox-firewall.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/firewall/cluster.rs     |  4 +
 proxmox-ve-config/src/firewall/common.rs      |  4 +
 proxmox-ve-config/src/firewall/guest.rs       |  4 +
 proxmox-ve-config/src/firewall/types/ipset.rs | 97 ++++++++++++++++++-
 proxmox-ve-config/src/firewall/types/rule.rs  |  7 +-
 .../src/firewall/types/rule_match.rs          |  4 +-
 6 files changed, 114 insertions(+), 6 deletions(-)

diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
index a477a53..d82e66f 100644
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ b/proxmox-ve-config/src/firewall/cluster.rs
@@ -53,6 +53,10 @@ impl Config {
         &self.config.ipsets
     }
 
+    pub fn ipset(&self, name: &str) -> Option<&Ipset> {
+        self.config.ipsets.get(name)
+    }
+
     pub fn alias(&self, name: &str) -> Option<&Alias> {
         self.config.alias(name)
     }
diff --git a/proxmox-ve-config/src/firewall/common.rs b/proxmox-ve-config/src/firewall/common.rs
index 3999168..d77fe72 100644
--- a/proxmox-ve-config/src/firewall/common.rs
+++ b/proxmox-ve-config/src/firewall/common.rs
@@ -189,6 +189,10 @@ where
         &self.ipsets
     }
 
+    pub fn ipset(&self, name: &str) -> Option<&Ipset> {
+        self.ipsets.get(name)
+    }
+
     pub fn alias(&self, name: &str) -> Option<&Alias> {
         self.aliases.get(name)
     }
diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs
index 349fdb4..65ef29d 100644
--- a/proxmox-ve-config/src/firewall/guest.rs
+++ b/proxmox-ve-config/src/firewall/guest.rs
@@ -108,6 +108,10 @@ impl Config {
         self.vmid
     }
 
+    pub fn ipset(&self, name: &str) -> Option<&Ipset> {
+        self.config.ipset(name)
+    }
+
     pub fn alias(&self, name: &str) -> Option<&Alias> {
         self.config.alias(name)
     }
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index 2df6943..3f93bb2 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -10,6 +10,7 @@ use crate::guest::vm::NetworkConfig;
 
 use super::alias::RuleAliasName;
 
+/// The scope of an ipset.
 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
 pub enum IpsetScope {
     Datacenter,
@@ -42,8 +43,47 @@ impl Display for IpsetScope {
     }
 }
 
-#[derive(Debug, Clone)]
-#[cfg_attr(test, derive(Eq, PartialEq))]
+/// An ipset name in the firewall rules without any scope identifier.
+///
+/// Legacy ipset names start with a +, followed by an alphabetic character and only contain
+/// alphanumeric characters or hyphens and underscores.
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[repr(transparent)]
+pub struct LegacyIpsetName(String);
+
+impl AsRef<str> for LegacyIpsetName {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl FromStr for LegacyIpsetName {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("+") {
+            if let Some((name, "")) = match_name(name) {
+                return Ok(Self(name.to_string()));
+            }
+
+            bail!("not a valid ipset name: {s}");
+        }
+
+        bail!("ipset name does not start with '+': {s}");
+    }
+}
+
+impl Display for LegacyIpsetName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+/// The new format of ipset names in firewall rules (with scope).
+///
+/// It starts with a plus sign, then a scope [`IpsetScope`], followed by a slash, and then has the
+/// same format as [`LegacyIpsetName`].
+#[derive(Debug, Clone, Eq, PartialEq)]
 pub struct IpsetName {
     pub scope: IpsetScope,
     pub name: String,
@@ -90,6 +130,39 @@ impl Display for IpsetName {
     }
 }
 
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum RuleIpsetName {
+    Scoped(IpsetName),
+    Legacy(LegacyIpsetName),
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(RuleIpsetName);
+
+impl FromStr for RuleIpsetName {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(scoped_name) = s.parse() {
+            return Ok(Self::Scoped(scoped_name));
+        }
+
+        if let Ok(legacy_name) = s.parse() {
+            return Ok(Self::Legacy(legacy_name));
+        }
+
+        bail!("is neither a valid scoped nor legacy ipset name: {s}");
+    }
+}
+
+impl Display for RuleIpsetName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Scoped(ipset_name) => ipset_name.fmt(f),
+            Self::Legacy(ipset_name) => ipset_name.fmt(f),
+        }
+    }
+}
+
 #[derive(Debug, Clone)]
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub enum IpsetAddress {
@@ -304,6 +377,26 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_parse_legacy_ipset_name() {
+        for test_case in [
+            ("+proxmox_123", "proxmox_123"),
+            ("+proxmox_---123", "proxmox_---123"),
+        ] {
+            let ipset_name = test_case
+                .0
+                .parse::<LegacyIpsetName>()
+                .expect("valid ipset name");
+
+            assert_eq!(ipset_name, LegacyIpsetName(test_case.1.to_string(),))
+        }
+
+        for name in ["guest/proxmox_123", "+-qwe", "+1qwe"] {
+            name.parse::<LegacyIpsetName>()
+                .expect_err("invalid ipset name");
+        }
+    }
+
     #[test]
     fn test_parse_ipset_address() {
         let mut ipset_address = "10.0.0.1"
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
index 049f270..3b2b8ca 100644
--- a/proxmox-ve-config/src/firewall/types/rule.rs
+++ b/proxmox-ve-config/src/firewall/types/rule.rs
@@ -251,8 +251,8 @@ mod tests {
 
     use crate::firewall::types::{
         address::{IpEntry, IpList},
-        ipset::{IpsetName, IpsetScope},
         alias::{AliasName, AliasScope, RuleAliasName},
+        ipset::{IpsetName, IpsetScope, RuleIpsetName},
         log::LogLevel,
         rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
     };
@@ -385,7 +385,10 @@ mod tests {
                                 AliasScope::Datacenter,
                                 "test"
                             ))),
-                            IpAddrMatch::Set(IpsetName::new(IpsetScope::Datacenter, "test"),),
+                            IpAddrMatch::Set(RuleIpsetName::Scoped(IpsetName::new(
+                                IpsetScope::Datacenter,
+                                "test"
+                            )),),
                         )
                         .unwrap()
                     ),
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
index 2be84c5..9b474d2 100644
--- a/proxmox-ve-config/src/firewall/types/rule_match.rs
+++ b/proxmox-ve-config/src/firewall/types/rule_match.rs
@@ -13,7 +13,7 @@ use proxmox_sortable_macro::sortable;
 use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
 use crate::firewall::types::address::IpList;
 use crate::firewall::types::alias::RuleAliasName;
-use crate::firewall::types::ipset::IpsetName;
+use crate::firewall::types::ipset::RuleIpsetName;
 use crate::firewall::types::log::LogLevel;
 use crate::firewall::types::port::PortList;
 use crate::firewall::types::rule::{Direction, Verdict};
@@ -253,7 +253,7 @@ impl IpMatch {
 #[cfg_attr(test, derive(Eq, PartialEq))]
 pub enum IpAddrMatch {
     Ip(IpList),
-    Set(IpsetName),
+    Set(RuleIpsetName),
     Alias(RuleAliasName),
 }
 
-- 
2.47.3


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  parent reply	other threads:[~2025-09-12 16:11 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-12 16:11 [pve-devel] [PATCH proxmox{-ve-rs, -firewall} 0/3] Add support for legacy ipset / alias names Stefan Hanreich
2025-09-12 16:11 ` [pve-devel] [PATCH proxmox-ve-rs 1/2] config: firewall: add support for legacy " Stefan Hanreich
2025-09-12 16:11 ` Stefan Hanreich [this message]
2025-09-12 16:11 ` [pve-devel] [PATCH proxmox-firewall 1/1] fix #6107: add support for legacy ipset / " Stefan Hanreich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250912161121.229819-3-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal