From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 187811FF0AA for ; Fri, 21 Aug 2026 16:06:23 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 13AF021845; Fri, 21 Aug 2026 16:04:35 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Date: Fri, 21 Aug 2026 16:03:46 +0200 Message-ID: <20260821140404.322081-3-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260821140404.322081-1-g.goller@proxmox.com> References: <20260821140404.322081-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787321022673 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.620 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: 6MCBS3QWICJD3XTV4PIFGMAHBO36BSXX X-Message-ID-Hash: 6MCBS3QWICJD3XTV4PIFGMAHBO36BSXX X-MailFrom: g.goller@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 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Allow OSPF and BGP fabrics to refer to an SDN zone. The zone provides the VRF in which the fabric routing protocol will run. Support setting and deleting the reference through the fabric update API. Scope prefix overlap checks and OSPF area uniqueness by zone. Different VRFs have independent routing tables, so they can reuse addresses and OSPF areaas. Signed-off-by: Gabriel Goller --- proxmox-ve-config/src/sdn/fabric/mod.rs | 30 ++++++- .../src/sdn/fabric/section_config/fabric.rs | 26 +++++- .../sdn/fabric/section_config/protocol/bgp.rs | 11 ++- .../fabric/section_config/protocol/ospf.rs | 11 ++- proxmox-ve-config/tests/fabric/main.rs | 85 ++++++++++++++++++- 5 files changed, 155 insertions(+), 8 deletions(-) diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs index 22f19c7af2e1..1ed72cbb4661 100644 --- a/proxmox-ve-config/src/sdn/fabric/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -773,8 +773,8 @@ impl Validatable for FabricConfig { /// Ensures that: /// - (node, interface) combinations exist only once across all fabrics /// - every entry (fabric) validates - /// - all the ospf fabrics have different areas - /// - IP prefixes of fabrics do not overlap + /// - OSPF areas are unique within each VRF + /// - IP prefixes do not overlap within the same VRF fn validate(&self) -> Result<(), FabricConfigError> { let mut wireguard_interfaces = HashSet::new(); let mut wireguard_listen_ports = HashSet::new(); @@ -789,6 +789,11 @@ impl Validatable for FabricConfig { .flat_map(|(i, f1)| fabrics.iter().skip(i + 1).map(move |f2| (f1, f2))); for (fabric1, fabric2) in cartesian_product { + // Separate VRFs have independent routing tables and may use overlapping address space. + if fabric1.zone() != fabric2.zone() { + continue; + } + if let (Some(prefix1), Some(prefix2)) = (fabric1.ip_prefix(), fabric2.ip_prefix()) { if prefix1.overlaps(&prefix2) { return Err(FabricConfigError::OverlappingIp4Prefix( @@ -815,13 +820,14 @@ impl Validatable for FabricConfig { // additionally, for wireguard check the listen ports of the interfaces as well for entry in self.fabrics.values() { if let FabricEntry::Ospf(entry) = entry { - if !ospf_area.insert( + if !ospf_area.insert(( + entry.fabric_section().properties().zone().cloned(), entry .fabric_section() .properties() .area() .get_ipv4_representation(), - ) { + )) { return Err(FabricConfigError::DuplicateOspfArea); } } @@ -1002,6 +1008,7 @@ impl FabricConfig { ip6_prefix, properties: OspfPropertiesUpdater { + zone, area, route_filter, redistribute, @@ -1017,6 +1024,10 @@ impl FabricConfig { fabric_section.ip6_prefix = Some(prefix); } + if let Some(zone) = zone { + fabric_section.properties.zone = Some(zone); + } + if let Some(area) = area { fabric_section.properties.area = area; } @@ -1037,6 +1048,9 @@ impl FabricConfig { FabricDeletableProperties::Ip6Prefix => { fabric_section.ip6_prefix = None; } + FabricDeletableProperties::Protocol(OspfDeletableProperties::Zone) => { + fabric_section.properties.zone = None; + } FabricDeletableProperties::Protocol( OspfDeletableProperties::RouteFilter, ) => { @@ -1100,6 +1114,7 @@ impl FabricConfig { ip6_prefix, properties: BgpPropertiesUpdater { + zone, bfd, redistribute, route_map_in, @@ -1117,6 +1132,10 @@ impl FabricConfig { fabric_section.ip6_prefix = Some(prefix); } + if let Some(zone) = zone { + fabric_section.properties.zone = Some(zone); + } + if let Some(bfd) = bfd { fabric_section.properties.bfd = bfd; } @@ -1145,6 +1164,9 @@ impl FabricConfig { FabricDeletableProperties::Ip6Prefix => { fabric_section.ip6_prefix = None; } + FabricDeletableProperties::Protocol(BgpDeletableProperties::Zone) => { + fabric_section.properties.zone = None; + } FabricDeletableProperties::Protocol( BgpDeletableProperties::Redistribute, ) => { diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs index 9d380c524d72..c6155f476d44 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs @@ -38,6 +38,21 @@ api_string_type! { pub struct FabricId(String); } +pub const ZONE_ID_REGEX_STR: &str = r"(?:[a-zA-Z])(?:[a-zA-Z0-9]){0,7}"; + +const_regex! { + pub ZONE_ID_REGEX = concatcp!(r"^", ZONE_ID_REGEX_STR, r"$"); +} + +pub const ZONE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&ZONE_ID_REGEX); + +api_string_type! { + /// ID of the SDN zone providing a VRF for a fabric. + #[api(format: &ZONE_ID_FORMAT)] + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, UpdaterType)] + pub struct ZoneId(String); +} + /// A fabric section in an SDN fabric config. /// /// This struct contains all the properties that are required for any fabric, regardless of @@ -196,6 +211,15 @@ impl Fabric { } } + /// Get the SDN zone whose VRF contains this fabric. + pub fn zone(&self) -> Option<&ZoneId> { + match self { + Fabric::Ospf(fabric_section) => fabric_section.properties().zone(), + Fabric::Bgp(fabric_section) => fabric_section.properties().zone(), + Fabric::Openfabric(_) | Fabric::WireGuard(_) => None, + } + } + /// Get the ip-prefix (IPv4 CIDR) of the [`Fabric`]. /// /// This is a common property for all protocols. @@ -253,7 +277,7 @@ impl Validatable for Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.validate(), Fabric::Ospf(fabric_section) => fabric_section.validate(), - Fabric::WireGuard(_fabric_section) => Ok(()), + Fabric::WireGuard(_) => Ok(()), Fabric::Bgp(fabric_section) => fabric_section.validate(), } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs index 90900793533e..4a56ec5f6c88 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs @@ -9,7 +9,7 @@ use proxmox_schema::{ApiStringFormat, Updater, api, property_string::PropertyStr use crate::common::valid::Validatable; use crate::sdn::fabric::FabricConfigError; -use crate::sdn::fabric::section_config::fabric::FabricSection; +use crate::sdn::fabric::section_config::fabric::{FabricSection, ZoneId}; use crate::sdn::fabric::section_config::interface::InterfaceName; use crate::sdn::fabric::section_config::node::NodeSection; @@ -95,6 +95,10 @@ impl ASN { #[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] /// Properties for a BGP fabric. pub struct BgpProperties { + /// SDN zone whose VRF contains this fabric. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) zone: Option, + /// enable BFD for this fabric #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] pub(crate) bfd: bool, @@ -120,6 +124,10 @@ pub struct BgpProperties { } impl BgpProperties { + pub fn zone(&self) -> Option<&ZoneId> { + self.zone.as_ref() + } + pub fn bfd(&self) -> bool { self.bfd } @@ -141,6 +149,7 @@ impl Validatable for FabricSection { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BgpDeletableProperties { + Zone, Redistribute, RouteFilter, RouteMapIn, diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs index 23cce1b54d1b..55f3c36f4631 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs @@ -8,7 +8,7 @@ use proxmox_schema::{ApiStringFormat, Updater, api, property_string::PropertyStr use crate::common::valid::Validatable; use crate::sdn::fabric::FabricConfigError; -use crate::sdn::fabric::section_config::fabric::FabricSection; +use crate::sdn::fabric::section_config::fabric::{FabricSection, ZoneId}; use crate::sdn::fabric::section_config::interface::InterfaceName; use crate::sdn::fabric::section_config::node::NodeSection; use crate::sdn::prefix_list::PrefixListId; @@ -97,6 +97,10 @@ mod frr { #[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] /// Properties for an Ospf fabric. pub struct OspfProperties { + /// SDN zone whose VRF contains this fabric. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) zone: Option, + /// OSPF area pub(crate) area: Area, @@ -113,6 +117,10 @@ pub struct OspfProperties { } impl OspfProperties { + pub fn zone(&self) -> Option<&ZoneId> { + self.zone.as_ref() + } + pub fn set_area(&mut self, value: Area) { self.area = value; } @@ -149,6 +157,7 @@ impl Validatable for FabricSection { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OspfDeletableProperties { + Zone, RouteFilter, Redistribute, } diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs index ee59e9a2ea44..aa76adb9819f 100644 --- a/proxmox-ve-config/tests/fabric/main.rs +++ b/proxmox-ve-config/tests/fabric/main.rs @@ -5,7 +5,12 @@ use std::str::FromStr; use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF}; use proxmox_frr::ser::{FrrConfig, VrfName, serializer::dump}; use proxmox_ve_config::sdn::fabric::{ - FabricConfig, frr::build_fabric, section_config::node::NodeId, + FabricConfig, + frr::build_fabric, + section_config::{ + fabric::{FabricId, FabricUpdater}, + node::NodeId, + }, }; mod helper; @@ -165,6 +170,84 @@ fn openfabric_ipv6_only() { insta::assert_snapshot!(helper::reference_name!("pve"), output); } +#[test] +fn vrf_prefix_overlap_is_scoped_by_zone() { + let raw = r#" +ospf_fabric: first + area 0 + ip_prefix 10.0.0.0/24 + zone zoneone + +ospf_fabric: second + area 1 + ip_prefix 10.0.0.0/24 + zone zonetwo + +ospf_node: first_pve + interfaces name=eth0 + ip 10.0.0.1 + +ospf_node: second_pve + interfaces name=eth1 + ip 10.0.0.1 +"#; + + assert!(FabricConfig::parse_section_config(raw).is_ok()); + assert!( + FabricConfig::parse_section_config(&raw.replace("zone zonetwo", "zone zoneone")).is_err() + ); +} + +#[test] +fn update_protocol_zone() { + for (protocol, raw) in [ + ( + "ospf", + r#" +ospf_fabric: test + area 0 + ip_prefix 10.0.0.0/24 +"#, + ), + ( + "bgp", + r#" +bgp_fabric: test + bfd 0 + ip_prefix 10.0.0.0/24 +"#, + ), + ] { + let mut config = FabricConfig::parse_section_config(raw) + .unwrap() + .into_inner(); + let updater: FabricUpdater = + serde_json::from_value(serde_json::json!({ "protocol": protocol, "zone": "myzone" })) + .unwrap(); + let id = FabricId::from_str("test").unwrap(); + + config.update_fabric(&id, updater).unwrap(); + assert_eq!( + config + .get_fabric(&id) + .unwrap() + .fabric() + .zone() + .unwrap() + .as_str(), + "myzone" + ); + + let updater: FabricUpdater = serde_json::from_value(serde_json::json!({ + "protocol": protocol, + "delete": ["zone"], + })) + .unwrap(); + config.update_fabric(&id, updater).unwrap(); + assert!(config.get_fabric(&id).unwrap().fabric().zone().is_none()); + } +} + #[test] fn bgp_default() { let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); -- 2.47.3