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 67146911C7 for ; Wed, 3 Apr 2024 12:46:58 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 48FFF16007 for ; Wed, 3 Apr 2024 12:46:28 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (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 firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 3 Apr 2024 12:46:26 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 5D8A344D24 for ; Wed, 3 Apr 2024 12:46:26 +0200 (CEST) Content-Type: text/plain; charset=UTF-8 Date: Wed, 03 Apr 2024 12:46:23 +0200 Message-Id: To: "Proxmox VE development discussion" From: "Max Carrara" Cc: "Wolfgang Bumiller" Content-Transfer-Encoding: quoted-printable Mime-Version: 1.0 X-Mailer: aerc 0.17.0-72-g6a84f1331f1c References: <20240402171629.536804-1-s.hanreich@proxmox.com> <20240402171629.536804-3-s.hanreich@proxmox.com> In-Reply-To: <20240402171629.536804-3-s.hanreich@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pve-devel] [PATCH proxmox-firewall 02/37] config: firewall: add types for ip addresses 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: Wed, 03 Apr 2024 10:46:58 -0000 On Tue Apr 2, 2024 at 7:15 PM CEST, Stefan Hanreich wrote: > Includes types for all kinds of IP values that can occur in the > firewall config. Additionally, FromStr implementations are available > for parsing from the config files. > > Co-authored-by: Wolfgang Bumiller > Signed-off-by: Stefan Hanreich > --- > proxmox-ve-config/src/firewall/mod.rs | 1 + > .../src/firewall/types/address.rs | 624 ++++++++++++++++++ > proxmox-ve-config/src/firewall/types/mod.rs | 3 + > proxmox-ve-config/src/lib.rs | 1 + > 4 files changed, 629 insertions(+) > create mode 100644 proxmox-ve-config/src/firewall/mod.rs > create mode 100644 proxmox-ve-config/src/firewall/types/address.rs > create mode 100644 proxmox-ve-config/src/firewall/types/mod.rs > > diff --git a/proxmox-ve-config/src/firewall/mod.rs b/proxmox-ve-config/sr= c/firewall/mod.rs > new file mode 100644 > index 0000000..cd40856 > --- /dev/null > +++ b/proxmox-ve-config/src/firewall/mod.rs > @@ -0,0 +1 @@ > +pub mod types; > diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve= -config/src/firewall/types/address.rs > new file mode 100644 > index 0000000..ce2f1cd > --- /dev/null > +++ b/proxmox-ve-config/src/firewall/types/address.rs > @@ -0,0 +1,624 @@ > +use std::fmt; > +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; > +use std::ops::Deref; > + > +use anyhow::{bail, format_err, Error}; > +use serde_with::DeserializeFromStr; > + > +#[derive(Clone, Copy, Debug, Eq, PartialEq)] > +pub enum Family { > + V4, > + V6, > +} > + > +impl fmt::Display for Family { > + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { > + match self { > + Family::V4 =3D> f.write_str("Ipv4"), > + Family::V6 =3D> f.write_str("Ipv6"), > + } > + } > +} > + > +#[derive(Clone, Debug)] > +#[cfg_attr(test, derive(Eq, PartialEq))] > +pub enum Cidr { > + Ipv4(Ipv4Cidr), > + Ipv6(Ipv6Cidr), > +} > + > +impl Cidr { > + pub fn new_v4(addr: impl Into, mask: u8) -> Result { > + Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?)) > + } > + > + pub fn new_v6(addr: impl Into, mask: u8) -> Result { > + Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?)) > + } > + > + pub const fn family(&self) -> Family { > + match self { > + Cidr::Ipv4(_) =3D> Family::V4, > + Cidr::Ipv6(_) =3D> Family::V6, > + } > + } > + > + pub fn is_ipv4(&self) -> bool { > + matches!(self, Cidr::Ipv4(_)) > + } > + > + pub fn is_ipv6(&self) -> bool { > + matches!(self, Cidr::Ipv6(_)) > + } > +} > + > +impl fmt::Display for Cidr { > + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { > + match self { > + Self::Ipv4(ip) =3D> f.write_str(ip.to_string().as_str()), > + Self::Ipv6(ip) =3D> f.write_str(ip.to_string().as_str()), > + } > + } > +} > + > +impl std::str::FromStr for Cidr { > + type Err =3D Error; > + > + fn from_str(s: &str) -> Result { > + if let Ok(ip) =3D s.parse::() { > + return Ok(Cidr::Ipv4(ip)); > + } > + > + if let Ok(ip) =3D s.parse::() { > + return Ok(Cidr::Ipv6(ip)); > + } > + > + bail!("invalid ip address or CIDR: {s:?}"); > + } > +} > + > +impl From for Cidr { > + fn from(cidr: Ipv4Cidr) -> Self { > + Cidr::Ipv4(cidr) > + } > +} > + > +impl From for Cidr { > + fn from(cidr: Ipv6Cidr) -> Self { > + Cidr::Ipv6(cidr) > + } > +} > + > +const IPV4_LENGTH: u8 =3D 32; > + > +#[derive(Clone, Debug)] > +#[cfg_attr(test, derive(Eq, PartialEq))] > +pub struct Ipv4Cidr { > + addr: Ipv4Addr, > + mask: u8, > +} > + > +impl Ipv4Cidr { > + pub fn new(addr: impl Into, mask: u8) -> Result { > + if mask > 32 { > + bail!("mask out of range for ipv4 cidr ({mask})"); > + } > + > + Ok(Self { > + addr: addr.into(), > + mask, > + }) > + } > + > + pub fn contains_address(&self, other: &Ipv4Addr) -> bool { > + let bits =3D u32::from_be_bytes(self.addr.octets()); > + let other_bits =3D u32::from_be_bytes(other.octets()); > + > + let shift_amount: u32 =3D IPV4_LENGTH.saturating_sub(self.mask).= into(); > + > + bits.checked_shr(shift_amount).unwrap_or(0) > + =3D=3D other_bits.checked_shr(shift_amount).unwrap_or(0) > + } > + > + pub fn address(&self) -> &Ipv4Addr { > + &self.addr > + } > + > + pub fn mask(&self) -> u8 { > + self.mask > + } > +} > + > +impl> From for Ipv4Cidr { > + fn from(value: T) -> Self { > + Self { > + addr: value.into(), > + mask: 32, > + } > + } > +} > + > +impl std::str::FromStr for Ipv4Cidr { > + type Err =3D Error; > + > + fn from_str(s: &str) -> Result { > + Ok(match s.find('/') { > + None =3D> Self { > + addr: s.parse()?, > + mask: 32, > + }, > + Some(pos) =3D> { > + let mask: u8 =3D s[(pos + 1)..] > + .parse() > + .map_err(|_| format_err!("invalid mask in ipv4 cidr:= {s:?}"))?; > + > + Self::new(s[..pos].parse::()?, mask)? > + } > + }) > + } > +} > + > +impl fmt::Display for Ipv4Cidr { > + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { > + write!(f, "{}/{}", &self.addr, self.mask) > + } > +} > + > +const IPV6_LENGTH: u8 =3D 128; > + > +#[derive(Clone, Debug)] > +#[cfg_attr(test, derive(Eq, PartialEq))] > +pub struct Ipv6Cidr { > + addr: Ipv6Addr, > + mask: u8, > +} > + > +impl Ipv6Cidr { > + pub fn new(addr: impl Into, mask: u8) -> Result { > + if mask > IPV6_LENGTH { > + bail!("mask out of range for ipv6 cidr"); > + } > + > + Ok(Self { > + addr: addr.into(), > + mask, > + }) > + } > + > + pub fn contains_address(&self, other: &Ipv6Addr) -> bool { > + let bits =3D u128::from_be_bytes(self.addr.octets()); > + let other_bits =3D u128::from_be_bytes(other.octets()); > + > + let shift_amount: u32 =3D IPV6_LENGTH.saturating_sub(self.mask).= into(); > + > + bits.checked_shr(shift_amount).unwrap_or(0) > + =3D=3D other_bits.checked_shr(shift_amount).unwrap_or(0) > + } > + > + pub fn address(&self) -> &Ipv6Addr { > + &self.addr > + } > + > + pub fn mask(&self) -> u8 { > + self.mask > + } > +} > + > +impl std::str::FromStr for Ipv6Cidr { > + type Err =3D Error; > + > + fn from_str(s: &str) -> Result { > + Ok(match s.find('/') { > + None =3D> Self { > + addr: s.parse()?, > + mask: 128, > + }, > + Some(pos) =3D> { > + let mask: u8 =3D s[(pos + 1)..] > + .parse() > + .map_err(|_| format_err!("invalid mask in ipv6 cidr:= {s:?}"))?; > + > + Self::new(s[..pos].parse::()?, mask)? > + } > + }) > + } > +} > + > +impl fmt::Display for Ipv6Cidr { > + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { > + write!(f, "{}/{}", &self.addr, self.mask) > + } > +} > + > +impl> From for Ipv6Cidr { > + fn from(addr: T) -> Self { > + Self { > + addr: addr.into(), > + mask: 128, > + } > + } > +} > + > +#[derive(Clone, Debug)] > +#[cfg_attr(test, derive(Eq, PartialEq))] > +pub enum IpEntry { > + Cidr(Cidr), > + Range(IpAddr, IpAddr), > +} > + > +impl std::str::FromStr for IpEntry { > + type Err =3D Error; > + > + fn from_str(s: &str) -> Result { > + if s.is_empty() { > + bail!("Empty IP specification!") > + } > + > + let entries: Vec<&str> =3D s > + .split('-') > + .take(3) // so we can check whether there are too many > + .collect(); > + > + match entries.len() { You could just `match` on a slice of `entries` here and then have ... > + 1 =3D> { [cidr] =3D> {=20 as pattern here ... > + let cidr =3D entries.first().expect("Vec contains an ele= ment"); > + > + Ok(IpEntry::Cidr(cidr.parse()?)) > + } > + 2 =3D> { ... and [beg, end] =3D> { as pattern here. > + let (beg, end) =3D ( > + entries.first().expect("Vec contains two elements"), > + entries.get(1).expect("Vec contains two elements"), > + ); > + > + if let Ok(beg) =3D beg.parse::() { > + if let Ok(end) =3D end.parse::() { > + if beg < end { > + return Ok(IpEntry::Range(beg.into(), end.int= o())); > + } > + > + bail!("start address is greater than end address= !"); > + } > + } > + > + if let Ok(beg) =3D beg.parse::() { > + if let Ok(end) =3D end.parse::() { > + if beg < end { > + return Ok(IpEntry::Range(beg.into(), end.int= o())); > + } > + > + bail!("start address is greater than end address= !"); > + } > + } > + > + bail!("start and end are not valid IP addresses of the s= ame type!") > + } > + _ =3D> bail!("Invalid amount of elements in IpEntry!"), > + } > + } > +} > + > +impl fmt::Display for IpEntry { > + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { > + match self { > + Self::Cidr(ip) =3D> write!(f, "{ip}"), > + Self::Range(beg, end) =3D> write!(f, "{beg}-{end}"), > + } > + } > +} > + > +impl IpEntry { > + fn family(&self) -> Family { > + match self { > + Self::Cidr(cidr) =3D> cidr.family(), > + Self::Range(start, end) =3D> { > + if start.is_ipv4() && end.is_ipv4() { > + return Family::V4; > + } > + > + if start.is_ipv6() && end.is_ipv6() { > + return Family::V6; > + } > + > + // should never be reached due to constructors validatin= g that > + // start type =3D=3D end type > + unreachable!("invalid IP entry") > + } > + } > + } > +} > + > +impl From for IpEntry { > + fn from(value: Cidr) -> Self { > + IpEntry::Cidr(value) > + } > +} > + > +#[derive(Clone, Debug, DeserializeFromStr)] > +#[cfg_attr(test, derive(Eq, PartialEq))] > +pub struct IpList { > + // guaranteed to have the same family > + entries: Vec, > + family: Family, > +} > + > +impl Deref for IpList { > + type Target =3D Vec; > + > + fn deref(&self) -> &Self::Target { > + &self.entries > + } > +} > + > +impl> From for IpList { > + fn from(value: T) -> Self { > + let entry =3D value.into(); > + > + Self { > + family: entry.family(), > + entries: vec![entry], > + } > + } > +} > + > +impl std::str::FromStr for IpList { > + type Err =3D Error; > + > + fn from_str(s: &str) -> Result { > + if s.is_empty() { > + bail!("Empty IP specification!") > + } > + > + let mut entries =3D Vec::new(); > + let mut current_family =3D None; > + > + for element in s.split(',') { > + let entry: IpEntry =3D element.parse()?; > + > + if let Some(family) =3D current_family { > + if family !=3D entry.family() { > + bail!("Incompatible families in IPList!") > + } > + } else { > + current_family =3D Some(entry.family()); > + } > + > + entries.push(entry); > + } > + > + if entries.is_empty() { > + bail!("empty ip list") > + } > + > + Ok(IpList { > + entries, > + family: current_family.unwrap(), // must be set due to lengt= h check above > + }) > + } > +} > + > +impl IpList { > + pub fn new(entries: Vec) -> Result { > + let family =3D entries.iter().try_fold(None, |result, entry| { > + if let Some(family) =3D result { > + if entry.family() !=3D family { > + bail!("non-matching families in entries list"); > + } > + > + Ok(Some(family)) > + } else { > + Ok(Some(entry.family())) > + } > + })?; > + > + if let Some(family) =3D family { > + return Ok(Self { entries, family }); > + } > + > + bail!("no elements in ip list entries"); > + } > + > + pub fn family(&self) -> Family { > + self.family > + } > +} > + > +#[cfg(test)] > +mod tests { > + use super::*; > + use std::net::{Ipv4Addr, Ipv6Addr}; > + > + #[test] > + fn test_v4_cidr() { > + let mut cidr: Ipv4Cidr =3D "0.0.0.0/0".parse().expect("valid IPv= 4 CIDR"); > + > + assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0)); > + assert_eq!(cidr.mask, 0); > + > + assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0))); > + assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)= )); > + > + cidr =3D "192.168.100.1".parse().expect("valid IPv4 CIDR"); > + > + assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1)); > + assert_eq!(cidr.mask, 32); > + > + assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)))= ; > + assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2))= ); > + assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0))= ); > + > + cidr =3D "10.100.5.0/24".parse().expect("valid IPv4 CIDR"); > + > + assert_eq!(cidr.mask, 24); > + > + assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0))); > + assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1))); > + assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100))); > + assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255))); > + assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)))= ; > + assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0))); > + > + "0.0.0.0/-1".parse::().unwrap_err(); > + "0.0.0.0/33".parse::().unwrap_err(); > + "256.256.256.256/10".parse::().unwrap_err(); > + > + "fe80::1/64".parse::().unwrap_err(); > + "qweasd".parse::().unwrap_err(); > + "".parse::().unwrap_err(); > + } > + > + #[test] > + fn test_v6_cidr() { > + let mut cidr: Ipv6Cidr =3D "abab::1/64".parse().expect("valid IP= v6 CIDR"); > + > + assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1)= ); > + assert_eq!(cidr.mask, 64); > + > + assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0,= 0, 0, 0))); > + assert!(cidr.contains_address(&Ipv6Addr::new( > + 0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA > + ))); > + assert!(cidr.contains_address(&Ipv6Addr::new( > + 0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF > + ))); > + assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0= , 0, 0, 0))); > + assert!(!cidr.contains_address(&Ipv6Addr::new( > + 0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF > + ))); > + > + cidr =3D "eeee::1".parse().expect("valid IPv6 CIDR"); > + > + assert_eq!(cidr.mask, 128); > + > + assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0,= 0, 0, 1))); > + assert!(!cidr.contains_address(&Ipv6Addr::new( > + 0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFF= FF > + ))); > + assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0= , 0, 0, 0))); > + > + "eeee::1/-1".parse::().unwrap_err(); > + "eeee::1/129".parse::().unwrap_err(); > + "gggg::1/64".parse::().unwrap_err(); > + > + "192.168.0.1".parse::().unwrap_err(); > + "qweasd".parse::().unwrap_err(); > + "".parse::().unwrap_err(); > + } > + > + #[test] > + fn test_parse_ip_entry() { > + let mut entry: IpEntry =3D "10.0.0.1".parse().expect("valid IP e= ntry"); > + > + assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into(= )); > + > + entry =3D "10.0.0.0/16".parse().expect("valid IP entry"); > + > + assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into(= )); > + > + entry =3D "192.168.0.1-192.168.99.255" > + .parse() > + .expect("valid IP entry"); > + > + assert_eq!( > + entry, > + IpEntry::Range([192, 168, 0, 1].into(), [192, 168, 99, 255].= into()) > + ); > + > + entry =3D "fe80::1".parse().expect("valid IP entry"); > + > + assert_eq!( > + entry, > + Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 128) > + .unwrap() > + .into() > + ); > + > + entry =3D "fe80::1/48".parse().expect("valid IP entry"); > + > + assert_eq!( > + entry, > + Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 48) > + .unwrap() > + .into() > + ); > + > + entry =3D "fd80::1-fd80::ffff".parse().expect("valid IP entry"); > + > + assert_eq!( > + entry, > + IpEntry::Range( > + [0xFD80, 0, 0, 0, 0, 0, 0, 1].into(), > + [0xFD80, 0, 0, 0, 0, 0, 0, 0xFFFF].into(), > + ) > + ); > + > + "192.168.100.0-192.168.99.255" > + .parse::() > + .unwrap_err(); > + "192.168.100.0-fe80::1".parse::().unwrap_err(); > + "192.168.100.0-192.168.200.0/16" > + .parse::() > + .unwrap_err(); > + "192.168.100.0-192.168.200.0-192.168.250.0" > + .parse::() > + .unwrap_err(); > + "qweasd".parse::().unwrap_err(); > + } > + > + #[test] > + fn test_parse_ip_list() { > + let mut ip_list: IpList =3D "192.168.0.1,192.168.100.0/24,172.16= .0.0-172.32.255.255" > + .parse() > + .expect("valid IP list"); > + > + assert_eq!( > + ip_list, > + IpList { > + entries: vec![ > + IpEntry::Cidr(Cidr::new_v4([192, 168, 0, 1], 32).unw= rap()), > + IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).u= nwrap()), > + IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255= , 255].into()), > + ], > + family: Family::V4, > + } > + ); > + > + ip_list =3D "fe80::1/64".parse().expect("valid IP list"); > + > + assert_eq!( > + ip_list, > + IpList { > + entries: vec![IpEntry::Cidr( > + Cidr::new_v6([0xFE80, 0, 0, 0, 0, 0, 0, 1], 64).unwr= ap() > + ),], > + family: Family::V6, > + } > + ); > + > + "192.168.0.1,fe80::1".parse::().unwrap_err(); > + > + "".parse::().unwrap_err(); > + "proxmox".parse::().unwrap_err(); > + } > + > + #[test] > + fn test_construct_ip_list() { > + let mut ip_list =3D IpList::new(vec![Cidr::new_v4([10, 0, 0, 0],= 8).unwrap().into()]) > + .expect("valid ip list"); > + > + assert_eq!(ip_list.family(), Family::V4); > + > + ip_list =3D > + IpList::new(vec![Cidr::new_v6([0x000; 8], 8).unwrap().into()= ]).expect("valid ip list"); > + > + assert_eq!(ip_list.family(), Family::V6); > + > + IpList::new(vec![]).expect_err("empty ip list is invalid"); > + > + IpList::new(vec![ > + Cidr::new_v4([10, 0, 0, 0], 8).unwrap().into(), > + Cidr::new_v6([0x0000; 8], 8).unwrap().into(), > + ]) > + .expect_err("cannot mix ip families in ip list"); > + } > +} > diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-con= fig/src/firewall/types/mod.rs > new file mode 100644 > index 0000000..de534b4 > --- /dev/null > +++ b/proxmox-ve-config/src/firewall/types/mod.rs > @@ -0,0 +1,3 @@ > +pub mod address; > + > +pub use address::Cidr; > diff --git a/proxmox-ve-config/src/lib.rs b/proxmox-ve-config/src/lib.rs > index e69de29..a0734b8 100644 > --- a/proxmox-ve-config/src/lib.rs > +++ b/proxmox-ve-config/src/lib.rs > @@ -0,0 +1 @@ > +pub mod firewall;