[pve-devel] [PATCH proxmox-firewall 02/37] config: firewall: add types for ip addresses

Max Carrara m.carrara at proxmox.com
Wed Apr 3 12:46:23 CEST 2024


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 <w.bumiller at proxmox.com>
> Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
> ---
>  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/src/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 => f.write_str("Ipv4"),
> +            Family::V6 => 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<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
> +        Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
> +    }
> +
> +    pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
> +        Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
> +    }
> +
> +    pub const fn family(&self) -> Family {
> +        match self {
> +            Cidr::Ipv4(_) => Family::V4,
> +            Cidr::Ipv6(_) => 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) => f.write_str(ip.to_string().as_str()),
> +            Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
> +        }
> +    }
> +}
> +
> +impl std::str::FromStr for Cidr {
> +    type Err = Error;
> +
> +    fn from_str(s: &str) -> Result<Self, Error> {
> +        if let Ok(ip) = s.parse::<Ipv4Cidr>() {
> +            return Ok(Cidr::Ipv4(ip));
> +        }
> +
> +        if let Ok(ip) = s.parse::<Ipv6Cidr>() {
> +            return Ok(Cidr::Ipv6(ip));
> +        }
> +
> +        bail!("invalid ip address or CIDR: {s:?}");
> +    }
> +}
> +
> +impl From<Ipv4Cidr> for Cidr {
> +    fn from(cidr: Ipv4Cidr) -> Self {
> +        Cidr::Ipv4(cidr)
> +    }
> +}
> +
> +impl From<Ipv6Cidr> for Cidr {
> +    fn from(cidr: Ipv6Cidr) -> Self {
> +        Cidr::Ipv6(cidr)
> +    }
> +}
> +
> +const IPV4_LENGTH: u8 = 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<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
> +        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 = u32::from_be_bytes(self.addr.octets());
> +        let other_bits = u32::from_be_bytes(other.octets());
> +
> +        let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
> +
> +        bits.checked_shr(shift_amount).unwrap_or(0)
> +            == other_bits.checked_shr(shift_amount).unwrap_or(0)
> +    }
> +
> +    pub fn address(&self) -> &Ipv4Addr {
> +        &self.addr
> +    }
> +
> +    pub fn mask(&self) -> u8 {
> +        self.mask
> +    }
> +}
> +
> +impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
> +    fn from(value: T) -> Self {
> +        Self {
> +            addr: value.into(),
> +            mask: 32,
> +        }
> +    }
> +}
> +
> +impl std::str::FromStr for Ipv4Cidr {
> +    type Err = Error;
> +
> +    fn from_str(s: &str) -> Result<Self, Error> {
> +        Ok(match s.find('/') {
> +            None => Self {
> +                addr: s.parse()?,
> +                mask: 32,
> +            },
> +            Some(pos) => {
> +                let mask: u8 = s[(pos + 1)..]
> +                    .parse()
> +                    .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
> +
> +                Self::new(s[..pos].parse::<Ipv4Addr>()?, 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 = 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<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
> +        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 = u128::from_be_bytes(self.addr.octets());
> +        let other_bits = u128::from_be_bytes(other.octets());
> +
> +        let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
> +
> +        bits.checked_shr(shift_amount).unwrap_or(0)
> +            == 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 = Error;
> +
> +    fn from_str(s: &str) -> Result<Self, Error> {
> +        Ok(match s.find('/') {
> +            None => Self {
> +                addr: s.parse()?,
> +                mask: 128,
> +            },
> +            Some(pos) => {
> +                let mask: u8 = s[(pos + 1)..]
> +                    .parse()
> +                    .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
> +
> +                Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
> +            }
> +        })
> +    }
> +}
> +
> +impl fmt::Display for Ipv6Cidr {
> +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
> +        write!(f, "{}/{}", &self.addr, self.mask)
> +    }
> +}
> +
> +impl<T: Into<Ipv6Addr>> From<T> 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 = Error;
> +
> +    fn from_str(s: &str) -> Result<Self, Error> {
> +        if s.is_empty() {
> +            bail!("Empty IP specification!")
> +        }
> +
> +        let entries: Vec<&str> = 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 => {

               [cidr] => { 
	       as pattern here ...

> +                let cidr = entries.first().expect("Vec contains an element");
> +
> +                Ok(IpEntry::Cidr(cidr.parse()?))
> +            }
> +            2 => {
               ... and
	       [beg, end] => {
	       as pattern here.
> +                let (beg, end) = (
> +                    entries.first().expect("Vec contains two elements"),
> +                    entries.get(1).expect("Vec contains two elements"),
> +                );
> +
> +                if let Ok(beg) = beg.parse::<Ipv4Addr>() {
> +                    if let Ok(end) = end.parse::<Ipv4Addr>() {
> +                        if beg < end {
> +                            return Ok(IpEntry::Range(beg.into(), end.into()));
> +                        }
> +
> +                        bail!("start address is greater than end address!");
> +                    }
> +                }
> +
> +                if let Ok(beg) = beg.parse::<Ipv6Addr>() {
> +                    if let Ok(end) = end.parse::<Ipv6Addr>() {
> +                        if beg < end {
> +                            return Ok(IpEntry::Range(beg.into(), end.into()));
> +                        }
> +
> +                        bail!("start address is greater than end address!");
> +                    }
> +                }
> +
> +                bail!("start and end are not valid IP addresses of the same type!")
> +            }
> +            _ => 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) => write!(f, "{ip}"),
> +            Self::Range(beg, end) => write!(f, "{beg}-{end}"),
> +        }
> +    }
> +}
> +
> +impl IpEntry {
> +    fn family(&self) -> Family {
> +        match self {
> +            Self::Cidr(cidr) => cidr.family(),
> +            Self::Range(start, end) => {
> +                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 validating that
> +                // start type == end type
> +                unreachable!("invalid IP entry")
> +            }
> +        }
> +    }
> +}
> +
> +impl From<Cidr> 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<IpEntry>,
> +    family: Family,
> +}
> +
> +impl Deref for IpList {
> +    type Target = Vec<IpEntry>;
> +
> +    fn deref(&self) -> &Self::Target {
> +        &self.entries
> +    }
> +}
> +
> +impl<T: Into<IpEntry>> From<T> for IpList {
> +    fn from(value: T) -> Self {
> +        let entry = value.into();
> +
> +        Self {
> +            family: entry.family(),
> +            entries: vec![entry],
> +        }
> +    }
> +}
> +
> +impl std::str::FromStr for IpList {
> +    type Err = Error;
> +
> +    fn from_str(s: &str) -> Result<Self, Error> {
> +        if s.is_empty() {
> +            bail!("Empty IP specification!")
> +        }
> +
> +        let mut entries = Vec::new();
> +        let mut current_family = None;
> +
> +        for element in s.split(',') {
> +            let entry: IpEntry = element.parse()?;
> +
> +            if let Some(family) = current_family {
> +                if family != entry.family() {
> +                    bail!("Incompatible families in IPList!")
> +                }
> +            } else {
> +                current_family = 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 length check above
> +        })
> +    }
> +}
> +
> +impl IpList {
> +    pub fn new(entries: Vec<IpEntry>) -> Result<Self, Error> {
> +        let family = entries.iter().try_fold(None, |result, entry| {
> +            if let Some(family) = result {
> +                if entry.family() != family {
> +                    bail!("non-matching families in entries list");
> +                }
> +
> +                Ok(Some(family))
> +            } else {
> +                Ok(Some(entry.family()))
> +            }
> +        })?;
> +
> +        if let Some(family) = 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 = "0.0.0.0/0".parse().expect("valid IPv4 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 = "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 = "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::<Ipv4Cidr>().unwrap_err();
> +        "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
> +        "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
> +
> +        "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
> +        "qweasd".parse::<Ipv4Cidr>().unwrap_err();
> +        "".parse::<Ipv4Cidr>().unwrap_err();
> +    }
> +
> +    #[test]
> +    fn test_v6_cidr() {
> +        let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 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 = "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, 0xFFFF
> +        )));
> +        assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
> +
> +        "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
> +        "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
> +        "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
> +
> +        "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
> +        "qweasd".parse::<Ipv6Cidr>().unwrap_err();
> +        "".parse::<Ipv6Cidr>().unwrap_err();
> +    }
> +
> +    #[test]
> +    fn test_parse_ip_entry() {
> +        let mut entry: IpEntry = "10.0.0.1".parse().expect("valid IP entry");
> +
> +        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 1], 32).unwrap().into());
> +
> +        entry = "10.0.0.0/16".parse().expect("valid IP entry");
> +
> +        assert_eq!(entry, Cidr::new_v4([10, 0, 0, 0], 16).unwrap().into());
> +
> +        entry = "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 = "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 = "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 = "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::<IpEntry>()
> +            .unwrap_err();
> +        "192.168.100.0-fe80::1".parse::<IpEntry>().unwrap_err();
> +        "192.168.100.0-192.168.200.0/16"
> +            .parse::<IpEntry>()
> +            .unwrap_err();
> +        "192.168.100.0-192.168.200.0-192.168.250.0"
> +            .parse::<IpEntry>()
> +            .unwrap_err();
> +        "qweasd".parse::<IpEntry>().unwrap_err();
> +    }
> +
> +    #[test]
> +    fn test_parse_ip_list() {
> +        let mut ip_list: IpList = "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).unwrap()),
> +                    IpEntry::Cidr(Cidr::new_v4([192, 168, 100, 0], 24).unwrap()),
> +                    IpEntry::Range([172, 16, 0, 0].into(), [172, 32, 255, 255].into()),
> +                ],
> +                family: Family::V4,
> +            }
> +        );
> +
> +        ip_list = "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).unwrap()
> +                ),],
> +                family: Family::V6,
> +            }
> +        );
> +
> +        "192.168.0.1,fe80::1".parse::<IpList>().unwrap_err();
> +
> +        "".parse::<IpList>().unwrap_err();
> +        "proxmox".parse::<IpList>().unwrap_err();
> +    }
> +
> +    #[test]
> +    fn test_construct_ip_list() {
> +        let mut ip_list = 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 =
> +            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-config/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;





More information about the pve-devel mailing list