From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id E1EEE1FF0AA for ; Fri, 21 Aug 2026 16:06:01 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 115CA217F6; Fri, 21 Aug 2026 16:04:29 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 10/15] sdn: allow fabrics to use simple zone VRFs Date: Fri, 21 Aug 2026 16:03:54 +0200 Message-ID: <20260821140404.322081-11-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260821140404.322081-1-g.goller@proxmox.com> References: <20260821140404.322081-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787321023422 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.394 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_MAILER 2 Automated Mailer Tag Left in Email RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: S2BUW6W5KI3SUCO2GOSYRPODB3RJES55 X-Message-ID-Hash: S2BUW6W5KI3SUCO2GOSYRPODB3RJES55 X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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