From: Gabriel Goller <g.goller@proxmox.com>
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 [thread overview]
Message-ID: <20260821140404.322081-3-g.goller@proxmox.com> (raw)
In-Reply-To: <20260821140404.322081-1-g.goller@proxmox.com>
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 <g.goller@proxmox.com>
---
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<ZoneId>,
+
/// 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<BgpProperties> {
#[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<ZoneId>,
+
/// 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<OspfProperties> {
#[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
next prev parent reply other threads:[~2026-08-21 14:06 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 01/15] frr: add VRF-aware OSPF rendering Gabriel Goller
2026-08-21 14:03 ` Gabriel Goller [this message]
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 03/15] sdn: generate fabric routing configuration in zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 04/15] sdn: fix VRF route-map scoping and zone ID validation Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 05/15] tests: fabrics: add test for fabrics in VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 06/15] pve-rs: fabrics: assign network interfaces to configured VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 07/15] pve-rs: fabrics: make per-fabric status queries VRF-aware Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 08/15] pve-rs: fabrics: include VRF routes in aggregate status Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 11/15] sdn: always generate EVPN " Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 12/15] api: sdn: allow EVPN zones as fabric VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 13/15] api: sdn: disallow BGP fabrics in EVPN zone VRFs Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 14/15] ui: sdn: add VRF zone selection for fabrics Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 15/15] ui: sdn: expose EVPN zones as fabric VRFs Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260821140404.322081-3-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.