[PATCH pve-network v5 30/46] evpn controller: add route_map_{in,out} parameter

Stefan Hanreich
2026-05-05T15:42:11Z

This parameter allows extending the default MAP_VTEP_{IN,OUT} route
maps by specifying a custom route map configured in route-maps.cfg.
This can be used for filtering incoming and outgoing routes, e.g. for
only advertising type-5 routes to external peers or only allow
importing routes with specific route targets.

The old default route maps are kept around in order to support the
exit nodes directive of the EVPN zone. They're still used for
filtering the default routes from other exit nodes and for setting the
metric of non-primary default routes. If a route map override is
configured, an additional call action gets inserted into the
auto-generated route map that jumps into the user-supplied route map,
after the entries handling the default routes are created.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../Network/SDN/RouteMaps/RouteMapEntry.pm | 6 ++++
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 30 ++++++++++++++++---
src/PVE/Network/SDN/Controllers/Plugin.pm | 14 +++++++++
src/PVE/Network/SDN/RouteMaps.pm | 20 +++++++++++++
4 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
index 6cede04..f53cfad 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
@@ -5,6 +5,7 @@ use warnings;

use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::RouteMaps;
use PVE::Tools qw(extract_param);

use PVE::RESTHandler;
@@ -126,6 +127,11 @@ __PACKAGE__->register_method({
my $order = extract_param($param, 'order');

$config->delete($route_map_id, $order);
+
+ my $remaining_entries = $config->list_route_map($route_map_id);
+ PVE::Network::SDN::RouteMaps::check_references($route_map_id)
+ if !$remaining_entries->%*;
+
PVE::Network::SDN::RouteMaps::write_config($config);
},
"deleting route map entry failed",
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 54a2227..f5c0bbb 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -45,6 +45,8 @@ sub options {
'asn' => { optional => 0 },
'peers' => { optional => 1 },
'fabric' => { optional => 1 },
+ 'route-map-in' => { optional => 1 },
+ 'route-map-out' => { optional => 1 },
};
}

@@ -165,11 +167,19 @@ sub generate_frr_config {

$bgp_router->{address_families}->{l2vpn_evpn}->{autort_as} = $autortas if $autortas;

- my $routemap_in = { seq => 1, action => "permit" };
- my $routemap_out = { seq => 1, action => "permit" };
+ if (!$config->{frr}->{routemaps}->{'MAP_VTEP_IN'}) {
+ my $entry = { seq => 1, action => "permit" };
+ $entry->{call} = $plugin_config->{'route-map-in'} if $plugin_config->{'route-map-in'};

- push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $routemap_in);
- push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $routemap_out);
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $entry);
+ }
+
+ if (!$config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}) {
+ my $entry = { seq => 1, action => "permit" };
+ $entry->{call} = $plugin_config->{'route-map-out'} if $plugin_config->{'route-map-out'};
+
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $entry);
+ }

return $config;
}
@@ -488,6 +498,18 @@ sub on_update_hook {
}

my $controller = $controller_cfg->{ids}->{$controllerid};
+ my $route_map_config = PVE::Network::SDN::RouteMaps::config(0);
+
+ if ($controller->{'route-map-in'}) {
+ my $entries = $route_map_config->list_route_map($controller->{'route-map-in'});
+ die "route map $controller->{'route-map-in'} does not exist!" if !$entries->%*;
+ }
+
+ if ($controller->{'route-map-out'}) {
+ my $entries = $route_map_config->list_route_map($controller->{'route-map-out'});
+ die "route map $controller->{'route-map-out'} does not exist!" if !$entries->%*;
+ }
+
if ($controller->{type} eq 'evpn') {
die "must have exactly one of peers / fabric defined"
if ($controller->{peers} && $controller->{fabric})
diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm
index 77d8f42..1068b5d 100644
--- a/src/PVE/Network/SDN/Controllers/Plugin.pm
+++ b/src/PVE/Network/SDN/Controllers/Plugin.pm
@@ -7,6 +7,8 @@ use PVE::Tools;
use PVE::JSONSchema;
use PVE::Cluster;

+use PVE::Network::SDN::RouteMaps;
+
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::SectionConfig);

@@ -40,6 +42,18 @@ my $defaultData = {
'pve-sdn-controller-id',
{ completion => \&PVE::Network::SDN::complete_sdn_controller },
),
+ 'route-map-in' => {
+ description => "Route Map that should be applied for incoming routes",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ optional => 1,
+ },
+ 'route-map-out' => {
+ description => "Route Map that should be applied for outgoing routes",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ optional => 1,
+ },
},
};

diff --git a/src/PVE/Network/SDN/RouteMaps.pm b/src/PVE/Network/SDN/RouteMaps.pm
index 9e44546..5560f18 100644
--- a/src/PVE/Network/SDN/RouteMaps.pm
+++ b/src/PVE/Network/SDN/RouteMaps.pm
@@ -107,6 +107,26 @@ sub write_config {
cfs_write_file("sdn/route-maps.cfg", $config->to_raw(), 1);
}

+sub check_references {
+ my ($route_map_id) = @_;
+
+ my $controller_config = PVE::Network::SDN::Controllers::config();
+
+ for my $controller_id (keys $controller_config->{ids}->%*) {
+ my $controller = $controller_config->{ids}->{$controller_id};
+
+ if ($controller->{'route-map-in'}) {
+ die "route map $route_map_id still referenced by controller $controller_id"
+ if $controller->{'route-map-in'} eq $route_map_id;
+ }
+
+ if ($controller->{'route-map-out'}) {
+ die "route map $route_map_id still referenced by controller $controller_id"
+ if $controller->{'route-map-out'} eq $route_map_id;
+ }
+ }
+}
+
sub route_map_properties {
my ($update) = @_;

--
2.47.3