From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id C2B111FF142 for ; Mon, 16 Feb 2026 11:44:44 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id F4017CD60; Mon, 16 Feb 2026 11:44:45 +0100 (CET) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC proxmox 14/22] firewall-api-types: add firewall address types Date: Mon, 16 Feb 2026 11:43:52 +0100 Message-ID: <20260216104401.3959270-15-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.568 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: L2CUSV7BDA4QSLV7F3W6MUESW5Y6JTUO X-Message-ID-Hash: L2CUSV7BDA4QSLV7F3W6MUESW5Y6JTUO 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 new types for representing firewall address matches: - FirewallAddressMatch: enum for IP lists, ipset references, or alias references - FirewallAddressList: validated list of address entries with consistent address family - FirewallAddressEntry: enum for CIDR or IP range entries The implementation includes: - Proper encapsulation with constructor and accessor methods - Address family validation in FirewallAddressList::new() - FromStr implementations for parsing address specifications - Integration with existing FirewallIpsetReference and FirewallAliasReference types Signed-off-by: Dietmar Maurer --- proxmox-firewall-api-types/Cargo.toml | 1 + proxmox-firewall-api-types/src/address.rs | 225 ++++++++++++++++++++++ proxmox-firewall-api-types/src/lib.rs | 3 + 3 files changed, 229 insertions(+) create mode 100644 proxmox-firewall-api-types/src/address.rs diff --git a/proxmox-firewall-api-types/Cargo.toml b/proxmox-firewall-api-types/Cargo.toml index 97b477b8..8b77b522 100644 --- a/proxmox-firewall-api-types/Cargo.toml +++ b/proxmox-firewall-api-types/Cargo.toml @@ -22,3 +22,4 @@ serde_plain = { workspace = true } proxmox-schema = { workspace = true, features = ["api-macro"] } proxmox-serde = { workspace = true, features = ["perl"] } proxmox-fixed-string = { workspace = true, optional = true } +proxmox-network-types = { workspace = true, features = [ "api-types" ] } diff --git a/proxmox-firewall-api-types/src/address.rs b/proxmox-firewall-api-types/src/address.rs new file mode 100644 index 00000000..46166352 --- /dev/null +++ b/proxmox-firewall-api-types/src/address.rs @@ -0,0 +1,225 @@ +use std::fmt; +use std::str::FromStr; + +use super::{FirewallAliasReference, FirewallIpsetReference}; + +use anyhow::{bail, Error}; +use proxmox_network_types::ip_address::{Cidr, Family, IpRange}; +use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema}; + +/// A match for source or destination address. +#[derive(Clone, Debug, PartialEq)] +pub enum FirewallAddressMatch { + /// IP address list match. + Ip(FirewallAddressList), + /// IP set match. + Ipset(FirewallIpsetReference), + /// Alias match. + Alias(FirewallAliasReference), +} + +impl ApiType for FirewallAddressMatch { + const API_SCHEMA: Schema = StringSchema::new( + r#"Restrict source or destination packet address. + This can refer to a single IP address, + an IP set ('+ipsetname') or an IP alias definition. You can also specify + an address range like '20.34.101.207-201.3.9.99', or a list of IP + addresses and networks (entries are separated by comma). Please do not + mix IPv4 and IPv6 addresses inside such lists."#, + ) + .format(&ApiStringFormat::VerifyFn(verify_firewall_address_match)) + .max_length(512) + .schema(); +} + +fn verify_firewall_address_match(s: &str) -> Result<(), Error> { + FirewallAddressMatch::from_str(s).map(|_| ()) +} + +serde_plain::derive_deserialize_from_fromstr!(FirewallAddressMatch, "valid firewall address match"); +serde_plain::derive_serialize_from_display!(FirewallAddressMatch); + +/// A firewall address entry (CIDR or Range). +#[derive(Clone, Debug, PartialEq)] +pub enum FirewallAddressEntry { + /// CIDR notation. + Cidr(Cidr), + /// IP range. + Range(IpRange), +} + +impl FirewallAddressEntry { + /// Get the address family of the entry. + pub fn family(&self) -> Family { + match self { + Self::Cidr(cidr) => cidr.family(), + Self::Range(range) => range.family(), + } + } +} + +impl fmt::Display for FirewallAddressEntry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Cidr(ip) => ip.fmt(f), + Self::Range(range) => range.fmt(f), + } + } +} + +impl FromStr for FirewallAddressEntry { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Ok(cidr) = s.parse() { + return Ok(FirewallAddressEntry::Cidr(cidr)); + } + + if let Ok(range) = s.parse() { + return Ok(FirewallAddressEntry::Range(range)); + } + + bail!("Invalid IP entry: {s}"); + } +} + +/// A list of firewall address entries. +#[derive(Clone, Debug, PartialEq)] +pub struct FirewallAddressList { + // guaranteed to have the same family + entries: Vec, + family: Family, +} + +impl FirewallAddressList { + /// Creates a new address list from a vector of entries. + /// + /// Validates that all entries have the same address family. + pub fn new(entries: Vec) -> Result { + if entries.is_empty() { + bail!("empty address list"); + } + + let family = entries[0].family(); + + for entry in &entries[1..] { + if entry.family() != family { + bail!("address list entries must have the same address family"); + } + } + + Ok(Self { entries, family }) + } + + /// Returns the entries of the address list. + pub fn entries(&self) -> &[FirewallAddressEntry] { + &self.entries + } + + /// Returns the address family of the address list. + pub fn family(&self) -> Family { + self.family + } +} + +impl fmt::Display for FirewallAddressMatch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Ip(list) => { + for (i, entry) in list.entries.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + entry.fmt(f)?; + } + Ok(()) + } + Self::Ipset(reference) => reference.fmt(f), + Self::Alias(reference) => reference.fmt(f), + } + } +} + +impl FromStr for FirewallAddressMatch { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + if s.is_empty() { + bail!("empty firewall address specification"); + } + + if s.starts_with('+') { + return Ok(FirewallAddressMatch::Ipset( + s.parse::()?, + )); + } + + if let Ok(alias_ref) = s.parse::() { + return Ok(FirewallAddressMatch::Alias(alias_ref)); + } + + let mut entries = Vec::new(); + + for element in s.split(',') { + let entry: FirewallAddressEntry = element.parse()?; + entries.push(entry); + } + + Ok(Self::Ip(FirewallAddressList::new(entries)?)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ip_addr_match() { + for input in [ + "10.0.0.0/8", + "10.0.0.0/8,192.168.0.0-192.168.255.255,172.16.0.1", + "dc/test", + "+guest/proxmox", + ] { + input + .parse::() + .expect("valid ip match"); + } + + for input in [ + "10.0.0.0/", + "10.0.0.0/8,192.168.256.0-192.168.255.255,172.16.0.1", + "dc/test!invalid", + "+guest/", + "", + ] { + input + .parse::() + .expect_err("invalid ip match"); + } + } + + #[test] + fn test_firewall_address_list_mixed_families() { + let entries = vec![ + "10.0.0.1/32".parse().unwrap(), + "fe80::1/128".parse().unwrap(), + ]; + + assert!(FirewallAddressList::new(entries).is_err()); + } + + #[test] + fn test_firewall_address_list_valid() { + let entries = vec![ + "10.0.0.1/32".parse().unwrap(), + "192.168.1.1/32".parse().unwrap(), + ]; + + let list = FirewallAddressList::new(entries).expect("valid list"); + assert_eq!(list.family(), Family::V4); + assert_eq!(list.entries().len(), 2); + } +} diff --git a/proxmox-firewall-api-types/src/lib.rs b/proxmox-firewall-api-types/src/lib.rs index 8fae5042..c6f00250 100644 --- a/proxmox-firewall-api-types/src/lib.rs +++ b/proxmox-firewall-api-types/src/lib.rs @@ -1,3 +1,6 @@ +mod address; +pub use address::{FirewallAddressEntry, FirewallAddressList, FirewallAddressMatch}; + mod alias; pub use alias::{FirewallAliasReference, FirewallAliasScope}; -- 2.47.3