From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 38F971FF142 for ; Mon, 16 Feb 2026 11:44:37 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B631FCBF5; Mon, 16 Feb 2026 11:44:42 +0100 (CET) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC proxmox 12/22] firewall-api-types: add FirewallIpsetReference type Date: Mon, 16 Feb 2026 11:43:50 +0100 Message-ID: <20260216104401.3959270-13-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260216104401.3959270-1-dietmar@proxmox.com> References: <20260216104401.3959270-1-dietmar@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.570 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: S527VWR7DDKGOPBC54VMR3VZANRQYOVI X-Message-ID-Hash: S527VWR7DDKGOPBC54VMR3VZANRQYOVI X-MailFrom: dietmar@zilli.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: This adds a new type to reference ipsets with proper scope handling (Datacenter, Guest, SDN, or None for legacy ipsets). The implementation includes: - FirewallIpsetScope enum for scope variants - FirewallIpsetReference struct with validation - Proper encapsulation with constructor and accessor methods - FromStr implementation for parsing ipset references Signed-off-by: Dietmar Maurer --- proxmox-firewall-api-types/src/ipset.rs | 191 ++++++++++++++++++++++++ proxmox-firewall-api-types/src/lib.rs | 3 + 2 files changed, 194 insertions(+) create mode 100644 proxmox-firewall-api-types/src/ipset.rs diff --git a/proxmox-firewall-api-types/src/ipset.rs b/proxmox-firewall-api-types/src/ipset.rs new file mode 100644 index 00000000..02659394 --- /dev/null +++ b/proxmox-firewall-api-types/src/ipset.rs @@ -0,0 +1,191 @@ +use std::fmt; +use std::str::FromStr; + +use anyhow::{bail, Error}; + +#[cfg(feature = "enum-fallback")] +use proxmox_fixed_string::FixedString; + +/// The scope of an ipset. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum FirewallIpsetScope { + /// Datacenter scope. + Datacenter, + /// Guest scope. + Guest, + /// SDN scope. + Sdn, + /// No scope (e.g. for legacy ipsets). + None, + #[cfg(feature = "enum-fallback")] + /// Unknown variants for forward compatibility. + UnknownEnumValue(FixedString), +} + +/// A reference to an ipset, including its scope. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FirewallIpsetReference { + scope: FirewallIpsetScope, + name: String, +} + +impl FirewallIpsetReference { + pub fn new(scope: FirewallIpsetScope, name: String) -> Result { + verify_ipset_name(&name)?; + Ok(Self { scope, name }) + } + + pub fn scope(&self) -> FirewallIpsetScope { + self.scope + } + + pub fn name(&self) -> &str { + &self.name + } +} + +fn verify_ipset_name(name: &str) -> Result<(), Error> { + if name.is_empty() { + bail!("ipset name cannot be empty"); + } + + if !name.starts_with(|c: char| c.is_ascii_alphabetic()) { + bail!("ipset name must start with an ASCII letter"); + } + + if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_') { + bail!("ipset name can only contain ASCII letters, digits, hyphens, and underscores"); + } + + Ok(()) +} + +impl fmt::Display for FirewallIpsetReference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + "+".fmt(f)?; + match self.scope { + FirewallIpsetScope::Datacenter => write!(f, "dc/{}", self.name), + FirewallIpsetScope::Guest => write!(f, "guest/{}", self.name), + FirewallIpsetScope::Sdn => write!(f, "sdn/{}", self.name), + #[cfg(feature = "enum-fallback")] + FirewallIpsetScope::UnknownEnumValue(scope) => write!(f, "{}/{}", scope, self.name), + FirewallIpsetScope::None => self.name.fmt(f), + } + } +} + +impl FromStr for FirewallIpsetReference { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + let s = match s.strip_prefix('+') { + Some(rest) => rest, + None => bail!("ipset reference must start with '+'"), + }; + + if s.is_empty() { + bail!("empty firewall ipset specification"); + } + + let (scope, name) = match s.split_once('/') { + Some(("dc", alias)) => (FirewallIpsetScope::Datacenter, alias), + Some(("guest", alias)) => (FirewallIpsetScope::Guest, alias), + Some(("sdn", alias)) => (FirewallIpsetScope::Sdn, alias), + #[cfg(not(feature = "enum-fallback"))] + Some((scope, _alias)) => bail!("invalid firewall ipset reference scope: {scope}"), + #[cfg(feature = "enum-fallback")] + Some((scope, alias)) => ( + FirewallIpsetScope::UnknownEnumValue(FixedString::from_str(scope)?), + alias, + ), + None => (FirewallIpsetScope::None, s), + }; + + Self::new(scope, name.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ipset_name() { + for test_case in [ + ( + "+dc/proxmox-123", + FirewallIpsetScope::Datacenter, + "proxmox-123", + ), + ( + "+guest/proxmox_123", + FirewallIpsetScope::Guest, + "proxmox_123", + ), + ] { + let ipset_name = test_case + .0 + .parse::() + .expect("valid ipset name"); + + assert_eq!( + ipset_name, + FirewallIpsetReference { + scope: test_case.1, + name: test_case.2.to_string(), + } + ) + } + + for name in ["+dc/", "guest/proxmox_123"] { + name.parse::() + .expect_err("invalid ipset name"); + } + + #[cfg(feature = "enum-fallback")] + for name in ["+guests/proxmox_123", "+nonsense/abc"] { + name.parse::() + .expect("valid ipset name (enum fallback feature)"); + } + } + + #[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::() + .expect("valid ipset name"); + + assert_eq!( + ipset_name, + FirewallIpsetReference { + scope: FirewallIpsetScope::None, + name: test_case.1.to_string(), + } + ) + } + + for name in ["guest/proxmox_123", "+-qwe", "+1qwe"] { + name.parse::() + .expect_err("invalid ipset name"); + } + } + + #[test] + fn test_new_ipset() { + let ipset = + FirewallIpsetReference::new(FirewallIpsetScope::Datacenter, "proxmox-123".to_string()) + .expect("valid ipset name"); + assert_eq!(ipset.scope(), FirewallIpsetScope::Datacenter); + assert_eq!(ipset.name(), "proxmox-123"); + + FirewallIpsetReference::new(FirewallIpsetScope::Datacenter, "+invalid".to_string()) + .expect_err("invalid ipset name"); + } +} diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs index 610282bb..abd78c98 100644 --- a/proxmox-firewall-api-types/src/lib.rs +++ b/proxmox-firewall-api-types/src/lib.rs @@ -4,6 +4,9 @@ pub use conntrack::FirewallConntrackHelper; mod icmp_type; pub use icmp_type::{FirewallIcmpType, FirewallIcmpTypeName}; +mod ipset; +pub use ipset::{FirewallIpsetReference, FirewallIpsetScope}; + mod log; pub use log::{ FirewallLogLevel, FirewallLogRateLimit, FirewallPacketRate, FirewallPacketRateTimescale, -- 2.47.3