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 2B79E90AEF for ; Tue, 2 Apr 2024 19:17:08 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 360D9A70D for ; Tue, 2 Apr 2024 19:16:36 +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 CC2672C339D; 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:15:57 +0200 Message-Id: <20240402171629.536804-6-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.336 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 05/37] config: firewall: add types for aliases 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:17:08 -0000 Co-authored-by: Wolfgang Bumiller Signed-off-by: Stefan Hanreich --- proxmox-ve-config/src/firewall/parse.rs | 44 +++++ proxmox-ve-config/src/firewall/types/alias.rs | 160 ++++++++++++++++++ proxmox-ve-config/src/firewall/types/mod.rs | 2 + 3 files changed, 206 insertions(+) create mode 100644 proxmox-ve-config/src/firewall/types/alias.rs diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs index a75daee..8e30006 100644 --- a/proxmox-ve-config/src/firewall/parse.rs +++ b/proxmox-ve-config/src/firewall/parse.rs @@ -1,5 +1,49 @@ use anyhow::{bail, format_err, Error}; +/// Parses out a "name" which can be alphanumeric and include dashes. +/// +/// Returns `None` if the name part would be empty. +/// +/// Returns a tuple with the name and the remainder (not trimmed). +pub fn match_name(line: &str) -> Option<(&str, &str)> { + let end = line + .as_bytes() + .iter() + .position(|&b| !(b.is_ascii_alphanumeric() || b == b'-')); + + let (name, rest) = match end { + Some(end) => line.split_at(end), + None => (line, ""), + }; + + if name.is_empty() { + None + } else { + Some((name, rest)) + } +} + +/// Parses up to the next whitespace character or end of the string. +/// +/// Returns `None` if the non-whitespace part would be empty. +/// +/// Returns a tuple containing the parsed section and the *trimmed* remainder. +pub fn match_non_whitespace(line: &str) -> Option<(&str, &str)> { + let (text, rest) = line + .as_bytes() + .iter() + .position(|&b| b.is_ascii_whitespace()) + .map(|pos| { + let (a, b) = line.split_at(pos); + (a, b.trim_start()) + }) + .unwrap_or((line, "")); + if text.is_empty() { + None + } else { + Some((text, rest)) + } +} pub fn parse_bool(value: &str) -> Result { Ok( if value == "0" diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs new file mode 100644 index 0000000..43c6486 --- /dev/null +++ b/proxmox-ve-config/src/firewall/types/alias.rs @@ -0,0 +1,160 @@ +use std::fmt::Display; +use std::str::FromStr; + +use anyhow::{bail, format_err, Error}; +use serde_with::DeserializeFromStr; + +use crate::firewall::parse::{match_name, match_non_whitespace}; +use crate::firewall::types::address::Cidr; + +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub enum AliasScope { + Datacenter, + Guest, +} + +impl FromStr for AliasScope { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "dc" => AliasScope::Datacenter, + "guest" => AliasScope::Guest, + _ => bail!("invalid scope for alias: {s}"), + }) + } +} + +impl Display for AliasScope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + AliasScope::Datacenter => "dc", + AliasScope::Guest => "guest", + }) + } +} + +#[derive(Debug, Clone, DeserializeFromStr)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct AliasName { + scope: AliasScope, + name: String, +} + +impl Display for AliasName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}/{}", self.scope, self.name)) + } +} + +impl FromStr for AliasName { + 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 Alias name!") + } + } + } +} + +impl AliasName { + pub fn new(scope: AliasScope, name: impl Into) -> Self { + Self { + scope, + name: name.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn scope(&self) -> &AliasScope { + &self.scope + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Alias { + name: String, + address: Cidr, + comment: Option, +} + +impl Alias { + pub fn new( + name: impl Into, + address: impl Into, + comment: impl Into>, + ) -> Self { + Self { + name: name.into(), + address: address.into(), + comment: comment.into(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn address(&self) -> &Cidr { + &self.address + } + + pub fn comment(&self) -> Option<&str> { + self.comment.as_deref() + } +} + +impl FromStr for Alias { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (name, line) = + match_name(s.trim_start()).ok_or_else(|| format_err!("expected an alias name"))?; + + let (address, line) = match_non_whitespace(line.trim_start()) + .ok_or_else(|| format_err!("expected a value for alias {name:?}"))?; + + let address: Cidr = 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 alias: {line:?}"), + None => None, + }; + + Ok(Alias { + name: name.to_string(), + address, + comment, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_alias_name() { + for name in ["dc/proxmox_123", "guest/proxmox-123"] { + name.parse::().expect("valid alias name"); + } + + for name in ["proxmox/proxmox_123", "guests/proxmox-123", "dc/", "/name"] { + name.parse::().expect_err("invalid alias name"); + } + } +} diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs index 8bf31b8..69b69f4 100644 --- a/proxmox-ve-config/src/firewall/types/mod.rs +++ b/proxmox-ve-config/src/firewall/types/mod.rs @@ -1,5 +1,7 @@ pub mod address; +pub mod alias; pub mod log; pub mod port; pub use address::Cidr; +pub use alias::Alias; -- 2.39.2