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