From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id A298E90939 for ; Tue, 2 Apr 2024 19:16:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A2DDBA74E for ; Tue, 2 Apr 2024 19:16:38 +0200 (CEST) Received: from lana.proxmox.com (unknown [94.136.29.99]) by firstgate.proxmox.com (Proxmox) with ESMTP for ; Tue, 2 Apr 2024 19:16:32 +0200 (CEST) Received: by lana.proxmox.com (Postfix, from userid 10043) id EA9942C3449; Tue, 2 Apr 2024 19:16:30 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Cc: Stefan Hanreich , Wolfgang Bumiller Date: Tue, 2 Apr 2024 19:16:00 +0200 Message-Id: <20240402171629.536804-9-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240402171629.536804-1-s.hanreich@proxmox.com> References: <20240402171629.536804-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.333 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 Subject: [pve-devel] [PATCH proxmox-firewall 08/37] config: firewall: add types for ipsets X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 02 Apr 2024 17:16:42 -0000 Co-authored-by: Wolfgang Bumiller Signed-off-by: Stefan Hanreich --- proxmox-ve-config/src/firewall/types/ipset.rs | 345 ++++++++++++++++++ proxmox-ve-config/src/firewall/types/mod.rs | 2 + 2 files changed, 347 insertions(+) create mode 100644 proxmox-ve-config/src/firewall/types/ipset.rs diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs new file mode 100644 index 0000000..32db51b --- /dev/null +++ b/proxmox-ve-config/src/firewall/types/ipset.rs @@ -0,0 +1,345 @@ +use core::fmt::Display; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +use anyhow::{bail, format_err, Error}; +use serde_with::DeserializeFromStr; + +use crate::firewall::parse::match_non_whitespace; +use crate::firewall::types::address::Cidr; +use crate::firewall::types::alias::AliasName; +use crate::guest::vm::NetworkConfig; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum IpsetScope { + Datacenter, + Guest, +} + +impl FromStr for IpsetScope { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "+dc" => IpsetScope::Datacenter, + "+guest" => IpsetScope::Guest, + _ => bail!("invalid scope for ipset: {s}"), + }) + } +} + +impl Display for IpsetScope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = match self { + Self::Datacenter => "dc", + Self::Guest => "guest", + }; + + f.write_str(prefix) + } +} + +#[derive(Debug, Clone, DeserializeFromStr)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct IpsetName { + pub scope: IpsetScope, + pub name: String, +} + +impl IpsetName { + pub fn new(scope: IpsetScope, name: impl Into) -> Self { + Self { + scope, + name: name.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn scope(&self) -> IpsetScope { + self.scope + } +} + +impl FromStr for IpsetName { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.split_once('/') { + Some((prefix, name)) if !name.is_empty() => Ok(Self { + scope: prefix.parse()?, + name: name.to_string(), + }), + _ => { + bail!("Invalid IPSet name: {s}") + } + } + } +} + +impl Display for IpsetName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.scope, self.name) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum IpsetAddress { + Alias(AliasName), + Cidr(Cidr), +} + +impl FromStr for IpsetAddress { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Ok(cidr) = s.parse() { + return Ok(IpsetAddress::Cidr(cidr)); + } + + if let Ok(name) = s.parse() { + return Ok(IpsetAddress::Alias(name)); + } + + bail!("Invalid address in IPSet: {s}") + } +} + +impl> From for IpsetAddress { + fn from(cidr: T) -> Self { + IpsetAddress::Cidr(cidr.into()) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct IpsetEntry { + pub nomatch: bool, + pub address: IpsetAddress, + pub comment: Option, +} + +impl> From for IpsetEntry { + fn from(value: T) -> Self { + Self { + nomatch: false, + address: value.into(), + comment: None, + } + } +} + +impl FromStr for IpsetEntry { + type Err = Error; + + fn from_str(line: &str) -> Result { + let line = line.trim_start(); + + let (nomatch, line) = match line.strip_prefix('!') { + Some(line) => (true, line), + None => (false, line), + }; + + let (address, line) = + match_non_whitespace(line.trim_start()).ok_or_else(|| format_err!("missing value"))?; + + let address: IpsetAddress = address.parse()?; + let line = line.trim_start(); + + let comment = match line.strip_prefix('#') { + Some(comment) => Some(comment.trim().to_string()), + None if !line.is_empty() => bail!("trailing characters in ipset entry: {line:?}"), + None => None, + }; + + Ok(Self { + nomatch, + address, + comment, + }) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Ipfilter<'a> { + index: i64, + ipset: &'a Ipset, +} + +impl Ipfilter<'_> { + pub fn index(&self) -> i64 { + self.index + } + + pub fn ipset(&self) -> &Ipset { + self.ipset + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Ipset { + pub name: IpsetName, + set: Vec, + pub comment: Option, +} + +impl Ipset { + pub const fn new(name: IpsetName) -> Self { + Self { + name, + set: Vec::new(), + comment: None, + } + } + + pub fn name(&self) -> &IpsetName { + &self.name + } + + pub fn from_parts(scope: IpsetScope, name: impl Into) -> Self { + Self::new(IpsetName::new(scope, name)) + } + + pub(crate) fn parse_entry(&mut self, line: &str) -> Result<(), Error> { + self.set.push(line.parse()?); + Ok(()) + } + + pub fn ipfilter(&self) -> Option { + if self.name.scope() != IpsetScope::Guest { + return None; + } + + let name = self.name.name(); + + if let Some(key) = name.strip_prefix("ipfilter-") { + let id = NetworkConfig::index_from_net_key(key); + + if let Ok(id) = id { + return Some(Ipfilter { + index: id, + ipset: self, + }); + } + } + + None + } +} + +impl Deref for Ipset { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.set + } +} + +impl DerefMut for Ipset { + #[inline] + fn deref_mut(&mut self) -> &mut Vec { + &mut self.set + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_ipset_name() { + for test_case in [ + ("+dc/proxmox-123", IpsetScope::Datacenter, "proxmox-123"), + ("+guest/proxmox_123", IpsetScope::Guest, "proxmox_123"), + ] { + let ipset_name = test_case.0.parse::().expect("valid ipset name"); + + assert_eq!( + ipset_name, + IpsetName { + scope: test_case.1, + name: test_case.2.to_string(), + } + ) + } + + for name in ["+dc/", "+guests/proxmox_123", "guest/proxmox_123"] { + name.parse::().expect_err("invalid ipset name"); + } + } + + #[test] + fn test_parse_ipset_address() { + let mut ipset_address = "10.0.0.1" + .parse::() + .expect("valid ipset address"); + assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv4(..)))); + + ipset_address = "fe80::1/64" + .parse::() + .expect("valid ipset address"); + assert!(matches!(ipset_address, IpsetAddress::Cidr(Cidr::Ipv6(..)))); + + ipset_address = "dc/proxmox-123" + .parse::() + .expect("valid ipset address"); + assert!(matches!(ipset_address, IpsetAddress::Alias(..))); + + ipset_address = "guest/proxmox_123" + .parse::() + .expect("valid ipset address"); + assert!(matches!(ipset_address, IpsetAddress::Alias(..))); + } + + #[test] + fn test_ipfilter() { + let mut ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-net0"); + ipset.ipfilter().expect("is an ipfilter"); + + ipset = Ipset::from_parts(IpsetScope::Guest, "ipfilter-qwe"); + assert!(ipset.ipfilter().is_none()); + + ipset = Ipset::from_parts(IpsetScope::Guest, "proxmox"); + assert!(ipset.ipfilter().is_none()); + + ipset = Ipset::from_parts(IpsetScope::Datacenter, "ipfilter-net0"); + assert!(ipset.ipfilter().is_none()); + } + + #[test] + fn test_parse_ipset_entry() { + let mut entry = "!10.0.0.1 # qweqweasd" + .parse::() + .expect("valid ipset entry"); + + assert_eq!( + entry, + IpsetEntry { + nomatch: true, + comment: Some("qweqweasd".to_string()), + address: IpsetAddress::Cidr(Cidr::new_v4([10, 0, 0, 1], 32).unwrap()) + } + ); + + entry = "fe80::1/48" + .parse::() + .expect("valid ipset entry"); + + assert_eq!( + entry, + IpsetEntry { + nomatch: false, + comment: None, + address: IpsetAddress::Cidr( + Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48).unwrap() + ) + } + ) + } +} diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs index 69b69f4..5833787 100644 --- a/proxmox-ve-config/src/firewall/types/mod.rs +++ b/proxmox-ve-config/src/firewall/types/mod.rs @@ -1,7 +1,9 @@ pub mod address; pub mod alias; +pub mod ipset; pub mod log; pub mod port; pub use address::Cidr; pub use alias::Alias; +pub use ipset::Ipset; -- 2.39.2