From: Stefan Hanreich <s.hanre...@proxmox.com> This crate contains SDN specific types, so they can be re-used across multiple crates (The initial use-case being shared types between proxmox-frr and proxmox-ve-config).
This initial commit contains types for the following entities: * OpenFabric Hello Interval/Multiplier and CSNP Interval * Network Entity Title (used as Router IDs in IS-IS / OpenFabric) Signed-off-by: Stefan Hanreich <s.hanre...@proxmox.com> Signed-off-by: Gabriel Goller <g.gol...@proxmox.com> --- Cargo.toml | 4 + proxmox-sdn-types/Cargo.toml | 14 + proxmox-sdn-types/debian/changelog | 5 + proxmox-sdn-types/debian/control | 39 +++ proxmox-sdn-types/debian/copyright | 18 ++ proxmox-sdn-types/debian/debcargo.toml | 7 + proxmox-sdn-types/src/lib.rs | 2 + proxmox-sdn-types/src/net.rs | 382 +++++++++++++++++++++++++ proxmox-sdn-types/src/openfabric.rs | 89 ++++++ proxmox-ve-config/Cargo.toml | 6 +- 10 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 proxmox-sdn-types/Cargo.toml create mode 100644 proxmox-sdn-types/debian/changelog create mode 100644 proxmox-sdn-types/debian/control create mode 100644 proxmox-sdn-types/debian/copyright create mode 100644 proxmox-sdn-types/debian/debcargo.toml create mode 100644 proxmox-sdn-types/src/lib.rs create mode 100644 proxmox-sdn-types/src/net.rs create mode 100644 proxmox-sdn-types/src/openfabric.rs diff --git a/Cargo.toml b/Cargo.toml index b6e6df77969b..bd6a9ca4a79a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "proxmox-ve-config", + "proxmox-sdn-types", ] exclude = [ "build", @@ -17,3 +18,6 @@ rust-version = "1.82" [workspace.dependencies] proxmox-network-types = { version = "0.1" } +serde = { version = "1" } +serde_with = "3" +thiserror = "1.0.59" diff --git a/proxmox-sdn-types/Cargo.toml b/proxmox-sdn-types/Cargo.toml new file mode 100644 index 000000000000..b3dc98550a57 --- /dev/null +++ b/proxmox-sdn-types/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "proxmox-sdn-types" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +exclude.workspace = true +rust-version.workspace = true + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true, features = [ "derive" ] } +serde_with = { workspace = true } diff --git a/proxmox-sdn-types/debian/changelog b/proxmox-sdn-types/debian/changelog new file mode 100644 index 000000000000..422921c2d1f4 --- /dev/null +++ b/proxmox-sdn-types/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-sdn-types (0.1.0-1) unstable; urgency=medium + + * Initial release. + + -- Proxmox Support Team <supp...@proxmox.com> Mon, 03 Jun 2024 10:51:11 +0200 diff --git a/proxmox-sdn-types/debian/control b/proxmox-sdn-types/debian/control new file mode 100644 index 000000000000..16a25313ad37 --- /dev/null +++ b/proxmox-sdn-types/debian/control @@ -0,0 +1,39 @@ +Source: rust-proxmox-sdn-types +Section: rust +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-sequence-cargo +Build-Depends-Arch: cargo:native <!nocheck>, + rustc:native (>= 1.82) <!nocheck>, + libstd-rust-dev <!nocheck>, + librust-serde-1+default-dev <!nocheck>, + librust-serde-1+derive-dev <!nocheck>, + librust-serde-with-3+default-dev <!nocheck>, + librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck> +Maintainer: Proxmox Support Team <supp...@proxmox.com> +Standards-Version: 4.7.0 +Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git +Homepage: https://proxmox.com +X-Cargo-Crate: proxmox-sdn-types +Rules-Requires-Root: no + +Package: librust-proxmox-sdn-types-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-serde-1+default-dev, + librust-serde-1+derive-dev, + librust-serde-with-3+default-dev, + librust-thiserror-1+default-dev (>= 1.0.59-~~) +Provides: + librust-proxmox-sdn-types+default-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0+default-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0.1-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0.1+default-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0.1.0-dev (= ${binary:Version}), + librust-proxmox-sdn-types-0.1.0+default-dev (= ${binary:Version}) +Description: Rust crate "proxmox-sdn-types" - Rust source code + Source code for Debianized Rust crate "proxmox-sdn-types" diff --git a/proxmox-sdn-types/debian/copyright b/proxmox-sdn-types/debian/copyright new file mode 100644 index 000000000000..1ea8a56b4f58 --- /dev/null +++ b/proxmox-sdn-types/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <supp...@proxmox.com> +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see <https://www.gnu.org/licenses/>. diff --git a/proxmox-sdn-types/debian/debcargo.toml b/proxmox-sdn-types/debian/debcargo.toml new file mode 100644 index 000000000000..87a787e6d03e --- /dev/null +++ b/proxmox-sdn-types/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team <supp...@proxmox.com>" + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git" diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs new file mode 100644 index 000000000000..018674612710 --- /dev/null +++ b/proxmox-sdn-types/src/lib.rs @@ -0,0 +1,2 @@ +pub mod net; +pub mod openfabric; diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs new file mode 100644 index 000000000000..97e019383bcc --- /dev/null +++ b/proxmox-sdn-types/src/net.rs @@ -0,0 +1,382 @@ +use std::{ + fmt::Display, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +use serde::Serialize; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NetError { + #[error("Some octets are missing")] + WrongLength, + #[error("The NET selector must be two characters wide and be 00")] + InvalidNetSelector, + #[error("Invalid AFI (wrong size or position)")] + InvalidAFI, + #[error("Invalid Area (wrong size or position)")] + InvalidArea, + #[error("Invalid SystemId (wrong size or position)")] + InvalidSystemId, +} + +/// Address Family authority Identifier - 49 The AFI value 49 is what IS-IS (and openfabric) uses +/// for private addressing. +#[derive( + Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, +)] +struct NetAFI(String); + +impl Default for NetAFI { + fn default() -> Self { + Self("49".to_owned()) + } +} + +impl Display for NetAFI { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for NetAFI { + type Err = NetError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.len() != 2 { + Err(NetError::InvalidAFI) + } else { + Ok(Self(s.to_owned())) + } + } +} + +/// Area identifier: 0001 IS-IS area number (numerical area 1) +/// The second part (system) of the `net` identifier. Every node has to have a different system +/// number. +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct NetArea(String); + +impl Default for NetArea { + fn default() -> Self { + Self("0001".to_owned()) + } +} + +impl Display for NetArea { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for NetArea { + type Err = NetError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.len() != 4 { + Err(NetError::InvalidArea) + } else { + Ok(Self(s.to_owned())) + } + } +} + +/// System identifier: 1921.6800.1002 - for system identifiers we recommend to use IP address or +/// MAC address of the router itself. The way to construct this is to keep all of the zeroes of the +/// router IP address, and then change the periods from being every three numbers to every four +/// numbers. The address that is listed here is 192.168.1.2, which if expanded will turn into +/// 192.168.001.002. Then all one has to do is move the dots to have four numbers instead of three. +/// This gives us 1921.6800.1002. +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct NetSystemId(String); + +impl Display for NetSystemId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for NetSystemId { + type Err = NetError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.split(".").count() != 3 + || s.split(".").any(|segment| { + segment.len() != 4 || !segment.chars().all(|c| c.is_ascii_hexdigit()) + }) + { + Err(NetError::InvalidSystemId) + } else { + Ok(Self(s.to_owned())) + } + } +} + +/// Convert IP-Address to a NET address with the default afi, area and selector values. Note that a +/// valid Ipv4Addr is always a valid SystemId as well. +impl From<Ipv4Addr> for NetSystemId { + fn from(value: Ipv4Addr) -> Self { + let octets = value.octets(); + + let system_id_str = format!( + "{:03}{:01}.{:02}{:02}.{:01}{:03}", + octets[0], + octets[1] / 100, + octets[1] % 100, + octets[2] / 10, + octets[2] % 10, + octets[3] + ); + + Self(system_id_str) + } +} + +/// Convert IPv6-Address to a NET address with the default afi, area and selector values. Note that a +/// valid Ipv6Addr is always a valid SystemId as well. +impl From<Ipv6Addr> for NetSystemId { + fn from(value: Ipv6Addr) -> Self { + let segments = value.segments(); + // + // Use the last 3 segments (out of 8) of the IPv6 address + let system_id_str = format!( + "{:04x}.{:04x}.{:04x}", + segments[5], segments[6], segments[7] + ); + + Self(system_id_str) + } +} + +/// NET selector: 00 Must always be 00. This setting indicates “this system” or “local system.” +#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct NetSelector(String); + +impl Default for NetSelector { + fn default() -> Self { + Self("00".to_owned()) + } +} + +impl Display for NetSelector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for NetSelector { + type Err = NetError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.len() != 2 { + Err(NetError::InvalidNetSelector) + } else { + Ok(Self(s.to_owned())) + } + } +} + +/// The OpenFabric Net. +/// +/// Every OpenFabric node and fabric is identified through the NET. It has a network and a host +/// part. +/// The first part is the network part (also called area). The entire OpenFabric fabric has to have +/// the same network part (area). The first number is the [`NetAFI`] and the second is the [`NetArea`]. +/// e.g.: "49.0001" +/// The second part is the host part, which has to differ on every node in the fabric. It contains +/// the [`NetSystemId`] and the [`NetSelector`]. +/// e.g.: "1921.6800.1002.00" +#[derive( + Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, +)] +pub struct Net { + afi: NetAFI, + area: NetArea, + system: NetSystemId, + selector: NetSelector, +} + +impl FromStr for Net { + type Err = NetError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.split(".").count() != 6 { + return Err(NetError::WrongLength); + } + let mut iter = s.split("."); + let afi = iter.next().ok_or(NetError::WrongLength)?; + let area = iter.next().ok_or(NetError::WrongLength)?; + let system = format!( + "{}.{}.{}", + iter.next().ok_or(NetError::WrongLength)?, + iter.next().ok_or(NetError::WrongLength)?, + iter.next().ok_or(NetError::WrongLength)? + ); + let selector = iter.next().ok_or(NetError::WrongLength)?; + Ok(Self { + afi: afi.parse()?, + area: area.parse()?, + system: system.parse()?, + selector: selector.parse()?, + }) + } +} + +/// Default NET address for a given Ipv4Addr. This adds the default afi, area and selector to the +/// address. +impl From<Ipv4Addr> for Net { + fn from(value: Ipv4Addr) -> Self { + Self { + afi: NetAFI::default(), + area: NetArea::default(), + system: value.into(), + selector: NetSelector::default(), + } + } +} + +/// Default NET address for a given Ipv6Addr. This adds the default afi, area and selector to the +/// address. +impl From<Ipv6Addr> for Net { + fn from(value: Ipv6Addr) -> Self { + Self { + afi: NetAFI::default(), + area: NetArea::default(), + system: value.into(), + selector: NetSelector::default(), + } + } +} + +/// Default NET address for a given IpAddr (can be either Ipv4 or Ipv6). This adds the default afi, +/// area and selector to the address. +impl From<IpAddr> for Net { + fn from(value: IpAddr) -> Self { + Self { + afi: NetAFI::default(), + area: NetArea::default(), + system: match value { + IpAddr::V4(ipv4_addr) => ipv4_addr.into(), + IpAddr::V6(ipv6_addr) => ipv6_addr.into(), + }, + selector: NetSelector::default(), + } + } +} + +impl Display for Net { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}.{}.{}.{}", + self.afi, self.area, self.system, self.selector + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_net_from_str() { + let input = "49.0001.1921.6800.1002.00"; + let net = input.parse::<Net>().expect("this net should parse"); + assert_eq!(net.afi, NetAFI("49".to_owned())); + assert_eq!(net.area, NetArea("0001".to_owned())); + assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned())); + assert_eq!(net.selector, NetSelector("00".to_owned())); + + let input = "45.0200.0100.1001.ba1f.01"; + let net = input.parse::<Net>().expect("this net should parse"); + assert_eq!(net.afi, NetAFI("45".to_owned())); + assert_eq!(net.area, NetArea("0200".to_owned())); + assert_eq!(net.system, NetSystemId("0100.1001.ba1f".to_owned())); + assert_eq!(net.selector, NetSelector("01".to_owned())); + } + + #[test] + fn test_net_from_str_failed() { + let input = "49.0001.1921.6800.1002.000"; + assert!(matches!( + input.parse::<Net>(), + Err(NetError::InvalidNetSelector) + )); + + let input = "49.0001.1921.6800.1002.00.00"; + assert!(matches!(input.parse::<Net>(), Err(NetError::WrongLength))); + + let input = "49.0001.1921.6800.10002.00"; + assert!(matches!( + input.parse::<Net>(), + Err(NetError::InvalidSystemId) + )); + + let input = "49.0001.1921.6800.1z02.00"; + assert!(matches!( + input.parse::<Net>(), + Err(NetError::InvalidSystemId) + )); + + let input = "409.0001.1921.6800.1002.00"; + assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidAFI))); + + let input = "49.00001.1921.6800.1002.00"; + assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea))); + } + + #[test] + fn test_net_display() { + let net = Net { + afi: NetAFI("49".to_owned()), + area: NetArea("0001".to_owned()), + system: NetSystemId("1921.6800.1002".to_owned()), + selector: NetSelector("00".to_owned()), + }; + assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00"); + } + + #[test] + fn test_net_from_ipv4() { + let ip: Ipv4Addr = "192.168.1.100".parse().unwrap(); + let net: Net = ip.into(); + assert_eq!(format!("{net}"), "49.0001.1921.6800.1100.00"); + + let ip1: Ipv4Addr = "10.10.2.245".parse().unwrap(); + let net1: Net = ip1.into(); + assert_eq!(format!("{net1}"), "49.0001.0100.1000.2245.00"); + + let ip2: Ipv4Addr = "1.1.1.1".parse().unwrap(); + let net2: Net = ip2.into(); + assert_eq!(format!("{net2}"), "49.0001.0010.0100.1001.00"); + } + + #[test] + fn test_net_from_ipv6() { + // 2001:db8::1 -> [2001, 0db8, 0, 0, 0, 0, 0, 1] + // last 3 segments: [0, 0, 1] + let ip: Ipv6Addr = "2001:db8::1".parse().unwrap(); + let net: Net = ip.into(); + assert_eq!(format!("{net}"), "49.0001.0000.0000.0001.00"); + + // fe80::1234:5678:abcd -> [fe80, 0, 0, 0, 0, 1234, 5678, abcd] + // last 3 segments: [1234, 5678, abcd] + let ip1: Ipv6Addr = "fe80::1234:5678:abcd".parse().unwrap(); + let net1: Net = ip1.into(); + assert_eq!(format!("{net1}"), "49.0001.1234.5678.abcd.00"); + + // 2001:0db8:85a3::8a2e:370:7334 -> [2001, 0db8, 85a3, 0, 0, 8a2e, 0370, 7334] + // last 3 segments: [8a2e, 0370, 7334] + let ip2: Ipv6Addr = "2001:0db8:85a3::8a2e:370:7334".parse().unwrap(); + let net2: Net = ip2.into(); + assert_eq!(format!("{net2}"), "49.0001.8a2e.0370.7334.00"); + + // ::1 -> [0, 0, 0, 0, 0, 0, 0, 1] + // last 3 segments: [0, 0, 1] + let ip3: Ipv6Addr = "::1".parse().unwrap(); + let net3: Net = ip3.into(); + assert_eq!(format!("{net3}"), "49.0001.0000.0000.0001.00"); + } +} diff --git a/proxmox-sdn-types/src/openfabric.rs b/proxmox-sdn-types/src/openfabric.rs new file mode 100644 index 000000000000..f3fce5dcca7c --- /dev/null +++ b/proxmox-sdn-types/src/openfabric.rs @@ -0,0 +1,89 @@ +use std::{fmt::Display, num::ParseIntError}; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum IntegerRangeError { + #[error("The value must be between {min} and {max} seconds")] + OutOfRange { min: i32, max: i32 }, + #[error("Error parsing to number")] + ParsingError(#[from] ParseIntError), +} + +/// The OpenFabric CSNP Interval. +/// +/// The Complete Sequence Number Packets (CSNP) interval in seconds. The interval range is 1 to +/// 600. +#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[serde(try_from = "u16")] +pub struct CsnpInterval(u16); + +impl TryFrom<u16> for CsnpInterval { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (1..=600).contains(&number) { + Ok(CsnpInterval(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 1, max: 600 }) + } + } +} + +impl Display for CsnpInterval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// The OpenFabric Hello Interval. +/// +/// The Hello Interval for a given interface in seconds. The range is 1 to 600. Hello packets are +/// used to establish and maintain adjacency between OpenFabric neighbors. +#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[serde(try_from = "u16")] +pub struct HelloInterval(u16); + +impl TryFrom<u16> for HelloInterval { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (1..=600).contains(&number) { + Ok(HelloInterval(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 1, max: 600 }) + } + } +} + +impl Display for HelloInterval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// The OpenFabric Hello Multiplier. +/// +/// This is the multiplier for the hello holding time on a given interface. The range is 2 to 100. +#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[serde(try_from = "u16")] +pub struct HelloMultiplier(u16); + +impl TryFrom<u16> for HelloMultiplier { + type Error = IntegerRangeError; + + fn try_from(number: u16) -> Result<Self, Self::Error> { + if (2..=100).contains(&number) { + Ok(HelloMultiplier(number)) + } else { + Err(IntegerRangeError::OutOfRange { min: 2, max: 100 }) + } + } +} + +impl Display for HelloMultiplier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml index 2f3544bd5611..d8735e33653b 100644 --- a/proxmox-ve-config/Cargo.toml +++ b/proxmox-ve-config/Cargo.toml @@ -10,12 +10,12 @@ exclude.workspace = true log = "0.4" anyhow = "1" nix = "0.26" -thiserror = "1.0.59" +thiserror = { workspace = true } -serde = { version = "1", features = [ "derive" ] } +serde = { workspace = true, features = [ "derive" ] } serde_json = "1" serde_plain = "1" -serde_with = "3" +serde_with = { workspace = true } proxmox-network-types = { workspace = true } proxmox-schema = "4" -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel