From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 394AF1FF13F for ; Thu, 26 Mar 2026 16:15:50 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AC76518079; Thu, 26 Mar 2026 16:16:12 +0100 (CET) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Thu, 26 Mar 2026 16:15:32 +0100 Message-Id: To: "Gabriel Goller" Subject: Re: [PATCH proxmox-ve-rs v7 04/21] sdn-types: support variable-length NET identifier X-Mailer: aerc 0.20.0 References: <20260323134934.243110-1-g.goller@proxmox.com> <20260323134934.243110-5-g.goller@proxmox.com> In-Reply-To: <20260323134934.243110-5-g.goller@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774538083649 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.122 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 Message-ID-Hash: 2DLWLNXCVI5MYAN23AR3VWI73CBEM34C X-Message-ID-Hash: 2DLWLNXCVI5MYAN23AR3VWI73CBEM34C X-MailFrom: s.sterz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Mon Mar 23, 2026 at 2:48 PM CET, Gabriel Goller wrote: > The NET (Network Entity Title) can actually be variable-length. We only > use the minimum length one (which corresponds to an ipv4 address) in the > fabrics, but in the ISIS tests we also use a longer NET. Support the > longer NET as well. > > Reviewed-by: Hannes Laimer > Tested-by: Stefan Hanreich > Signed-off-by: Gabriel Goller > --- > proxmox-sdn-types/src/net.rs | 136 +++++++++++++++++++++++++++++++---- > 1 file changed, 121 insertions(+), 15 deletions(-) > > diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs > index 3cd1e4f80ed7..f141fa5ab6b4 100644 > --- a/proxmox-sdn-types/src/net.rs > +++ b/proxmox-sdn-types/src/net.rs > @@ -10,7 +10,8 @@ use proxmox_schema::{api, api_string_type, const_regex,= ApiStringFormat, Updater > > const_regex! { > NET_AFI_REGEX =3D r"^(?:[a-fA-F0-9]{2})$"; > - NET_AREA_REGEX =3D r"^(?:[a-fA-F0-9]{4})$"; > + // Variable length area: 0 to 13 bytes (0 to 26 hex digits) accordin= g to ISO 10589 > + NET_AREA_REGEX =3D r"^(?:[a-fA-F0-9]{0,26})$"; maybe this should be `^(?:[a-fA-F0-9]{2}){0,13}$`? Otherwise you could have an uneven amount of "nibbles" and it might not be clear how to interpret them (e.g. is ADC "AD 0C", "A0 DC", or "AD C0"). > NET_SYSTEM_ID_REGEX =3D r"^(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})\.(= ?:[a-fA-F0-9]{4})$"; > NET_SELECTOR_REGEX =3D r"^(?:[a-fA-F0-9]{2})$"; > } > @@ -39,9 +40,9 @@ impl UpdaterType for NetAFI { > } > > api_string_type! { > - /// 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. > + /// Area identifier: Variable length (0-13 bytes / 0-26 hex digits) = according to ISO 10589 > + /// IS-IS area number that identifies the routing domain. All router= s in the same area must > + /// have the same area identifier. Can be empty or up to 26 hex digi= ts. > #[api(format: &NET_AREA_FORMAT)] > #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, = PartialOrd, Ord)] > struct NetArea(String); > @@ -146,6 +147,7 @@ pub struct Net { > selector: NetSelector, > } > > + > impl UpdaterType for Net { > type Updater =3D Option; > } > @@ -156,27 +158,58 @@ impl std::str::FromStr for Net { > fn from_str(s: &str) -> Result { > let parts: Vec<&str> =3D s.split(".").collect(); > > - if parts.len() !=3D 6 { > - bail!("invalid NET format: {s}") > + // Minimum: AFI.SystemID(3 parts).Selector =3D 5 parts > + // With area: AFI.Area.SystemID(3 parts).Selector =3D 6+ parts > + if parts.len() < 5 { > + bail!("invalid NET format: {s} (expected at least AFI.System= ID.Selector)") > } > > - let system =3D format!("{}.{}.{}", parts[2], parts[3], parts[4],= ); > + // Last part is selector (2 hex digits) > + let selector_idx =3D parts.len() - 1; > + let selector =3D parts[selector_idx]; > + > + // Three parts before selector are system ID (xxxx.xxxx.xxxx) > + let system_id_parts =3D &parts[selector_idx - 3..selector_idx]; > + let system =3D format!( > + "{}.{}.{}", > + system_id_parts[0], system_id_parts[1], system_id_parts[2] > + ); > + > + // First part is AFI (2 hex digits) > + let afi =3D parts[0]; > + > + // Everything between AFI and system ID is the area (can be empt= y) > + let area_parts =3D &parts[1..selector_idx - 3]; > + let area =3D area_parts.join(""); > > Ok(Self { > - afi: NetAFI::from_string(parts[0].to_string())?, > - area: NetArea::from_string(parts[1].to_string())?, > - system: NetSystemId::from_string(system.to_string())?, > - selector: NetSelector::from_string(parts[5].to_string())?, > + afi: NetAFI::from_str(afi)?, > + area: NetArea::from_string(area)?, > + system: NetSystemId::from_string(system)?, > + selector: NetSelector::from_str(selector)?, > }) > } > } > > impl Display for Net { > fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { > + // Format area with dots every 4 hex digits for readability > + let area_str =3D self.area.0.as_str(); > + let area_formatted =3D if area_str.is_empty() { > + String::new() > + } else { > + let chunks: Vec<&str> =3D area_str > + .as_bytes() > + .chunks(4) > + .map(|chunk| std::str::from_utf8(chunk).unwrap()) > + .collect(); > + format!(".{}", chunks.join(".")) > + }; > + > write!( > f, > - "{}.{}.{}.{}", > - self.afi, self.area, self.system, self.selector > + "{}{}.{}.{}", > + self.afi, area_formatted, self.system, self.selector > ) > } > } > @@ -258,8 +291,16 @@ mod tests { > let input =3D "409.0001.1921.6800.1002.00"; > input.parse::().expect_err("invalid AFI"); > > - let input =3D "49.00001.1921.6800.1002.00"; > - input.parse::().expect_err("invalid area"); > + // Area can now be variable length (0-26 hex digits), so 5 digit= s is valid > + // but 27 digits would be invalid > + let input =3D "49.0123.4567.8901.2345.6789.0123.4569.1921.6800.1= 002.00"; > + input > + .parse::() > + .expect_err("area too long (>26 hex digits)"); > + > + // Too few parts > + let input =3D "49.1921.6800.00"; > + input.parse::().expect_err("not enough parts"); > } > > #[test] > @@ -320,4 +361,69 @@ mod tests { > let net4: Net =3D ip4.into(); > assert_eq!(format!("{net4}"), "49.0001.0000.0000.0000.00"); > } > + > + #[test] > + fn test_net_variable_length_area() { > + // Test with no area (just AFI) > + let input =3D "49.1921.6800.1002.00"; > + let net =3D input.parse::().expect("should parse NET with n= o area"); > + assert_eq!(net.afi, NetAFI("49".to_owned())); > + assert_eq!(net.area, NetArea("".to_owned())); > + assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()))= ; > + assert_eq!(net.selector, NetSelector("00".to_owned())); > + assert_eq!(format!("{net}"), "49.1921.6800.1002.00"); > + > + // Test with 2 hex digit area > + let input =3D "49.01.1921.6800.1002.00"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 2-digit area"); > + assert_eq!(net.area, NetArea("01".to_owned())); > + assert_eq!(format!("{net}"), "49.01.1921.6800.1002.00"); > + > + // Test with 4 hex digit area (standard) > + let input =3D "49.0001.1921.6800.1002.00"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 4-digit area"); > + assert_eq!(net.area, NetArea("0001".to_owned())); > + assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00"); > + > + // Test with 8 hex digit area (formatted with dots every 4 digit= s) > + let input =3D "49.0001.0002.1921.6800.1002.00"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 8-digit area"); > + assert_eq!(net.area, NetArea("00010002".to_owned())); > + // Should be formatted with dots every 4 hex digits > + assert_eq!(format!("{net}"), "49.0001.0002.1921.6800.1002.00"); > + > + // Test with 12 hex digit area > + let input =3D "49.0001.0002.0003.1921.6800.1002.00"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 12-digit area"); > + assert_eq!(net.area, NetArea("000100020003".to_owned())); > + assert_eq!(format!("{net}"), "49.0001.0002.0003.1921.6800.1002.0= 0"); > + > + // Test with odd-length area (5 hex digits) > + let input =3D "49.12345.1921.6800.1002.00"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 5-digit area"); > + assert_eq!(net.area, NetArea("12345".to_owned())); > + // Should be formatted with dots every 4 digits, last chunk has = 1 digit > + assert_eq!(format!("{net}"), "49.1234.5.1921.6800.1002.00"); > + > + // Test with maximum length area (26 hex digits =3D 13 bytes) > + let input =3D "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.0= 0"; > + let net =3D input > + .parse::() > + .expect("should parse NET with 26-digit area"); > + assert_eq!(net.area, NetArea("0123456789abcdef01234567".to_owned= ())); > + assert_eq!( > + format!("{net}"), > + "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.00" > + ); > + } > }