* [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support
@ 2026-08-21 14:03 Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 01/15] frr: add VRF-aware OSPF rendering Gabriel Goller
` (14 more replies)
0 siblings, 15 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
This RFC adds VRF support to SDN zones and fabrics.
A simple zone can now use either the default VRF or a dedicated VRF. The
simple zone dialog has a checkbox for this and uses the default VRF by
default. If the checkbox is disabled, the zone creates a dedicated VRF named
`vrf_<zone-name>`. EVPN zones always create a dedicated VRF. BGP and OSPF
fabrics can select a zone and run inside its VRF.
This is the second iteration. The first iteration added VRFs as a separate
SDN object with a distinct GUI subdirectory. This was scrapped because VRFs
are an implementation detail and should not be exposed on their own in the
GUI. Zones already provide a logical L3 building block and should map to VRFs
instead.
Route leaking between VRFs partly depends on the following FRR change, so it
will come a bit later: https://github.com/FRRouting/frr/pull/22426
As always, thanks to Stefan Hanreich for his help.
proxmox-ve-rs:
Gabriel Goller (5):
frr: add VRF-aware OSPF rendering
sdn: add zone references to OSPF and BGP fabrics
sdn: generate fabric routing configuration in zone VRFs
sdn: fix VRF route-map scoping and zone ID validation
tests: fabrics: add test for fabrics in VRFs
proxmox-frr-templates/templates/fabricd.jinja | 2 +-
.../templates/interface.jinja | 6 +-
proxmox-frr-templates/templates/isisd.jinja | 2 +-
proxmox-frr-templates/templates/ospfd.jinja | 12 +-
.../templates/protocol_routemaps.jinja | 13 ++
proxmox-frr/debian/control | 8 +-
proxmox-frr/src/ser/mod.rs | 8 +
proxmox-ve-config/src/sdn/fabric/frr.rs | 137 ++++++++----
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 +-
.../tests/fabric/cfg/vrf/fabrics.cfg | 43 ++++
.../tests/fabric/cfg/vrf/route-maps.cfg | 15 ++
proxmox-ve-config/tests/fabric/helper.rs | 20 ++
proxmox-ve-config/tests/fabric/main.rs | 207 +++++++++++++++++-
.../fabric/snapshots/fabric__vrf_pve.snap | 134 ++++++++++++
17 files changed, 627 insertions(+), 58 deletions(-)
create mode 100644 proxmox-ve-config/tests/fabric/cfg/vrf/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/vrf/route-maps.cfg
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__vrf_pve.snap
proxmox-perl-rs:
Gabriel Goller (3):
pve-rs: fabrics: assign network interfaces to configured VRFs
pve-rs: fabrics: make per-fabric status queries VRF-aware
pve-rs: fabrics: include VRF routes in aggregate status
pve-rs/src/bindings/sdn/fabrics.rs | 301 +++++++++++++++++++----------
pve-rs/src/sdn/status.rs | 19 +-
2 files changed, 207 insertions(+), 113 deletions(-)
pve-network:
Gabriel Goller (5):
sdn: add optional VRFs for simple zones
sdn: allow fabrics to use simple zone VRFs
sdn: always generate EVPN zone VRFs
api: sdn: allow EVPN zones as fabric VRFs
api: sdn: disallow BGP fabrics in EVPN zone VRFs
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 50 ++++++++++++++++---
.../API2/Network/SDN/Fabrics/FabricNode.pm | 18 +++++++
src/PVE/API2/Network/SDN/Zones.pm | 33 ++++++++++++
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 2 +-
src/PVE/Network/SDN/Fabrics.pm | 29 ++++++++++-
src/PVE/Network/SDN/Zones.pm | 17 +++++--
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 35 +++++++------
src/PVE/Network/SDN/Zones/Plugin.pm | 6 +++
src/PVE/Network/SDN/Zones/SimplePlugin.pm | 20 +++++++-
.../evpn/vrf-empty/expected_sdn_interfaces | 6 +++
src/test/zones/evpn/vrf-empty/interfaces | 2 +
src/test/zones/evpn/vrf-empty/sdn_config | 13 +++++
.../simple/vrf-empty/expected_sdn_interfaces | 5 ++
src/test/zones/simple/vrf-empty/interfaces | 2 +
src/test/zones/simple/vrf-empty/sdn_config | 12 +++++
.../zones/simple/vrf/expected_sdn_interfaces | 12 +++++
src/test/zones/simple/vrf/interfaces | 2 +
src/test/zones/simple/vrf/sdn_config | 17 +++++++
18 files changed, 253 insertions(+), 28 deletions(-)
create mode 100644 src/test/zones/evpn/vrf-empty/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/vrf-empty/interfaces
create mode 100644 src/test/zones/evpn/vrf-empty/sdn_config
create mode 100644 src/test/zones/simple/vrf-empty/expected_sdn_interfaces
create mode 100644 src/test/zones/simple/vrf-empty/interfaces
create mode 100644 src/test/zones/simple/vrf-empty/sdn_config
create mode 100644 src/test/zones/simple/vrf/expected_sdn_interfaces
create mode 100644 src/test/zones/simple/vrf/interfaces
create mode 100644 src/test/zones/simple/vrf/sdn_config
pve-manager:
Gabriel Goller (2):
ui: sdn: add VRF zone selection for fabrics
ui: sdn: expose EVPN zones as fabric VRFs
www/manager6/form/SDNZoneSelector.js | 16 +++++++++++++++-
www/manager6/sdn/fabrics/FabricEdit.js | 18 ++++++++++++++++++
www/manager6/sdn/zones/SimpleEdit.js | 12 +++++++++++-
3 files changed, 44 insertions(+), 2 deletions(-)
Summary over all repositories:
40 files changed, 1131 insertions(+), 201 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH proxmox-ve-rs 01/15] frr: add VRF-aware OSPF rendering
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Gabriel Goller
` (13 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Add an optional VRF name and render it in the FRR interface declaration.
Keep OSPF router configuration for custom VRFs separately from the
router in the default routing table (like we do with BGP).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr-templates/templates/fabricd.jinja | 2 +-
proxmox-frr-templates/templates/interface.jinja | 6 +++++-
proxmox-frr-templates/templates/isisd.jinja | 2 +-
proxmox-frr-templates/templates/ospfd.jinja | 12 +++++++++++-
proxmox-frr/debian/control | 8 ++++----
proxmox-frr/src/ser/mod.rs | 6 ++++++
6 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/proxmox-frr-templates/templates/fabricd.jinja b/proxmox-frr-templates/templates/fabricd.jinja
index 068959a97e0e..04283180679e 100644
--- a/proxmox-frr-templates/templates/fabricd.jinja
+++ b/proxmox-frr-templates/templates/fabricd.jinja
@@ -6,7 +6,7 @@ router openfabric {{ router_name }}
exit
{% endfor %}
{% for interface_name, interface_config in openfabric.interfaces|items %}
-{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6) %}
+{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6, interface_config.vrf) %}
{% if interface_config.fabric_id and interface_config.is_ipv4 %}
ip router openfabric {{ interface_config.fabric_id }}
{% endif %}
diff --git a/proxmox-frr-templates/templates/interface.jinja b/proxmox-frr-templates/templates/interface.jinja
index 4c5014844fc8..e52b05681bb8 100644
--- a/proxmox-frr-templates/templates/interface.jinja
+++ b/proxmox-frr-templates/templates/interface.jinja
@@ -1,6 +1,10 @@
-{% macro interface(name, addresses_v4, addresses_v6) %}
+{% macro interface(name, addresses_v4, addresses_v6, vrf) %}
!
+{% if vrf %}
+interface {{ name }} vrf {{ vrf }}
+{% else %}
interface {{ name }}
+{% endif %}
{% for address in addresses_v6 %}
ipv6 address {{address}}
{% endfor %}
diff --git a/proxmox-frr-templates/templates/isisd.jinja b/proxmox-frr-templates/templates/isisd.jinja
index e3c9292d575c..2d369d75eee6 100644
--- a/proxmox-frr-templates/templates/isisd.jinja
+++ b/proxmox-frr-templates/templates/isisd.jinja
@@ -20,7 +20,7 @@ router isis {{ router_name }}
exit
{% endfor %}
{% for interface_name, interface_config in isis.interfaces|items %}
-{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6) %}
+{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6, interface_config.vrf) %}
{% if interface_config.domain and interface_config.is_ipv4 %}
ip router isis {{ interface_config.domain }}
{% endif %}
diff --git a/proxmox-frr-templates/templates/ospfd.jinja b/proxmox-frr-templates/templates/ospfd.jinja
index 427ed7f9ddb4..5d440f2ff663 100644
--- a/proxmox-frr-templates/templates/ospfd.jinja
+++ b/proxmox-frr-templates/templates/ospfd.jinja
@@ -9,8 +9,18 @@ router ospf
{% endfor %}
exit
{% endif %}
+{% for vrf_name, router in ospf.vrf_router|items %}
+!
+router ospf vrf {{ vrf_name }}
+ ospf router-id {{ router.router_id }}
+{% for redistribution in router.redistribute %}
+ redistribute {{ redistribution.source }}{% if redistribution.metric is defined %} metric {{ redistribution.metric }}{% endif %}{% if redistribution.route_map is defined %} route-map {{ redistribution.route_map }}{% endif %}
+
+{% endfor %}
+exit
+{% endfor %}
{% for interface_name, interface_config in ospf.interfaces|items %}
-{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6) %}
+{% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6, interface_config.vrf) %}
ip ospf area {{ interface_config.area }}
{% if interface_config.passive %}
ip ospf passive
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index b118becedeea..183472b32d5e 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -10,8 +10,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-minijinja-2+default-dev (>= 2.5-~~) <!nocheck>,
librust-minijinja-2+loader-dev (>= 2.5-~~) <!nocheck>,
librust-minijinja-2+multi-template-dev (>= 2.5-~~) <!nocheck>,
- librust-proxmox-network-types-1+api-types-dev (>= 1.0.1-~~) <!nocheck>,
- librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~) <!nocheck>,
+ librust-proxmox-network-types-1+api-types-dev (>= 1.1.2-~~) <!nocheck>,
+ librust-proxmox-network-types-1+default-dev (>= 1.1.2-~~) <!nocheck>,
librust-proxmox-sdn-types-0.2+default-dev <!nocheck>,
librust-proxmox-serde-1+default-dev <!nocheck>,
librust-proxmox-sortable-macro-1+default-dev <!nocheck>,
@@ -36,8 +36,8 @@ Depends:
librust-minijinja-2+default-dev (>= 2.5-~~),
librust-minijinja-2+loader-dev (>= 2.5-~~),
librust-minijinja-2+multi-template-dev (>= 2.5-~~),
- librust-proxmox-network-types-1+api-types-dev (>= 1.0.1-~~),
- librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~),
+ librust-proxmox-network-types-1+api-types-dev (>= 1.1.2-~~),
+ librust-proxmox-network-types-1+default-dev (>= 1.1.2-~~),
librust-proxmox-sdn-types-0.2+default-dev,
librust-proxmox-serde-1+default-dev,
librust-proxmox-sortable-macro-1+default-dev,
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index b651121b4a82..22c503950ba3 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -146,6 +146,8 @@ pub struct Interface<T> {
pub addresses_v4: Vec<Ipv4Cidr>,
#[serde(default)]
pub addresses_v6: Vec<Ipv6Cidr>,
+ #[serde(default)]
+ pub vrf: Option<VrfName>,
#[serde(flatten)]
pub properties: T,
@@ -155,6 +157,7 @@ impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricI
Interface {
addresses_v4: Vec::new(),
addresses_v6: Vec::new(),
+ vrf: None,
properties: value,
}
}
@@ -165,6 +168,7 @@ impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
Interface {
addresses_v4: Vec::new(),
addresses_v6: Vec::new(),
+ vrf: None,
properties: value,
}
}
@@ -259,6 +263,8 @@ pub struct OspfFrrConfig {
#[serde(default)]
pub router: Option<ospf::OspfRouter>,
#[serde(default)]
+ pub vrf_router: BTreeMap<VrfName, ospf::OspfRouter>,
+ #[serde(default)]
pub interfaces: BTreeMap<InterfaceName, Interface<ospf::OspfInterface>>,
}
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics
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
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 03/15] sdn: generate fabric routing configuration in zone VRFs Gabriel Goller
` (12 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
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
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-ve-rs 03/15] sdn: generate fabric routing configuration in zone VRFs
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 ` [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Gabriel Goller
@ 2026-08-21 14:03 ` 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
` (11 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Map a fabric zone to the corresponding vrf_<zone> FRR instance. Generate
OSPF routers and interfaces in that VRF, and store BGP routers under the
same VRF name. Keep fabrics without a zone in the default routing table.
Collect OSPF redistribution rules on the router for the selected VRF so
that several fabrics in one VRF contribute to the same OSPF instance.
This keeps protocol configuration and interface configuration in the
routing table selected by the zone.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/frr.rs | 82 ++++++++++++++++++-------
1 file changed, 61 insertions(+), 21 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 9203dcd16dae..ca0fbfe6d542 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -24,10 +24,13 @@ use crate::sdn::fabric::section_config::protocol::{
openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
ospf::OspfInterfaceProperties,
};
-use crate::sdn::fabric::section_config::{fabric::FabricId, node::NodeId};
+use crate::sdn::fabric::section_config::{
+ fabric::{FabricId, ZoneId},
+ node::NodeId,
+};
use crate::sdn::fabric::{FabricConfig, FabricEntry};
-/// Constructs the FRR config from the the passed [`Valid<FabricConfig>`].
+/// Constructs the FRR config from the passed [`Valid<FabricConfig>`].
///
/// Iterates over the [`FabricConfig`] and constructs all the FRR routers, interfaces, route-maps,
/// etc.
@@ -207,27 +210,50 @@ pub fn build_fabric(
.get_or_insert(node.ip().expect("node must have an ipv4 address"));
let fabric = ospf_entry.fabric_section();
+ let vrf_name = fabric_vrf_name(fabric.properties().zone());
let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?;
let frr_area = ser::ospf::Area::new(frr_word_area)?;
- if frr_config.ospf.router.is_none() {
- let mut ospf_router = build_ospf_router(*router_id)?;
-
- ospf_router.redistribute = fabric
- .properties()
- .redistributions()
- .into_iter()
- .cloned()
- .map(OspfRedistribution::from)
- .collect();
+ let redistributions = fabric
+ .properties()
+ .redistributions()
+ .into_iter()
+ .cloned()
+ .map(OspfRedistribution::from);
+
+ let ospf_router = if vrf_name == VrfName::Default {
+ if frr_config.ospf.router.is_none() {
+ frr_config.ospf.router = Some(build_ospf_router(*router_id)?);
+ }
+ frr_config
+ .ospf
+ .router
+ .as_mut()
+ .expect("router was inserted")
+ } else {
+ if !frr_config.ospf.vrf_router.contains_key(&vrf_name) {
+ frr_config
+ .ospf
+ .vrf_router
+ .insert(vrf_name.clone(), build_ospf_router(*router_id)?);
+ }
+ frr_config
+ .ospf
+ .vrf_router
+ .get_mut(&vrf_name)
+ .expect("router was inserted")
+ };
+ ospf_router.redistribute.extend(redistributions);
- frr_config.ospf.router = Some(ospf_router);
- }
+ let interface_vrf = match &vrf_name {
+ VrfName::Default => None,
+ custom => Some(custom.clone()),
+ };
// Add dummy interface
let (interface, interface_name) =
- build_ospf_dummy_interface(fabric_id, frr_area.clone())?;
+ build_ospf_dummy_interface(fabric_id, frr_area.clone(), interface_vrf.clone())?;
if frr_config
.ospf
@@ -242,7 +268,7 @@ pub fn build_fabric(
for interface in node.properties().interfaces.iter() {
let (interface, interface_name) =
- build_ospf_interface(frr_area.clone(), interface)?;
+ build_ospf_interface(frr_area.clone(), interface, interface_vrf.clone())?;
if frr_config
.ospf
@@ -309,6 +335,7 @@ pub fn build_fabric(
};
let fabric = bgp_entry.fabric_section();
+ let vrf_name = fabric_vrf_name(fabric.properties().zone());
let local_asn = properties.asn().as_u32();
@@ -323,7 +350,7 @@ pub fn build_fabric(
let local_as = frr_config
.bgp
.vrf_router
- .get(&VrfName::Default)
+ .get(&vrf_name)
.filter(|existing| existing.asn != local_asn)
.map(|_| LocalAsSettings {
asn: local_asn,
@@ -562,10 +589,10 @@ pub fn build_fabric(
custom_frr_config: Default::default(),
};
- if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) {
+ if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&vrf_name) {
existing.merge_fabric(router);
} else {
- frr_config.bgp.vrf_router.insert(VrfName::Default, router);
+ frr_config.bgp.vrf_router.insert(vrf_name, router);
}
}
}
@@ -595,6 +622,13 @@ pub fn build_fabric(
Ok(())
}
+fn fabric_vrf_name(zone: Option<&ZoneId>) -> VrfName {
+ match zone {
+ Some(zone) => VrfName::Custom(format!("vrf_{zone}")),
+ None => VrfName::Default,
+ }
+}
+
/// Helper that builds a OSPF router with a the router_id.
fn build_ospf_router(router_id: Ipv4Addr) -> Result<OspfRouter, anyhow::Error> {
Ok(OspfRouter::new(router_id))
@@ -615,6 +649,7 @@ fn build_openfabric_router(
fn build_ospf_interface(
area: ser::ospf::Area,
interface: &OspfInterfaceProperties,
+ vrf: Option<VrfName>,
) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
let frr_interface = ser::ospf::OspfInterface {
area,
@@ -633,13 +668,16 @@ fn build_ospf_interface(
};
let interface_name = interface.name.as_ref().try_into()?;
- Ok((frr_interface.into(), interface_name))
+ let mut interface: Interface<OspfInterface> = frr_interface.into();
+ interface.vrf = vrf;
+ Ok((interface, interface_name))
}
/// Helper that builds the OSPF dummy interface using the [`FabricId`] and the [`ospf::Area`].
fn build_ospf_dummy_interface(
fabric_id: &FabricId,
area: ospf::Area,
+ vrf: Option<VrfName>,
) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
let frr_interface = ser::ospf::OspfInterface {
area,
@@ -647,7 +685,9 @@ fn build_ospf_dummy_interface(
network_type: None,
};
let interface_name = format!("dummy_{}", fabric_id).try_into()?;
- Ok((frr_interface.into(), interface_name))
+ let mut interface: Interface<OspfInterface> = frr_interface.into();
+ interface.vrf = vrf;
+ Ok((interface, interface_name))
}
/// Helper that builds the OpenFabric interface.
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-ve-rs 04/15] sdn: fix VRF route-map scoping and zone ID validation
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (2 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-ve-rs 05/15] tests: fabrics: add test for fabrics in VRFs Gabriel Goller
` (10 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Attach protocol route-maps inside the VRF where the protocol runs. Give
VRF route-maps distinct names so fabrics in separate routing tables do
not overwrite or refer to each other. Keep the existing global route-map
handling for protocols in the default routing table.
Add the final BGP permit entry to both global and VRF-specific route-maps
so unrelated routes are not dropped by the implicit deny.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../templates/protocol_routemaps.jinja | 13 ++++
proxmox-frr/src/ser/mod.rs | 2 +
proxmox-ve-config/src/sdn/fabric/frr.rs | 55 ++++++++++-----
proxmox-ve-config/tests/fabric/main.rs | 69 ++++++++++++++++++-
4 files changed, 119 insertions(+), 20 deletions(-)
diff --git a/proxmox-frr-templates/templates/protocol_routemaps.jinja b/proxmox-frr-templates/templates/protocol_routemaps.jinja
index 38c00b6407ee..0f4ca5fdbe0c 100644
--- a/proxmox-frr-templates/templates/protocol_routemaps.jinja
+++ b/proxmox-frr-templates/templates/protocol_routemaps.jinja
@@ -8,3 +8,16 @@ ip protocol {{ protocol_name }} route-map {{ protocol_routemap.v4 }}
ipv6 protocol {{ protocol_name }} route-map {{ protocol_routemap.v6 }}
{% endif %}
{% endfor %}
+{% for vrf_name, protocol_routemaps in vrf_protocol_routemaps | items %}
+!
+vrf {{ vrf_name }}
+{% for protocol_name, protocol_routemap in protocol_routemaps | items %}
+{% if protocol_routemap.v4 %}
+ ip protocol {{ protocol_name }} route-map {{ protocol_routemap.v4 }}
+{% endif %}
+{% if protocol_routemap.v6 %}
+ ipv6 protocol {{ protocol_name }} route-map {{ protocol_routemap.v6 }}
+{% endif %}
+{% endfor %}
+exit-vrf
+{% endfor %}
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 22c503950ba3..20d7240d72d3 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -232,6 +232,8 @@ pub struct FrrConfig {
#[serde(default)]
pub protocol_routemaps: BTreeMap<FrrProtocol, IpProtocolRouteMap>,
#[serde(default)]
+ pub vrf_protocol_routemaps: BTreeMap<VrfName, BTreeMap<FrrProtocol, IpProtocolRouteMap>>,
+ #[serde(default)]
pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
#[serde(default)]
pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index ca0fbfe6d542..2849a1ee0596 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
use std::net::{IpAddr, Ipv4Addr};
use tracing;
@@ -40,6 +41,7 @@ pub fn build_fabric(
frr_config: &mut FrrConfig,
) -> Result<(), anyhow::Error> {
let mut routemap_seq = 100;
+ let mut fabric_bgp_routemaps = BTreeSet::new();
let mut current_router_id: Option<Ipv4Addr> = None;
let mut current_net: Option<Net> = None;
@@ -280,7 +282,7 @@ pub fn build_fabric(
}
}
- let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
+ let routemap_name = protocol_routemap_name("pve_ospf", &vrf_name);
let routemap = frr_config
.routemaps
.entry(routemap_name.clone())
@@ -317,10 +319,7 @@ pub fn build_fabric(
routemap.push(routemap_entry);
- let protocol_routemap = frr_config
- .protocol_routemaps
- .entry(FrrProtocol::Ospf)
- .or_default();
+ let protocol_routemap = protocol_routemap(frr_config, &vrf_name, FrrProtocol::Ospf);
protocol_routemap.v4 = Some(routemap_name);
}
@@ -465,7 +464,7 @@ pub fn build_fabric(
networks: vec![Ipv4Cidr::from(ip)],
});
- let routemap_name = ser::route_map::RouteMapName::new("pve_bgp".to_owned());
+ let routemap_name = protocol_routemap_name("pve_bgp", &vrf_name);
let routemap = frr_config
.routemaps
.entry(routemap_name.clone())
@@ -477,11 +476,10 @@ pub fn build_fabric(
routemap.push(routemap_entry);
- let protocol_routemap = frr_config
- .protocol_routemaps
- .entry(FrrProtocol::Bgp)
- .or_default();
+ let protocol_routemap =
+ protocol_routemap(frr_config, &vrf_name, FrrProtocol::Bgp);
+ fabric_bgp_routemaps.insert(routemap_name.clone());
protocol_routemap.v4 = Some(routemap_name);
}
@@ -552,7 +550,7 @@ pub fn build_fabric(
redistribute,
});
- let routemap_name = ser::route_map::RouteMapName::new("pve_bgp6".to_owned());
+ let routemap_name = protocol_routemap_name("pve_bgp6", &vrf_name);
let routemap = frr_config
.routemaps
.entry(routemap_name.clone())
@@ -564,11 +562,10 @@ pub fn build_fabric(
routemap.push(routemap_entry);
- let protocol_routemap = frr_config
- .protocol_routemaps
- .entry(FrrProtocol::Bgp)
- .or_default();
+ let protocol_routemap =
+ protocol_routemap(frr_config, &vrf_name, FrrProtocol::Bgp);
+ fabric_bgp_routemaps.insert(routemap_name.clone());
protocol_routemap.v6 = Some(routemap_name);
};
@@ -602,10 +599,7 @@ pub fn build_fabric(
// routes (e.g. EVPN-imported VRF routes) reach the kernel unchanged.
// Without this, the implicit deny at the end of the route-map would drop
// them.
- for routemap_name in [
- ser::route_map::RouteMapName::new("pve_bgp".to_owned()),
- ser::route_map::RouteMapName::new("pve_bgp6".to_owned()),
- ] {
+ for routemap_name in fabric_bgp_routemaps {
if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
routemap.push(RouteMapEntry {
seq: 65535,
@@ -629,6 +623,29 @@ fn fabric_vrf_name(zone: Option<&ZoneId>) -> VrfName {
}
}
+fn protocol_routemap_name(prefix: &str, vrf_name: &VrfName) -> RouteMapName {
+ match vrf_name {
+ VrfName::Default => RouteMapName::new(prefix.to_owned()),
+ VrfName::Custom(vrf_name) => RouteMapName::new(format!("{prefix}_{vrf_name}")),
+ }
+}
+
+fn protocol_routemap<'a>(
+ frr_config: &'a mut FrrConfig,
+ vrf_name: &VrfName,
+ protocol: FrrProtocol,
+) -> &'a mut ser::IpProtocolRouteMap {
+ match vrf_name {
+ VrfName::Default => frr_config.protocol_routemaps.entry(protocol).or_default(),
+ custom => frr_config
+ .vrf_protocol_routemaps
+ .entry(custom.clone())
+ .or_default()
+ .entry(protocol)
+ .or_default(),
+ }
+}
+
/// Helper that builds a OSPF router with a the router_id.
fn build_ospf_router(router_id: Ipv4Addr) -> Result<OspfRouter, anyhow::Error> {
Ok(OspfRouter::new(router_id))
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index aa76adb9819f..f65c5d26aeed 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -3,7 +3,7 @@ use std::net::Ipv4Addr;
use std::str::FromStr;
use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF};
-use proxmox_frr::ser::{FrrConfig, VrfName, serializer::dump};
+use proxmox_frr::ser::{FrrConfig, FrrProtocol, VrfName, serializer::dump};
use proxmox_ve_config::sdn::fabric::{
FabricConfig,
frr::build_fabric,
@@ -170,6 +170,19 @@ fn openfabric_ipv6_only() {
insta::assert_snapshot!(helper::reference_name!("pve"), output);
}
+#[test]
+fn zone_id_boundaries() {
+ for zone in ["a", "abcdefgh"] {
+ let raw = format!("ospf_fabric: test\n\tarea 0\n\tip_prefix 10.0.0.0/24\n\tzone {zone}\n");
+ assert!(FabricConfig::parse_section_config(&raw).is_ok());
+ }
+
+ for zone in ["1", "abcdefghi"] {
+ let raw = format!("ospf_fabric: test\n\tarea 0\n\tip_prefix 10.0.0.0/24\n\tzone {zone}\n");
+ assert!(FabricConfig::parse_section_config(&raw).is_err());
+ }
+}
+
#[test]
fn vrf_prefix_overlap_is_scoped_by_zone() {
let raw = r#"
@@ -248,6 +261,60 @@ bgp_fabric: test
}
}
+#[test]
+fn protocol_routemaps_are_scoped_by_vrf() {
+ let raw = r#"
+ospf_fabric: ospf
+ area 0
+ ip_prefix 10.0.0.0/24
+ zone zoneone
+
+ospf_node: ospf_pve
+ interfaces name=eth0
+ ip 10.0.0.1
+
+bgp_fabric: bgp
+ bfd 0
+ ip_prefix 10.0.1.0/24
+ zone zonetwo
+
+bgp_node: bgp_pve
+ asn 65001
+ interfaces name=eth1
+ ip 10.0.1.1
+ role internal
+"#;
+ let config = FabricConfig::parse_section_config(raw).unwrap();
+ let mut frr_config = FrrConfig::default();
+
+ build_fabric(NodeId::from_str("pve").unwrap(), config, &mut frr_config).unwrap();
+
+ let ospf_routemap = &frr_config.vrf_protocol_routemaps
+ [&VrfName::Custom("vrf_zoneone".to_owned())][&FrrProtocol::Ospf];
+ assert_eq!(
+ ospf_routemap.v4.as_ref().unwrap(),
+ &proxmox_frr::ser::route_map::RouteMapName::new("pve_ospf_vrf_zoneone".to_owned())
+ );
+ let bgp_routemap = &frr_config.vrf_protocol_routemaps
+ [&VrfName::Custom("vrf_zonetwo".to_owned())][&FrrProtocol::Bgp];
+ assert_eq!(
+ bgp_routemap.v4.as_ref().unwrap(),
+ &proxmox_frr::ser::route_map::RouteMapName::new("pve_bgp_vrf_zonetwo".to_owned())
+ );
+ assert!(
+ frr_config
+ .protocol_routemaps
+ .get(&FrrProtocol::Ospf)
+ .is_none()
+ );
+ assert!(
+ frr_config
+ .protocol_routemaps
+ .get(&FrrProtocol::Bgp)
+ .is_none()
+ );
+}
+
#[test]
fn bgp_default() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-ve-rs 05/15] tests: fabrics: add test for fabrics in VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (3 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 06/15] pve-rs: fabrics: assign network interfaces to configured VRFs Gabriel Goller
` (9 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Add an integration test covering OSPF and BGP fabrics assigned to
multiple SDN zone VRFs.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../tests/fabric/cfg/vrf/fabrics.cfg | 43 ++++++
.../tests/fabric/cfg/vrf/route-maps.cfg | 15 ++
proxmox-ve-config/tests/fabric/helper.rs | 20 +++
proxmox-ve-config/tests/fabric/main.rs | 75 ++++++++--
.../fabric/snapshots/fabric__vrf_pve.snap | 134 ++++++++++++++++++
5 files changed, 275 insertions(+), 12 deletions(-)
create mode 100644 proxmox-ve-config/tests/fabric/cfg/vrf/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/vrf/route-maps.cfg
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__vrf_pve.snap
diff --git a/proxmox-ve-config/tests/fabric/cfg/vrf/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/vrf/fabrics.cfg
new file mode 100644
index 000000000000..05a1cb57642a
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/vrf/fabrics.cfg
@@ -0,0 +1,43 @@
+ospf_fabric: ospfred
+ area 0
+ ip_prefix 10.0.0.0/24
+ zone red
+
+ospf_fabric: ospfblue
+ area 0
+ ip_prefix 10.0.0.0/24
+ zone blue
+
+ospf_node: ospfred_pve
+ interfaces name=ens18
+ ip 10.0.0.1
+
+ospf_node: ospfblue_pve
+ interfaces name=ens19
+ ip 10.0.0.1
+
+bgp_fabric: bgpred
+ bfd 0
+ ip_prefix 10.1.0.0/24
+ route_map_in red-in
+ route_map_out red-out
+ zone red
+
+bgp_fabric: bgpblue
+ bfd 0
+ ip_prefix 10.1.0.0/24
+ route_map_in blue-in
+ route_map_out blue-out
+ zone blue
+
+bgp_node: bgpred_pve
+ asn 65001
+ interfaces name=ens20
+ ip 10.1.0.1
+ role internal
+
+bgp_node: bgpblue_pve
+ asn 65002
+ interfaces name=ens21
+ ip 10.1.0.1
+ role internal
diff --git a/proxmox-ve-config/tests/fabric/cfg/vrf/route-maps.cfg b/proxmox-ve-config/tests/fabric/cfg/vrf/route-maps.cfg
new file mode 100644
index 000000000000..b382fc369b51
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/vrf/route-maps.cfg
@@ -0,0 +1,15 @@
+route-map-entry: blue-in_10
+ action permit
+ set key=local-preference,value=200
+
+route-map-entry: blue-out_10
+ action permit
+ set key=metric,value=20
+
+route-map-entry: red-in_10
+ action permit
+ set key=local-preference,value=100
+
+route-map-entry: red-out_10
+ action permit
+ set key=metric,value=10
diff --git a/proxmox-ve-config/tests/fabric/helper.rs b/proxmox-ve-config/tests/fabric/helper.rs
index 93404b8eb7af..ec464d334a4c 100644
--- a/proxmox-ve-config/tests/fabric/helper.rs
+++ b/proxmox-ve-config/tests/fabric/helper.rs
@@ -18,6 +18,24 @@ macro_rules! get_fabrics_config {
}};
}
+#[allow(unused_macros)]
+macro_rules! get_route_maps_config {
+ () => {{
+ fn f() {}
+ fn type_name_of<T>(_: T) -> &'static str {
+ std::any::type_name::<T>()
+ }
+ let mut name = type_name_of(f);
+
+ name = match &name[..name.len() - 3].rfind(':') {
+ Some(pos) => &name[pos + 1..name.len() - 3],
+ None => &name[..name.len() - 3],
+ };
+ let real_filename = format!("tests/fabric/cfg/{name}/route-maps.cfg");
+ &std::fs::read_to_string(real_filename).expect("cannot find route-map config file")
+ }};
+}
+
#[allow(unused_macros)]
macro_rules! reference_name {
($suffix:expr) => {{
@@ -40,4 +58,6 @@ macro_rules! reference_name {
#[allow(unused_imports)]
pub(crate) use get_fabrics_config;
#[allow(unused_imports)]
+pub(crate) use get_route_maps_config;
+#[allow(unused_imports)]
pub(crate) use reference_name;
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index f65c5d26aeed..249816b54327 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -3,14 +3,19 @@ use std::net::Ipv4Addr;
use std::str::FromStr;
use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF};
-use proxmox_frr::ser::{FrrConfig, FrrProtocol, VrfName, serializer::dump};
-use proxmox_ve_config::sdn::fabric::{
- FabricConfig,
- frr::build_fabric,
- section_config::{
- fabric::{FabricId, FabricUpdater},
- node::NodeId,
+use proxmox_frr::ser::route_map::{AccessAction, RouteMapEntry, RouteMapName};
+use proxmox_frr::ser::{FrrConfig, FrrProtocol, IpProtocolRouteMap, VrfName, serializer::dump};
+use proxmox_section_config::typed::ApiSectionDataEntry;
+use proxmox_ve_config::sdn::{
+ fabric::{
+ FabricConfig,
+ frr::build_fabric,
+ section_config::{
+ fabric::{FabricId, FabricUpdater},
+ node::NodeId,
+ },
},
+ route_map::{RouteMap, frr::build_frr_route_maps},
};
mod helper;
@@ -286,9 +291,31 @@ bgp_node: bgp_pve
"#;
let config = FabricConfig::parse_section_config(raw).unwrap();
let mut frr_config = FrrConfig::default();
+ let correct_src = RouteMapName::new("correct_src".to_owned());
+ let correct_src_entry = RouteMapEntry {
+ seq: 1,
+ action: AccessAction::Permit,
+ matches: Vec::new(),
+ sets: Vec::new(),
+ custom_frr_config: Vec::new(),
+ call: None,
+ exit_action: None,
+ };
+ frr_config
+ .routemaps
+ .insert(correct_src.clone(), vec![correct_src_entry.clone()]);
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Bgp,
+ IpProtocolRouteMap {
+ v4: Some(correct_src.clone()),
+ v6: None,
+ },
+ );
build_fabric(NodeId::from_str("pve").unwrap(), config, &mut frr_config).unwrap();
+ assert_eq!(frr_config.routemaps[&correct_src], vec![correct_src_entry]);
+
let ospf_routemap = &frr_config.vrf_protocol_routemaps
[&VrfName::Custom("vrf_zoneone".to_owned())][&FrrProtocol::Ospf];
assert_eq!(
@@ -307,14 +334,38 @@ bgp_node: bgp_pve
.get(&FrrProtocol::Ospf)
.is_none()
);
- assert!(
- frr_config
- .protocol_routemaps
- .get(&FrrProtocol::Bgp)
- .is_none()
+ assert_eq!(
+ frr_config.protocol_routemaps[&FrrProtocol::Bgp].v4,
+ Some(correct_src)
);
}
+#[test]
+fn vrf() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let route_maps =
+ RouteMap::parse_section_config("route-maps.cfg", helper::get_route_maps_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_route_maps(
+ route_maps
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )
+ .unwrap();
+ build_fabric(
+ NodeId::from_str("pve").expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
+
#[test]
fn bgp_default() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__vrf_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__vrf_pve.snap
new file mode 100644
index 000000000000..84973240deb4
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__vrf_pve.snap
@@ -0,0 +1,134 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002 vrf vrf_blue
+ bgp router-id 10.1.0.1
+ no bgp default ipv4-unicast
+ neighbor bgpblue peer-group
+ neighbor bgpblue remote-as external
+ neighbor ens21 interface peer-group bgpblue
+ !
+ address-family ipv4 unicast
+ network 10.1.0.1/32
+ neighbor bgpblue activate
+ neighbor bgpblue soft-reconfiguration inbound
+ neighbor bgpblue route-map pve_bgp_bgpblue_in in
+ neighbor bgpblue route-map blue-out out
+ exit-address-family
+exit
+!
+router bgp 65001 vrf vrf_red
+ bgp router-id 10.1.0.1
+ no bgp default ipv4-unicast
+ neighbor bgpred peer-group
+ neighbor bgpred remote-as external
+ neighbor ens20 interface peer-group bgpred
+ !
+ address-family ipv4 unicast
+ network 10.1.0.1/32
+ neighbor bgpred activate
+ neighbor bgpred soft-reconfiguration inbound
+ neighbor bgpred route-map pve_bgp_bgpred_in in
+ neighbor bgpred route-map red-out out
+ exit-address-family
+exit
+!
+router ospf vrf vrf_blue
+ ospf router-id 10.0.0.1
+exit
+!
+router ospf vrf vrf_red
+ ospf router-id 10.0.0.1
+exit
+!
+interface dummy_ospfblue vrf vrf_blue
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface dummy_ospfred vrf vrf_red
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens18 vrf vrf_red
+ ip ospf area 0
+ ip ospf network point-to-point
+exit
+!
+interface ens19 vrf vrf_blue
+ ip ospf area 0
+ ip ospf network point-to-point
+exit
+!
+access-list pve_bgp_bgpblue_ips permit 10.1.0.0/24
+!
+access-list pve_bgp_bgpred_ips permit 10.1.0.0/24
+!
+access-list pve_ospf_ospfblue_ips permit 10.0.0.0/24
+!
+access-list pve_ospf_ospfred_ips permit 10.0.0.0/24
+!
+route-map blue-in permit 10
+ set local-preference 200
+exit
+!
+route-map blue-out permit 10
+ set metric 20
+exit
+!
+route-map pve_bgp_bgpblue_in permit 10
+ match ip address pve_bgp_bgpblue_ips
+ call blue-in
+exit
+!
+route-map pve_bgp_bgpred_in permit 10
+ match ip address pve_bgp_bgpred_ips
+ call red-in
+exit
+!
+route-map pve_bgp_vrf_blue permit 100
+ match ip address pve_bgp_bgpblue_ips
+ set src 10.1.0.1
+exit
+!
+route-map pve_bgp_vrf_blue permit 65535
+exit
+!
+route-map pve_bgp_vrf_red permit 110
+ match ip address pve_bgp_bgpred_ips
+ set src 10.1.0.1
+exit
+!
+route-map pve_bgp_vrf_red permit 65535
+exit
+!
+route-map pve_ospf_vrf_blue permit 120
+ match ip address pve_ospf_ospfblue_ips
+ set src 10.0.0.1
+exit
+!
+route-map pve_ospf_vrf_red permit 130
+ match ip address pve_ospf_ospfred_ips
+ set src 10.0.0.1
+exit
+!
+route-map red-in permit 10
+ set local-preference 100
+exit
+!
+route-map red-out permit 10
+ set metric 10
+exit
+!
+vrf vrf_blue
+ ip protocol bgp route-map pve_bgp_vrf_blue
+ ip protocol ospf route-map pve_ospf_vrf_blue
+exit-vrf
+!
+vrf vrf_red
+ ip protocol bgp route-map pve_bgp_vrf_red
+ ip protocol ospf route-map pve_ospf_vrf_red
+exit-vrf
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-perl-rs 06/15] pve-rs: fabrics: assign network interfaces to configured VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (4 preceding siblings ...)
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 ` 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
` (8 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Add the VRF stanza to generated interface configuration when a fabric
has a VRF. Apply it to dummy addresses, protocol interfaces, and BGP
unnumbered interfaces so traffic enters the correct routing table.
Fabrics without a VRF keep their existing configuration.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 69 +++++++++++++++++++++++-------
1 file changed, 53 insertions(+), 16 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index f96b6b1c656f..98470eeaa397 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -582,8 +582,23 @@ pub mod pve_rs_sdn_fabrics {
Ok(interface)
}
+ pub(crate) fn fabric_vrf_name(fabric: &FabricEntry) -> Option<String> {
+ let zone = match fabric {
+ FabricEntry::Ospf(entry) => entry.fabric_section().properties().zone(),
+ FabricEntry::Bgp(entry) => entry.fabric_section().properties().zone(),
+ FabricEntry::Openfabric(_) | FabricEntry::WireGuard(_) => None,
+ };
+
+ zone.map(|zone| format!("vrf_{zone}"))
+ }
+
/// Helper function to generate the default `/etc/network/interfaces` config for a given CIDR.
- fn render_interface(name: &str, cidr: Cidr, link_type: Option<&str>) -> Result<String, Error> {
+ fn render_interface(
+ name: &str,
+ cidr: Cidr,
+ link_type: Option<&str>,
+ vrf: Option<&str>,
+ ) -> Result<String, Error> {
let mut interface = String::new();
writeln!(interface, "auto {name}")?;
@@ -595,6 +610,9 @@ pub mod pve_rs_sdn_fabrics {
if let Some(link_type) = link_type {
writeln!(interface, "\tlink-type {link_type}")?;
}
+ if let Some(vrf) = vrf {
+ writeln!(interface, "\tvrf {vrf}")?;
+ }
writeln!(interface, "\tip-forward 1")?;
Ok(interface)
@@ -604,12 +622,14 @@ pub mod pve_rs_sdn_fabrics {
interfaces: &mut String,
fabric: &Fabric,
node: &ConfigNode,
+ vrf: Option<&str>,
) -> Result<(), Error> {
if let Some(ip) = node.ip() {
let interface = render_interface(
&format!("dummy_{}", fabric.id()),
Cidr::new_v4(ip, 32)?,
Some("dummy"),
+ vrf,
)?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
@@ -620,6 +640,7 @@ pub mod pve_rs_sdn_fabrics {
&format!("dummy_{}", fabric.id()),
Cidr::new_v6(ip6, 128)?,
Some("dummy"),
+ vrf,
)?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
@@ -637,28 +658,35 @@ pub mod pve_rs_sdn_fabrics {
let config = this.fabric_config.lock().unwrap();
let mut interfaces = String::new();
- let node_fabrics = config.values().filter_map(|entry| {
- entry
- .get_node(&node_id)
- .map(|node| (entry.fabric(), node))
- .ok()
- });
+ let node_fabrics = config
+ .values()
+ .filter_map(|entry| entry.get_node(&node_id).map(|node| (entry, node)).ok());
- for (fabric, node) in node_fabrics {
- render_dummy_interfaces(&mut interfaces, fabric, node)?;
+ for (entry, node) in node_fabrics {
+ let fabric = entry.fabric();
+ let vrf = fabric_vrf_name(entry);
+ render_dummy_interfaces(&mut interfaces, fabric, node, vrf.as_deref())?;
match node {
ConfigNode::Openfabric(node_section) => {
for interface in node_section.properties().interfaces() {
if let Some(ip) = interface.ip() {
- let interface =
- render_interface(interface.name(), Cidr::from(ip), None)?;
+ let interface = render_interface(
+ interface.name(),
+ Cidr::from(ip),
+ None,
+ vrf.as_deref(),
+ )?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
if let Some(ip) = interface.ip6() {
- let interface =
- render_interface(interface.name(), Cidr::from(ip), None)?;
+ let interface = render_interface(
+ interface.name(),
+ Cidr::from(ip),
+ None,
+ vrf.as_deref(),
+ )?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
@@ -675,7 +703,8 @@ pub mod pve_rs_sdn_fabrics {
} else {
anyhow::bail!("there has to be a ipv4 or ipv6 node address");
});
- let interface = render_interface(interface.name(), cidr, None)?;
+ let interface =
+ render_interface(interface.name(), cidr, None, vrf.as_deref())?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
@@ -685,8 +714,12 @@ pub mod pve_rs_sdn_fabrics {
for interface in node_section.properties().interfaces() {
writeln!(interfaces)?;
if let Some(ip) = interface.ip() {
- let interface =
- render_interface(interface.name(), Cidr::from(ip), None)?;
+ let interface = render_interface(
+ interface.name(),
+ Cidr::from(ip),
+ None,
+ vrf.as_deref(),
+ )?;
write!(interfaces, "{interface}")?;
} else {
let interface = render_interface(
@@ -695,6 +728,7 @@ pub mod pve_rs_sdn_fabrics {
anyhow::anyhow!("there has to be a ipv4 address")
})?)),
None,
+ vrf.as_deref(),
)?;
write!(interfaces, "{interface}")?;
}
@@ -762,6 +796,9 @@ pub mod pve_rs_sdn_fabrics {
writeln!(interfaces)?;
writeln!(interfaces, "auto {name}")?;
writeln!(interfaces, "iface {name} inet manual")?;
+ if let Some(vrf) = &vrf {
+ writeln!(interfaces, "\tvrf {vrf}")?;
+ }
writeln!(interfaces, "\tip-forward 1")?;
writeln!(interfaces, "\tip6-forward 1")?;
// BGP unnumbered uses RAs to discover peer link-local
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-perl-rs 07/15] pve-rs: fabrics: make per-fabric status queries VRF-aware
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (5 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH proxmox-perl-rs 08/15] pve-rs: fabrics: include VRF routes in aggregate status Gabriel Goller
` (7 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Use the fabric VRF in FRR route, neighbor, and interface queries for
OSPF and BGP.
Keep the existing commands for fabrics in the default routing table.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 63 +++++++++++++++++++-----------
1 file changed, 41 insertions(+), 22 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 98470eeaa397..0fb1ccec9b7c 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -848,6 +848,7 @@ pub mod pve_rs_sdn_fabrics {
};
let fabric = config.get_fabric(&fabric_id)?;
+ let vrf = fabric_vrf_name(fabric);
match fabric {
FabricEntry::Openfabric(_) => {
let openfabric_ipv4_routes_string = String::from_utf8(
@@ -885,12 +886,12 @@ pub mod pve_rs_sdn_fabrics {
)
}
FabricEntry::Ospf(_) => {
- let ospf_routes_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ip route ospf json'"])
- .output()?
- .stdout,
- )?;
+ let command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show ip route vrf {vrf} ospf json'"),
+ None => "vtysh -c 'show ip route ospf json'".to_string(),
+ };
+ let ospf_routes_string =
+ String::from_utf8(Command::new("sh").args(["-c", &command]).output()?.stdout)?;
let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() {
proxmox_frr::de::Routes::default()
} else {
@@ -902,10 +903,18 @@ pub mod pve_rs_sdn_fabrics {
}
FabricEntry::WireGuard(_) => Ok(Vec::new()),
FabricEntry::Bgp(_) => {
+ let ipv4_command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show ip route vrf {vrf} bgp json'"),
+ None => "vtysh -c 'show ip route bgp json'".to_string(),
+ };
+ let ipv6_command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show ipv6 route vrf {vrf} bgp json'"),
+ None => "vtysh -c 'show ipv6 route bgp json'".to_string(),
+ };
let bgp_ipv4_routes_string = String::from_utf8(
Command::new("sh")
.env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show ip route bgp json'"])
+ .args(["-c", &ipv4_command])
.output()?
.stdout,
)?;
@@ -913,7 +922,7 @@ pub mod pve_rs_sdn_fabrics {
let bgp_ipv6_routes_string = String::from_utf8(
Command::new("sh")
.env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show ipv6 route bgp json'"])
+ .args(["-c", &ipv6_command])
.output()?
.stdout,
)?;
@@ -948,6 +957,7 @@ pub mod pve_rs_sdn_fabrics {
};
let fabric = config.get_fabric(&fabric_id)?;
+ let vrf = fabric_vrf_name(fabric);
match fabric {
FabricEntry::Openfabric(_) => {
@@ -968,12 +978,12 @@ pub mod pve_rs_sdn_fabrics {
status::get_neighbors_openfabric(fabric_id, openfabric_neighbors).map(|v| v.into())
}
FabricEntry::Ospf(fabric) => {
- let ospf_neighbors_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ip ospf neighbor json'"])
- .output()?
- .stdout,
- )?;
+ let command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show ip ospf vrf {vrf} neighbor json'"),
+ None => "vtysh -c 'show ip ospf neighbor json'".to_string(),
+ };
+ let ospf_neighbors_string =
+ String::from_utf8(Command::new("sh").args(["-c", &command]).output()?.stdout)?;
let ospf_neighbors: proxmox_frr::de::ospf::Neighbors =
if ospf_neighbors_string.is_empty() {
proxmox_frr::de::ospf::Neighbors::default()
@@ -992,10 +1002,14 @@ pub mod pve_rs_sdn_fabrics {
}
FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
FabricEntry::Bgp(_) => {
+ let command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show bgp vrf {vrf} neighbors json'"),
+ None => "vtysh -c 'show bgp neighbors json'".to_string(),
+ };
let bgp_neighbors_string = String::from_utf8(
Command::new("sh")
.env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show bgp neighbors json'"])
+ .args(["-c", &command])
.output()?
.stdout,
)?;
@@ -1025,6 +1039,7 @@ pub mod pve_rs_sdn_fabrics {
};
let fabric = config.get_fabric(&fabric_id)?;
+ let vrf = fabric_vrf_name(fabric);
match fabric {
FabricEntry::Openfabric(_) => {
@@ -1046,12 +1061,12 @@ pub mod pve_rs_sdn_fabrics {
.map(|v| v.into())
}
FabricEntry::Ospf(fabric) => {
- let ospf_interfaces_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ip ospf interface json'"])
- .output()?
- .stdout,
- )?;
+ let command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show ip ospf vrf {vrf} interface json'"),
+ None => "vtysh -c 'show ip ospf interface json'".to_string(),
+ };
+ let ospf_interfaces_string =
+ String::from_utf8(Command::new("sh").args(["-c", &command]).output()?.stdout)?;
let ospf_interfaces: proxmox_frr::de::ospf::Interfaces =
if ospf_interfaces_string.is_empty() {
proxmox_frr::de::ospf::Interfaces::default()
@@ -1070,10 +1085,14 @@ pub mod pve_rs_sdn_fabrics {
}
FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())),
FabricEntry::Bgp(_) => {
+ let command = match &vrf {
+ Some(vrf) => format!("vtysh -c 'show bgp vrf {vrf} neighbors json'"),
+ None => "vtysh -c 'show bgp neighbors json'".to_string(),
+ };
let bgp_neighbors_string = String::from_utf8(
Command::new("sh")
.env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show bgp neighbors json'"])
+ .args(["-c", &command])
.output()?
.stdout,
)?;
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH proxmox-perl-rs 08/15] pve-rs: fabrics: include VRF routes in aggregate status
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (6 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones Gabriel Goller
` (6 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Collect OSPF and BGP routes from every VRF used by a fabric on the
local node, in addition to routes from the default routing table. Run
the related commands in one vtysh process and merge their JSON output,
including entries which share the same prefix.
Match routes by VRF when deciding whether a fabric has active routes.
This prevents routes from another routing table from affecting its
status. Add a parser test for duplicate prefixes across VRFs.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 169 +++++++++++++++++------------
pve-rs/src/sdn/status.rs | 19 ++--
2 files changed, 113 insertions(+), 75 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 0fb1ccec9b7c..b714b83951a8 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -5,7 +5,7 @@ pub mod pve_rs_sdn_fabrics {
//! This provides the configuration for the SDN fabrics, as well as helper methods for reading
//! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
- use std::collections::{BTreeMap, HashMap, HashSet};
+ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt::Write;
use std::net::IpAddr;
use std::ops::Deref;
@@ -1109,6 +1109,68 @@ pub mod pve_rs_sdn_fabrics {
}
}
+ fn extend_routes(destination: &mut proxmox_frr::de::Routes, source: proxmox_frr::de::Routes) {
+ for (prefix, routes) in source.0 {
+ destination.0.entry(prefix).or_default().extend(routes);
+ }
+ }
+
+ /// Query and merge routes from multiple FRR commands in a single `vtysh` process.
+ fn query_routes(
+ commands: impl IntoIterator<Item = String>,
+ protocol: &str,
+ ) -> Result<proxmox_frr::de::Routes, Error> {
+ let mut command = Command::new("vtysh");
+ command.env("VTYSH_HISTFILE", "/dev/null");
+ for query in commands {
+ command.args(["-c", &query]);
+ }
+
+ let output = command.output()?;
+ if !output.status.success() {
+ anyhow::bail!(
+ "error querying {protocol} routes: {}",
+ String::from_utf8_lossy(&output.stderr).trim()
+ );
+ }
+
+ parse_routes(&output.stdout, protocol)
+ }
+
+ fn parse_routes(raw: &[u8], protocol: &str) -> Result<proxmox_frr::de::Routes, Error> {
+ let mut routes = proxmox_frr::de::Routes::default();
+ let route_stream =
+ serde_json::Deserializer::from_slice(raw).into_iter::<proxmox_frr::de::Routes>();
+ for queried_routes in route_stream {
+ extend_routes(
+ &mut routes,
+ queried_routes.with_context(|| format!("error parsing {protocol} routes"))?,
+ );
+ }
+
+ Ok(routes)
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::parse_routes;
+
+ #[test]
+ fn parses_batched_routes_with_duplicate_prefixes() {
+ let raw = br#"
+ {"10.0.0.0/24":[{"nexthops":[],"metric":10,"protocol":"bgp","vrfName":"vrf_a"}]}
+ {"10.0.0.0/24":[{"nexthops":[],"metric":10,"protocol":"bgp","vrfName":"vrf_b"}]}
+ "#;
+
+ let routes = parse_routes(raw, "BGP").expect("batched routes should parse");
+ let routes = routes.0.values().next().expect("route should exist");
+
+ assert_eq!(routes.len(), 2);
+ assert_eq!(routes[0].vrf_name, "vrf_a");
+ assert_eq!(routes[1].vrf_name, "vrf_b");
+ }
+ }
+
/// Return the status of all fabrics on this node.
///
/// Go through all fabrics in the config, then filter out the ones that exist on this node.
@@ -1124,76 +1186,47 @@ pub mod pve_rs_sdn_fabrics {
return Ok(HashMap::new());
};
- let openfabric_ipv4_routes_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ip route openfabric json'"])
- .output()?
- .stdout,
- )?;
-
- let openfabric_ipv6_routes_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ipv6 route openfabric json'"])
- .output()?
- .stdout,
- )?;
-
- let ospf_routes_string = String::from_utf8(
- Command::new("sh")
- .args(["-c", "vtysh -c 'show ip route ospf json'"])
- .output()?
- .stdout,
- )?;
-
- let mut openfabric_routes: proxmox_frr::de::Routes =
- if openfabric_ipv4_routes_string.is_empty() {
- proxmox_frr::de::Routes::default()
- } else {
- serde_json::from_str(&openfabric_ipv4_routes_string)
- .with_context(|| "error parsing openfabric ipv4 routes")?
- };
- if !openfabric_ipv6_routes_string.is_empty() {
- let openfabric_ipv6_routes: proxmox_frr::de::Routes =
- serde_json::from_str(&openfabric_ipv6_routes_string)
- .with_context(|| "error parsing openfabric ipv6 routes")?;
- openfabric_routes.0.extend(openfabric_ipv6_routes.0);
- }
-
- let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() {
- proxmox_frr::de::Routes::default()
- } else {
- serde_json::from_str(&ospf_routes_string)
- .with_context(|| "error parsing ospf routes")?
- };
-
- let bgp_ipv4_routes_string = String::from_utf8(
- Command::new("sh")
- .env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show ip route bgp json'"])
- .output()?
- .stdout,
- )?;
-
- let bgp_ipv6_routes_string = String::from_utf8(
- Command::new("sh")
- .env("VTYSH_HISTFILE", "/dev/null")
- .args(["-c", "vtysh -c 'show ipv6 route bgp json'"])
- .output()?
- .stdout,
+ let openfabric_routes = query_routes(
+ [
+ "show ip route openfabric json".to_string(),
+ "show ipv6 route openfabric json".to_string(),
+ ],
+ "OpenFabric",
)?;
- let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() {
- proxmox_frr::de::Routes::default()
- } else {
- serde_json::from_str(&bgp_ipv4_routes_string)
- .with_context(|| "error parsing bgp ipv4 routes")?
- };
- if !bgp_ipv6_routes_string.is_empty() {
- let bgp_ipv6_routes: proxmox_frr::de::Routes =
- serde_json::from_str(&bgp_ipv6_routes_string)
- .with_context(|| "error parsing bgp ipv6 routes")?;
- bgp_routes.0.extend(bgp_ipv6_routes.0);
+ let node_id = NodeId::from_string(proxmox_sys::nodename().to_string())?;
+ let ospf_vrfs: BTreeSet<_> = config
+ .values()
+ .filter(|entry| {
+ matches!(entry, FabricEntry::Ospf(_)) && entry.get_node(&node_id).is_ok()
+ })
+ .filter_map(fabric_vrf_name)
+ .collect();
+ let mut ospf_commands = vec!["show ip route ospf json".to_string()];
+ ospf_commands.extend(
+ ospf_vrfs
+ .iter()
+ .map(|vrf| format!("show ip route vrf {vrf} ospf json")),
+ );
+ let ospf_routes = query_routes(ospf_commands, "OSPF")?;
+
+ let bgp_vrfs: BTreeSet<_> = config
+ .values()
+ .filter(|entry| {
+ matches!(entry, FabricEntry::Bgp(_)) && entry.get_node(&node_id).is_ok()
+ })
+ .filter_map(fabric_vrf_name)
+ .collect();
+ let mut bgp_commands = vec![
+ "show ip route bgp json".to_string(),
+ "show ipv6 route bgp json".to_string(),
+ ];
+ for vrf in bgp_vrfs {
+ bgp_commands.extend(
+ ["ip", "ipv6"].map(|family| format!("show {family} route vrf {vrf} bgp json")),
+ );
}
+ let bgp_routes = query_routes(bgp_commands, "BGP")?;
let route_status = status::RoutesParsed {
openfabric: openfabric_routes,
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 7a1334d20804..56a5646ded6d 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -18,6 +18,8 @@ use proxmox_ve_config::{
},
};
+use crate::bindings::pve_rs_sdn_fabrics::fabric_vrf_name;
+
// The status of a fabric interface
//
// Either up or down.
@@ -535,6 +537,8 @@ pub fn get_status(
continue;
}
let fabric_id = node.id().fabric_id();
+ let vrf_name =
+ fabric_vrf_name(config.get_fabric(fabric_id)?).unwrap_or_else(|| "default".to_string());
let (current_protocol, all_routes) = match &node {
ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
@@ -565,13 +569,14 @@ pub fn get_status(
// determine status by checking if any routes exist for our interfaces
let has_routes = all_routes.values().any(|v| {
v.iter().any(|route| {
- route.nexthops.iter().any(|nexthop| {
- if let Some(iface_name) = &nexthop.interface_name {
- interface_names.contains(iface_name.as_str())
- } else {
- false
- }
- })
+ route.vrf_name == vrf_name
+ && route.nexthops.iter().any(|nexthop| {
+ if let Some(iface_name) = &nexthop.interface_name {
+ interface_names.contains(iface_name.as_str())
+ } else {
+ false
+ }
+ })
})
});
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (7 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs Gabriel Goller
` (5 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Add a default-vrf option to simple zones. It defaults to true to keep
backwards-compat. When disabled, create one VRF device for the zone and
attach all of its VNets to it.
Generate the zone device independently of its VNets. This also creates the
VRF for an empty zone, so other users (e.g. fabrics) can rely on the
VRF being present. Add tests for zones with and without VNets.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN/Zones.pm | 6 ++++++
src/PVE/Network/SDN/Zones.pm | 19 +++++++++++++++---
src/PVE/Network/SDN/Zones/SimplePlugin.pm | 20 ++++++++++++++++++-
.../simple/vrf-empty/expected_sdn_interfaces | 5 +++++
src/test/zones/simple/vrf-empty/interfaces | 2 ++
src/test/zones/simple/vrf-empty/sdn_config | 12 +++++++++++
.../zones/simple/vrf/expected_sdn_interfaces | 12 +++++++++++
src/test/zones/simple/vrf/interfaces | 2 ++
src/test/zones/simple/vrf/sdn_config | 17 ++++++++++++++++
9 files changed, 91 insertions(+), 4 deletions(-)
create mode 100644 src/test/zones/simple/vrf-empty/expected_sdn_interfaces
create mode 100644 src/test/zones/simple/vrf-empty/interfaces
create mode 100644 src/test/zones/simple/vrf-empty/sdn_config
create mode 100644 src/test/zones/simple/vrf/expected_sdn_interfaces
create mode 100644 src/test/zones/simple/vrf/interfaces
create mode 100644 src/test/zones/simple/vrf/sdn_config
diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index b897cbdff973..95192497fb9e 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -94,6 +94,12 @@ my $ZONE_PROPERTIES = {
optional => 1,
description => 'Name of DHCP server backend for this zone.',
},
+ 'default-vrf' => {
+ type => 'boolean',
+ default => 1,
+ optional => 1,
+ description => 'Create a simple zone in the default VRF.',
+ },
'rt-import' => {
type => 'string',
optional => 1,
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index f66830324766..893ec436d536 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -116,14 +116,27 @@ sub generate_etc_network_config {
my $controller_cfg = $cfg->{controllers};
return if !$vnet_cfg && !$zone_cfg;
+ my $vnet_ids = $vnet_cfg ? ($vnet_cfg->{ids} // {}) : {};
+ my $zone_ids = $zone_cfg ? ($zone_cfg->{ids} // {}) : {};
+
my $interfaces_config = PVE::INotify::read_file('interfaces');
#generate configuration
my $config = {};
my $nodename = PVE::INotify::nodename();
- for my $id (sort keys %{ $vnet_cfg->{ids} }) {
- my $vnet = $vnet_cfg->{ids}->{$id};
+ for my $zone_id (sort keys %$zone_ids) {
+ my $plugin_config = $zone_ids->{$zone_id};
+ next if $plugin_config->{type} ne 'simple';
+ next if defined($plugin_config->{nodes}) && !$plugin_config->{nodes}->{$nodename};
+
+ PVE::Network::SDN::Zones::SimplePlugin->generate_zone_config(
+ $plugin_config, $zone_id, $config,
+ );
+ }
+
+ for my $id (sort keys %$vnet_ids) {
+ my $vnet = $vnet_ids->{$id};
my $zone = $vnet->{zone};
if (!$zone) {
@@ -131,7 +144,7 @@ sub generate_etc_network_config {
next;
}
- my $plugin_config = $zone_cfg->{ids}->{$zone};
+ my $plugin_config = $zone_ids->{$zone};
if (!defined($plugin_config)) {
warn "can't generate vnet '$id': zone $zone don't exist\n";
diff --git a/src/PVE/Network/SDN/Zones/SimplePlugin.pm b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
index 347eee9a46ba..681d4396d4e4 100644
--- a/src/PVE/Network/SDN/Zones/SimplePlugin.pm
+++ b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
@@ -35,6 +35,11 @@ sub properties {
type => 'string',
enum => PVE::Network::SDN::Dhcp->plugin_types(),
},
+ 'default-vrf' => {
+ description => 'Create the zone in the default VRF.',
+ type => 'boolean',
+ default => 1,
+ },
};
}
@@ -47,15 +52,27 @@ sub options {
dnszone => { optional => 1 },
ipam => { optional => 1 },
dhcp => { optional => 1 },
+ 'default-vrf' => { optional => 1 },
};
}
+sub generate_zone_config {
+ my ($class, $plugin_config, $zone_id, $config) = @_;
+
+ if (!($plugin_config->{'default-vrf'} // 1)) {
+ my $vrf_iface = "vrf_$zone_id";
+ $config->{$vrf_iface} = ['vrf-table auto'] if !defined($config->{$vrf_iface});
+ }
+
+ return $config;
+}
+
# Plugin implementation
sub generate_sdn_config {
my (
$class,
$plugin_config,
- $zoneid,
+ $zone_id,
$vnetid,
$vnet,
$controller,
@@ -150,6 +167,7 @@ sub generate_sdn_config {
push @iface_config, "alias $alias" if $alias;
push @iface_config, "ip-forward on" if $enable_forward_v4;
push @iface_config, "ip6-forward on" if $enable_forward_v6;
+ push @iface_config, "vrf vrf_$zone_id" if !($plugin_config->{'default-vrf'} // 1);
push @{ $config->{$vnetid} }, @iface_config;
diff --git a/src/test/zones/simple/vrf-empty/expected_sdn_interfaces b/src/test/zones/simple/vrf-empty/expected_sdn_interfaces
new file mode 100644
index 000000000000..27150761a60d
--- /dev/null
+++ b/src/test/zones/simple/vrf-empty/expected_sdn_interfaces
@@ -0,0 +1,5 @@
+#version:1
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
diff --git a/src/test/zones/simple/vrf-empty/interfaces b/src/test/zones/simple/vrf-empty/interfaces
new file mode 100644
index 000000000000..f1bd92ed2b43
--- /dev/null
+++ b/src/test/zones/simple/vrf-empty/interfaces
@@ -0,0 +1,2 @@
+auto lo
+iface lo inet loopback
diff --git a/src/test/zones/simple/vrf-empty/sdn_config b/src/test/zones/simple/vrf-empty/sdn_config
new file mode 100644
index 000000000000..663c251349e3
--- /dev/null
+++ b/src/test/zones/simple/vrf-empty/sdn_config
@@ -0,0 +1,12 @@
+{
+ version => 1,
+ vnets => { ids => {} },
+ zones => {
+ ids => {
+ myzone => {
+ type => "simple",
+ "default-vrf" => 0,
+ },
+ },
+ },
+}
diff --git a/src/test/zones/simple/vrf/expected_sdn_interfaces b/src/test/zones/simple/vrf/expected_sdn_interfaces
new file mode 100644
index 000000000000..3cb20f15f133
--- /dev/null
+++ b/src/test/zones/simple/vrf/expected_sdn_interfaces
@@ -0,0 +1,12 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ bridge_ports none
+ bridge_stp off
+ bridge_fd 0
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
diff --git a/src/test/zones/simple/vrf/interfaces b/src/test/zones/simple/vrf/interfaces
new file mode 100644
index 000000000000..f1bd92ed2b43
--- /dev/null
+++ b/src/test/zones/simple/vrf/interfaces
@@ -0,0 +1,2 @@
+auto lo
+iface lo inet loopback
diff --git a/src/test/zones/simple/vrf/sdn_config b/src/test/zones/simple/vrf/sdn_config
new file mode 100644
index 000000000000..de7fc5c8596c
--- /dev/null
+++ b/src/test/zones/simple/vrf/sdn_config
@@ -0,0 +1,17 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { type => "vnet", zone => "myzone" },
+ },
+ },
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "simple",
+ "default-vrf" => 0,
+ },
+ },
+ },
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (8 preceding siblings ...)
2026-08-21 14:03 ` [PATCH pve-network 09/15] sdn: add optional VRFs for simple zones Gabriel Goller
@ 2026-08-21 14:03 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 11/15] sdn: always generate EVPN " Gabriel Goller
` (4 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Let BGP and OSPF fabrics run inside the dedicated VRF of a simple zone.
Restrict this to zones with default-vrf disabled, since a fabric without
a zone already uses the default VRFF.
Validate that every fabric node is enabled for the selected zone (the
zone is not automatically on the whole cluster anymore). Repeat the
checks when fabrics, fabric nodes, or zones change, and prevent deleting
a zone while a fabric still uses it. This avoids scenarios where a
fabric node exists on a node where the VRF (zone) doesn't.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 48 +++++++++++++++----
.../API2/Network/SDN/Fabrics/FabricNode.pm | 18 +++++++
src/PVE/API2/Network/SDN/Zones.pm | 27 +++++++++++
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 2 +-
src/PVE/Network/SDN/Fabrics.pm | 29 ++++++++++-
5 files changed, 114 insertions(+), 10 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index 4695a6ee7d71..e381dc2a08fb 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -6,13 +6,51 @@ use warnings;
use PVE::Network::SDN;
use PVE::Network::SDN::Controllers;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::Zones;
+use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+sub validate_fabric_zone {
+ my ($fabric, $config) = @_;
+
+ my $protocol = $fabric->{protocol} // '';
+ if ($protocol eq 'bgp' && !$fabric->{zone}) {
+ my $controller_cfg = PVE::Network::SDN::Controllers::config();
+ for my $controller_id (keys %{ $controller_cfg->{ids} // {} }) {
+ my $controller = $controller_cfg->{ids}->{$controller_id};
+ if ($controller->{type} eq 'bgp') {
+ die "cannot configure a BGP fabric while BGP controller '$controller_id' exists:"
+ . " both target the default-VRF BGP router\n";
+ }
+ }
+ }
+
+ my $zone_id = $fabric->{zone};
+ return if !$zone_id;
+
+ raise_param_exc({ zone => 'VRF zones are only supported for BGP and OSPF fabrics' })
+ if $protocol !~ /^(?:bgp|ospf)$/;
+
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id);
+
+ raise_param_exc({ zone => "zone '$zone_id' does not exist" }) if !$zone;
+ raise_param_exc({ zone => "zone '$zone_id' is not a simple zone" })
+ if $zone->{type} ne 'simple';
+ raise_param_exc({ zone => "zone '$zone_id' is configured to use the default VRF" })
+ if $zone->{'default-vrf'} // 1;
+
+ if ($config) {
+ PVE::Network::SDN::Fabrics::assert_fabric_nodes_in_zone(
+ $config, $fabric->{id}, $zone_id, $zone,
+ );
+ }
+}
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -148,14 +186,7 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
- if (($param->{protocol} // '') eq 'bgp') {
- my $controller_cfg = PVE::Network::SDN::Controllers::config();
- for my $id (keys %{ $controller_cfg->{ids} // {} }) {
- die "cannot add a BGP fabric while BGP controller '$id' exists:"
- . " both target the default-VRF BGP router\n"
- if $controller_cfg->{ids}->{$id}->{type} eq 'bgp';
- }
- }
+ validate_fabric_zone($param);
$config->add_fabric($param);
PVE::Network::SDN::Fabrics::write_config($config);
@@ -195,6 +226,7 @@ __PACKAGE__->register_method({
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
$config->update_fabric($id, $param);
+ validate_fabric_zone($config->get_fabric($id), $config);
PVE::Network::SDN::Fabrics::write_config($config);
},
"updating fabric failed",
diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
index 7a148b550f7d..eb3b429075e5 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
@@ -9,6 +9,7 @@ use PVE::Tools qw(extract_param run_command);
use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
use PVE::Network::SDN::WireGuard;
+use PVE::Network::SDN::Zones;
use PVE::RS::SDN::Fabrics;
use PVE::RESTHandler;
@@ -138,6 +139,21 @@ my sub is_internal_wireguard_node {
return $node->{protocol} eq 'wireguard' && $node->{role} eq 'internal';
}
+my sub validate_node_fabric_zone {
+ my ($config, $fabric_id, $node_id) = @_;
+
+ my $fabric = $config->get_fabric($fabric_id);
+ my $zone_id = $fabric->{zone};
+ return if !$zone_id;
+
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id);
+
+ die "zone '$zone_id' does not exist\n" if !$zone;
+ PVE::Network::SDN::Fabrics::assert_fabric_node_in_zone(
+ $fabric_id, $node_id, $zone_id, $zone,
+ );
+}
+
__PACKAGE__->register_method({
name => 'add_node',
path => '',
@@ -169,6 +185,8 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+ validate_node_fabric_zone($config, $param->{fabric_id}, $param->{node_id});
+
if (is_internal_wireguard_node($param) && $param->{interfaces}) {
my $private_keys = PVE::Network::SDN::WireGuard::private_keys();
diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 95192497fb9e..d9cb1f41ec34 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -13,6 +13,7 @@ use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::Network::SDN::Dns;
+use PVE::Network::SDN::Fabrics;
use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN;
@@ -396,6 +397,13 @@ sub create_etc_interfaces_sdn_dir {
mkdir("/etc/pve/sdn");
}
+sub fabrics_using_zone {
+ my ($zone_id, $fabric_config) = @_;
+
+ my $fabrics = $fabric_config->list_fabrics();
+ return grep { ($fabrics->{$_}->{zone} // '') eq $zone_id } sort keys $fabrics->%*;
+}
+
__PACKAGE__->register_method({
name => 'create',
protected => 1,
@@ -517,6 +525,21 @@ __PACKAGE__->register_method({
$scfg->{$_} = $opts->{$_} for keys $opts->%*;
+ my $fabric_config = PVE::Network::SDN::Fabrics::config();
+ my @fabric_ids = fabrics_using_zone($id, $fabric_config);
+
+ if (@fabric_ids && $scfg->{type} eq 'simple' && ($scfg->{'default-vrf'} // 1)) {
+ raise_param_exc({
+ 'default-vrf' => "zone is still used as VRF by fabric '$fabric_ids[0]'",
+ });
+ }
+
+ for my $fabric_id (@fabric_ids) {
+ PVE::Network::SDN::Fabrics::assert_fabric_nodes_in_zone(
+ $fabric_config, $fabric_id, $id, $scfg,
+ );
+ }
+
my $new_ipam = $scfg->{ipam};
if (!$new_ipam != !$old_ipam || (($new_ipam // '') ne ($old_ipam // ''))) {
# don't allow ipam change if subnet are defined for now, need to implement resync ipam content
@@ -598,6 +621,10 @@ __PACKAGE__->register_method({
my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($scfg->{type});
my $vnet_cfg = PVE::Network::SDN::Vnets::config();
+ my $fabric_config = PVE::Network::SDN::Fabrics::config();
+ my @fabric_ids = fabrics_using_zone($id, $fabric_config);
+ die "zone is still used as VRF by fabric '$fabric_ids[0]'\n" if @fabric_ids;
+
$plugin->on_delete_hook($id, $vnet_cfg);
delete $cfg->{ids}->{$id};
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index ea2ba5a764ef..3d1593e7926a 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -207,7 +207,7 @@ sub on_update_hook {
for my $id (keys %$fabrics) {
die "cannot configure a BGP controller while BGP fabric '$id' exists:"
. " both target the default-VRF BGP router\n"
- if $fabrics->{$id}->{protocol} eq 'bgp';
+ if $fabrics->{$id}->{protocol} eq 'bgp' && !$fabrics->{$id}->{zone};
}
my $controller = $controller_cfg->{ids}->{$controllerid};
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 4f842f1013ef..573cb0b1d197 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -192,6 +192,23 @@ sub write_config {
cfs_write_file("sdn/fabrics.cfg", $config->to_raw(), 1);
}
+sub assert_fabric_node_in_zone {
+ my ($fabric_id, $node_id, $zone_id, $zone) = @_;
+
+ return if !defined($zone->{nodes}) || $zone->{nodes}->{$node_id};
+
+ die "fabric '$fabric_id' contains node '$node_id', which is not enabled for zone '$zone_id'\n";
+}
+
+sub assert_fabric_nodes_in_zone {
+ my ($config, $fabric_id, $zone_id, $zone) = @_;
+
+ my $nodes = $config->list_nodes_fabric($fabric_id);
+ for my $node_id (sort keys $nodes->%*) {
+ assert_fabric_node_in_zone($fabric_id, $node_id, $zone_id, $zone);
+ }
+}
+
sub get_frr_daemon_status {
my ($fabric_config) = @_;
@@ -469,6 +486,15 @@ sub fabric_properties {
protocol => get_standard_option('pve-sdn-fabric-protocol'),
digest => get_standard_option('pve-config-digest'),
'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ zone => get_standard_option(
+ 'pve-sdn-zone-id',
+ {
+ description => 'Simple zone whose VRF contains this fabric.',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['bgp', 'ospf'],
+ optional => 1,
+ },
+ ),
ip_prefix => {
type => 'string',
format => 'CIDR',
@@ -619,6 +645,7 @@ sub fabric_properties {
enum => [
'ip_prefix',
'ip6_prefix',
+ 'zone',
'redistribute',
'route_filter',
'route_map_in',
@@ -632,7 +659,7 @@ sub fabric_properties {
'instance-types' => ['ospf'],
items => {
type => 'string',
- enum => ['area', 'redistribute', 'route_filter'],
+ enum => ['area', 'redistribute', 'route_filter', 'zone'],
},
optional => 1,
},
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-network 11/15] sdn: always generate EVPN zone VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (9 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 12/15] api: sdn: allow EVPN zones as fabric VRFs Gabriel Goller
` (3 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Move per-zone interface generation into the zone plugin API and use it for
EVPN zones. This creates the EVPN VRF even when the zone has no VNets
-- previously the VRF was only created when at least one VNet existed on
the zone.
Keep the unreachable default route handling on the VRF device. Exit
nodes remove that route, while other nodes install it. Add a test for an
empty EVPN zone.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/Network/SDN/Zones.pm | 6 ++--
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 35 +++++++++++--------
src/PVE/Network/SDN/Zones/Plugin.pm | 6 ++++
.../evpn/vrf-empty/expected_sdn_interfaces | 6 ++++
src/test/zones/evpn/vrf-empty/interfaces | 2 ++
src/test/zones/evpn/vrf-empty/sdn_config | 13 +++++++
6 files changed, 50 insertions(+), 18 deletions(-)
create mode 100644 src/test/zones/evpn/vrf-empty/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/vrf-empty/interfaces
create mode 100644 src/test/zones/evpn/vrf-empty/sdn_config
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index 893ec436d536..ad23cee4c292 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -127,12 +127,10 @@ sub generate_etc_network_config {
for my $zone_id (sort keys %$zone_ids) {
my $plugin_config = $zone_ids->{$zone_id};
- next if $plugin_config->{type} ne 'simple';
next if defined($plugin_config->{nodes}) && !$plugin_config->{nodes}->{$nodename};
- PVE::Network::SDN::Zones::SimplePlugin->generate_zone_config(
- $plugin_config, $zone_id, $config,
- );
+ my $plugin = PVE::Network::SDN::Zones::Plugin->lookup($plugin_config->{type});
+ $plugin->generate_zone_config($plugin_config, $zone_id, $config);
}
for my $id (sort keys %$vnet_ids) {
diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 0e79707c3746..4c019071ecce 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -129,6 +129,25 @@ sub options {
}
# Plugin implementation
+sub generate_zone_config {
+ my ($class, $plugin_config, $zone_id, $config) = @_;
+
+ my $vrf_iface = "vrf_$zone_id";
+ my $local_node = PVE::INotify::nodename();
+ my $is_evpn_gateway =
+ $plugin_config->{exitnodes} && $plugin_config->{exitnodes}->{$local_node};
+ my $route_action = $is_evpn_gateway ? 'del' : 'add';
+
+ if (!defined($config->{$vrf_iface})) {
+ $config->{$vrf_iface} = [
+ 'vrf-table auto',
+ "post-up ip route $route_action vrf $vrf_iface unreachable default metric 4278198272",
+ ];
+ }
+
+ return $config;
+}
+
sub generate_sdn_config {
my (
$class,
@@ -209,7 +228,8 @@ sub generate_sdn_config {
die "neither fabric nor peers configured for EVPN controller $controller->{id}";
}
- my $is_evpn_gateway = $plugin_config->{'exitnodes'}->{$local_node};
+ my $is_evpn_gateway =
+ $plugin_config->{exitnodes} && $plugin_config->{exitnodes}->{$local_node};
my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'};
my $mtu = 1450;
@@ -308,19 +328,6 @@ sub generate_sdn_config {
push(@{ $config->{$vnetid} }, @iface_config) if !$config->{$vnetid};
if ($vrf_iface) {
- #vrf interface
- @iface_config = ();
- push @iface_config, "vrf-table auto";
- if (!$is_evpn_gateway) {
- push @iface_config,
- "post-up ip route add vrf $vrf_iface unreachable default metric 4278198272";
- } else {
- push @iface_config,
- "post-up ip route del vrf $vrf_iface unreachable default metric 4278198272";
- }
-
- push(@{ $config->{$vrf_iface} }, @iface_config) if !$config->{$vrf_iface};
-
if ($vrfvxlan) {
#l3vni vxlan interface
my $iface_vrf_vxlan = "vrfvx_$zoneid";
diff --git a/src/PVE/Network/SDN/Zones/Plugin.pm b/src/PVE/Network/SDN/Zones/Plugin.pm
index 74a3384cd7ae..5a752a0545f6 100644
--- a/src/PVE/Network/SDN/Zones/Plugin.pm
+++ b/src/PVE/Network/SDN/Zones/Plugin.pm
@@ -100,6 +100,12 @@ sub encode_value {
return $value;
}
+sub generate_zone_config {
+ my ($class, $plugin_config, $zone_id, $config) = @_;
+
+ return $config;
+}
+
sub generate_sdn_config {
my (
$class,
diff --git a/src/test/zones/evpn/vrf-empty/expected_sdn_interfaces b/src/test/zones/evpn/vrf-empty/expected_sdn_interfaces
new file mode 100644
index 000000000000..f78a7d21782c
--- /dev/null
+++ b/src/test/zones/evpn/vrf-empty/expected_sdn_interfaces
@@ -0,0 +1,6 @@
+#version:1
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
diff --git a/src/test/zones/evpn/vrf-empty/interfaces b/src/test/zones/evpn/vrf-empty/interfaces
new file mode 100644
index 000000000000..f1bd92ed2b43
--- /dev/null
+++ b/src/test/zones/evpn/vrf-empty/interfaces
@@ -0,0 +1,2 @@
+auto lo
+iface lo inet loopback
diff --git a/src/test/zones/evpn/vrf-empty/sdn_config b/src/test/zones/evpn/vrf-empty/sdn_config
new file mode 100644
index 000000000000..00c1c724b95e
--- /dev/null
+++ b/src/test/zones/evpn/vrf-empty/sdn_config
@@ -0,0 +1,13 @@
+{
+ version => 1,
+ vnets => { ids => {} },
+ zones => {
+ ids => {
+ myzone => {
+ type => "evpn",
+ controller => "evpnctl",
+ "vrf-vxlan" => 1000,
+ },
+ },
+ },
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-network 12/15] api: sdn: allow EVPN zones as fabric VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (10 preceding siblings ...)
2026-08-21 14:03 ` [PATCH pve-network 11/15] sdn: always generate EVPN " Gabriel Goller
@ 2026-08-21 14:03 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-network 13/15] api: sdn: disallow BGP fabrics in EVPN zone VRFs Gabriel Goller
` (2 subsequent siblings)
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Accept EVPN zones as the VRF provider for BGP and OSPF fabrics. EVPN zones
always have a dedicated VRF, so only simple zones need the default-vrf check.
Use a generic validation error and API description now that fabric VRFs are
not limited to simple zones.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 6 +++---
src/PVE/Network/SDN/Fabrics.pm | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index e381dc2a08fb..2ef55259ede3 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -39,10 +39,10 @@ sub validate_fabric_zone {
my $zone = PVE::Network::SDN::Zones::get_zone($zone_id);
raise_param_exc({ zone => "zone '$zone_id' does not exist" }) if !$zone;
- raise_param_exc({ zone => "zone '$zone_id' is not a simple zone" })
- if $zone->{type} ne 'simple';
+ raise_param_exc({ zone => "zone '$zone_id' does not provide a dedicated VRF" })
+ if $zone->{type} ne 'simple' && $zone->{type} ne 'evpn';
raise_param_exc({ zone => "zone '$zone_id' is configured to use the default VRF" })
- if $zone->{'default-vrf'} // 1;
+ if $zone->{type} eq 'simple' && ($zone->{'default-vrf'} // 1);
if ($config) {
PVE::Network::SDN::Fabrics::assert_fabric_nodes_in_zone(
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 573cb0b1d197..e9419d718dbb 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -489,7 +489,7 @@ sub fabric_properties {
zone => get_standard_option(
'pve-sdn-zone-id',
{
- description => 'Simple zone whose VRF contains this fabric.',
+ description => 'SDN zone whose VRF contains this fabric.',
'type-property' => 'protocol',
'instance-types' => ['bgp', 'ospf'],
optional => 1,
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-network 13/15] api: sdn: disallow BGP fabrics in EVPN zone VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (11 preceding siblings ...)
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 ` 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
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
EVPN zones already configure a BGP router in their dedicated VRF. Reject BGP
fabrics using the same VRF to avoid conflicting FRR BGP instances.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index 2ef55259ede3..e3156b89df24 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -41,6 +41,8 @@ sub validate_fabric_zone {
raise_param_exc({ zone => "zone '$zone_id' does not exist" }) if !$zone;
raise_param_exc({ zone => "zone '$zone_id' does not provide a dedicated VRF" })
if $zone->{type} ne 'simple' && $zone->{type} ne 'evpn';
+ raise_param_exc({ zone => "BGP fabrics cannot use an EVPN zone VRF" })
+ if $protocol eq 'bgp' && $zone->{type} eq 'evpn';
raise_param_exc({ zone => "zone '$zone_id' is configured to use the default VRF" })
if $zone->{type} eq 'simple' && ($zone->{'default-vrf'} // 1);
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-manager 14/15] ui: sdn: add VRF zone selection for fabrics
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (12 preceding siblings ...)
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 ` Gabriel Goller
2026-08-21 14:03 ` [PATCH pve-manager 15/15] ui: sdn: expose EVPN zones as fabric VRFs Gabriel Goller
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
Allow BGP and OSPF fabrics to use the VRF created by a simple zone. The
selector ignores simple zones that use the default VRF, because they do
not create separate VRF and thus don't isolate the fabrics. Leaving the
field empty keeps the fabric in the default VRF.
Add the default VRF option to the simple zone editor so users can control
whether a zone creates its own VRF.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/form/SDNZoneSelector.js | 12 +++++++++++-
www/manager6/sdn/fabrics/FabricEdit.js | 18 ++++++++++++++++++
www/manager6/sdn/zones/SimpleEdit.js | 12 +++++++++++-
3 files changed, 40 insertions(+), 2 deletions(-)
diff --git a/www/manager6/form/SDNZoneSelector.js b/www/manager6/form/SDNZoneSelector.js
index d8a63baeb7bd..4813539cb62a 100644
--- a/www/manager6/form/SDNZoneSelector.js
+++ b/www/manager6/form/SDNZoneSelector.js
@@ -8,6 +8,9 @@ Ext.define(
valueField: 'zone',
displayField: 'zone',
+ // Only show simple zones which create a dedicated VRF.
+ vrfOnly: false,
+
initComponent: function () {
var me = this;
@@ -17,6 +20,13 @@ Ext.define(
property: 'zone',
direction: 'ASC',
},
+ filters: me.vrfOnly
+ ? [
+ function (record) {
+ return record.get('type') === 'simple' && !record.get('default-vrf');
+ },
+ ]
+ : [],
});
Ext.apply(me, {
@@ -42,7 +52,7 @@ Ext.define(
function () {
Ext.define('pve-sdn-zone', {
extend: 'Ext.data.Model',
- fields: ['zone', 'type'],
+ fields: ['zone', 'type', { name: 'default-vrf', type: 'boolean', defaultValue: true }],
proxy: {
type: 'proxmox',
url: '/api2/json/cluster/sdn/zones',
diff --git a/www/manager6/sdn/fabrics/FabricEdit.js b/www/manager6/sdn/fabrics/FabricEdit.js
index e9e0d1fa6286..b3992b294b93 100644
--- a/www/manager6/sdn/fabrics/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/FabricEdit.js
@@ -37,6 +37,19 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
disabled: '{!isCreate}',
},
},
+ {
+ xtype: 'pveSDNZoneSelector',
+ fieldLabel: gettext('VRF Zone'),
+ labelWidth: 120,
+ name: 'zone',
+ vrfOnly: true,
+ allowBlank: true,
+ skipEmptyText: true,
+ emptyText: gettext('Default VRF'),
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
],
additionalItems: [],
@@ -50,6 +63,11 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
me.autoLoad = !me.isCreate;
me.method = me.isCreate ? 'POST' : 'PUT';
+ let supportsVrf = ['bgp', 'ospf'].includes(me.extraRequestParams.protocol);
+ let vrfZoneField = me.items.find((item) => item.name === 'zone');
+ vrfZoneField.hidden = !supportsVrf;
+ vrfZoneField.disabled = !supportsVrf;
+
if (!me.isCreate) {
me.url = `${me.baseUrl}/${me.fabricId}`;
} else {
diff --git a/www/manager6/sdn/zones/SimpleEdit.js b/www/manager6/sdn/zones/SimpleEdit.js
index ba10bb3616ea..4ec77057a6e5 100644
--- a/www/manager6/sdn/zones/SimpleEdit.js
+++ b/www/manager6/sdn/zones/SimpleEdit.js
@@ -18,7 +18,17 @@ Ext.define('PVE.sdn.zones.SimpleInputPanel', {
initComponent: function () {
var me = this;
- me.items = [];
+ me.items = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'default-vrf',
+ fieldLabel: gettext('Create zone in default VRF'),
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: !me.isCreate,
+ },
+ ];
me.advancedItems = [
{
xtype: 'proxmoxcheckbox',
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH pve-manager 15/15] ui: sdn: expose EVPN zones as fabric VRFs
2026-08-21 14:03 [RFC manager/network/proxmox{-ve-rs,-perl-rs} 00/15] SDN VRF support Gabriel Goller
` (13 preceding siblings ...)
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 ` Gabriel Goller
14 siblings, 0 replies; 16+ messages in thread
From: Gabriel Goller @ 2026-08-21 14:03 UTC (permalink / raw)
To: pve-devel
EVPN zones also create a dedicated VRF and can therefore provide a
routing table for a fabric. Include them in the VRF zone selector with
simple zones that do not use the default VRF.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/form/SDNZoneSelector.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/www/manager6/form/SDNZoneSelector.js b/www/manager6/form/SDNZoneSelector.js
index 4813539cb62a..e91588e37d18 100644
--- a/www/manager6/form/SDNZoneSelector.js
+++ b/www/manager6/form/SDNZoneSelector.js
@@ -8,7 +8,7 @@ Ext.define(
valueField: 'zone',
displayField: 'zone',
- // Only show simple zones which create a dedicated VRF.
+ // Only show zones which create a dedicated VRF.
vrfOnly: false,
initComponent: function () {
@@ -23,7 +23,11 @@ Ext.define(
filters: me.vrfOnly
? [
function (record) {
- return record.get('type') === 'simple' && !record.get('default-vrf');
+ let type = record.get('type');
+ return (
+ type === 'evpn' ||
+ (type === 'simple' && !record.get('default-vrf'))
+ );
},
]
: [],
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-08-21 14:06 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH proxmox-ve-rs 02/15] sdn: add zone references to OSPF and BGP fabrics Gabriel Goller
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
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.