* [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN
@ 2026-04-01 14:39 Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 01/34] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
` (33 more replies)
0 siblings, 34 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
## Introduction
This patch adds support for managing route maps and prefix lists to the SDN
stack. With this patch series, route maps can be applied to the BGP and EVPN
controller for incoming / outgoing route filtering. There are currently some
other features in development that would make use of route maps as well, namely:
* VRF route leaking
* Route Redistribution for Fabrics
Prefix Lists can be used for matching inside route map match statements. They
are implemented so they can be used inside route map match statements for now.
FRR also provides access lists, which provide a subset of the functionality of
prefix lists. For that reason, we decided to omit access lists for now, since
everything can be modeled with prefix lists as well.
Ran this against the e2e test suite, with all tests passing!
## Motivation
There are a lot of use-cases for enabling users to create their own route-maps,
which was currently only possible by utilizing frr.conf.local - which was clunky
and prone to issues. Route maps can be used for filtering in/outgoing routes and
modifiy them, so users could e.g. only selectively advertise some routes via BGP
or only import certain EVPN routes from outside.
It also allows us to programmatically manage route maps via the UI, e.g. for
deeper EVPN integration in PDM. This opens up a lot of possibilities for
new features.
## Configuration Format
This patch series adds two new configuration files, route-maps.cfg and
prefix-lists.cfg in /etc/pve/sdn.
### route-maps.cfg
An example route map configuration looks as follows:
route-map-entry: example_123
action permit
match key=vni,value=23487
set key=tag,value=23487
This would create the following FRR route map entry:
route-map example permit 123
match evpn vni 23487
set tag 23487
Every entry in route-maps.cfg maps to an entry in a route map. They are
identified by their name as well as their ordering number. `example_123`
specifies the 123th entry in the route map 'example'. The main reason for
choosing this format is, that having a single section for one route-map would be
quite unwieldy. It'd require some format like this, which is pretty awkward to
handle / validate:
route-map-entry: example
action permit,seq=123
match key=vni,value=23487,seq=123
set key=tag,value=23487,seq=123
>From a UI POV editing singular route map entries seems better as well, and with
the mapping of section entries to route map entries, a suitable API design
follows quite naturally and easily maps to the respective section config
entries, without too much data mangling required.
### prefix-lists.cfg
An example prefix list configuration looks as follows:
prefix-list: example-1
entries action=permit,prefix=192.0.2.0/24
entries action=permit,prefix=192.0.2.0/24,le=32
entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
This would create the following FRR prefix list:
ip prefix-list example-1 permit 192.0.2.0/24
ip prefix-list example-1 permit 192.0.2.0/24 le 32
ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
## API endpoints
This patch series introduces the following API endpoints in the /cluster/sdn
subfolder:
### Route Maps
GET /route-maps - lists all route map entries
GET /route-maps/<id> - lists all route map entries for the route map <id>
GET /route-maps/<id>/<order> - gets the order'th entry in route map <id>
POST /route-maps - creates a new route map entry
PUT /route-maps/<id>/<order> - updates the order'th entry in route map <id>
DELETE /route-maps/<id>/<order> - deletes the order'th entry in route map <id>
### Prefix Lists
GET /prefix-lists - lists all prefix lists
GET /prefix-lists/<id> - get prefix list <id>
POST /prefix-lists - create a new prefix list
PUT /prefix-lists/<id> - update prefix list <id>
DELETE /prefix-lists/<id> - delete prefix list <id>
## Open questions
How should we handle overriding the auto-generated route maps (e.g. in the EVPN
controller) and prefix lists?
Currently this patch series disallows creating any route map / prefix list that
have the same name as PVE auto-generated ones via the API. They can be
overridden by creating a new route map and then selecting it in the respective
entity (e.g. via route-map-in in the EVPN controller). Pre-defined prefix-lists
cannot currently be overridden, since this usually makes little sense, as they
are used in the auto-generated route maps, which can be overridden anyway.
This is the most restrictive option, which leaves the possibility of re-thinking
our approach depending on if this comes up in the future.
How should we handle setting custom route maps on exit nodes?
For exit nodes a special route map entry is generated that disallows importing
default routes to avoid traffic loops between exit nodes. With the current
implementation, those entries still get created and executed in order to make it
easy for users to use route maps on EVPN exit nodes. This also makes it
impossible to override this behavior, since a route map terminates with the
first matching entry. The proposed solution for this is a future patch series,
that allows defining multiple EVPN controllers and limit them to specific nodes.
Users could then manually build what we currently do on exit nodes together with
this patch series.
## Dependencies
proxmox-frr depends on proxmox-frr-templates
proxmox-frr depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-frr
proxmox-perl-rs depends on proxmox-ve-config
pve-network depends on proxmox-perl-rs
pve-network depends on pve-cluster
Changes from v1 (Thanks @Gabriel, @Hannes, @Wolfgang):
* rebase on top of current master
* fix newly introduced vtysh tests
* include missing access-control patch
* fix an error in the permission API path of GET /route-maps/{route-map-id}
* fix permission check in list route maps / prefix lists endpoint
* implement From instead of Into for section config to frr conversions
* replace core::* imports with std::*
* improve comments in both pve-rs modules
* use get() instead of iter().find() in get methods of both pve-rs modules
* use entry API when creating new entities in both pve-rs modules
* removed duplicate PrefixList implementation block
* fixed pending parameter in GET endpoints
* add route maps / prefix lists to has_pending_changes method
* fixed change detection for newly introduced fields in prefix lists / route
maps
* fixed reserved id 'loopbacks_ips' for prefix lists (instead of reserving
loopback_ips)
* properly pass delete parameter to the route map update pve-rs method
* remove additional prefix list / route map rendering methods and just use dump
instead in the ve-config FRR integration tests
* improved documentation of the FRR route map generation logic, so it better
explains *how* the configuration gets merged.
* added another test-case for EVPN zones with a controller with custom route-map
+ exit nodes
* implement exit action and call features of route maps
* jump into user-supplied route maps instead of replacing them directly, to
avoid breaking exit-node setups if users do not recreate the auto-generated
route map
* improve indentation of FRR template
* update tests to reflect changes w.r.t. FRR config generation
* improve error message on trying to GET non-existing route map entry
* move the tests from the frr module in route maps / prefix lists to
the integration tests in proxmox-ve-config
* make order u16 instead of u32, because in FRR it is an u16 as well
* add unit tests to some new types
* change route map merging logic to overwrite existing route maps, if an entry
with the same route map name exists in the section config
* added separate patch for PrefixListName::new, since the vtysh patch from
gabriel hasn't been applied yet, but this patch series requires the new
function
pve-cluster:
Stefan Hanreich (2):
cfs: add 'sdn/route-maps.cfg' to observed files
cfs: add 'sdn/prefix-lists.cfg' to observed files
src/PVE/Cluster.pm | 2 ++
src/pmxcfs/status.c | 2 ++
2 files changed, 4 insertions(+)
pve-access-control:
Stefan Hanreich (1):
permissions: add ACL path for prefix-lists and route-maps
src/PVE/AccessControl.pm | 4 ++++
1 file changed, 4 insertions(+)
proxmox-ve-rs:
Stefan Hanreich (13):
frr: add constructor to prefix list name
sdn-types: add common route-map helper types
frr: change order type to u16
frr: implement routemap match/set statements via adjacent tagging
frr: implement support for call and exit action
frr-templates: change route maps template to adapt to new frr types
ve-config: fabrics: adapt frr config generation
ve-config: add prefix list section config
ve-config: frr: implement frr config generation for prefix lists
ve-config: add route map section config
ve-config: frr: implement frr config generation for route maps
ve-config: add prefix lists integration tests
ve-config: add route maps integration tests
.../templates/route_maps.jinja | 19 +-
proxmox-frr/Cargo.toml | 2 +-
proxmox-frr/debian/control | 2 +
proxmox-frr/src/ser/route_map.rs | 108 ++-
proxmox-sdn-types/src/bgp.rs | 62 ++
proxmox-sdn-types/src/lib.rs | 179 +++++
proxmox-ve-config/src/sdn/fabric/frr.rs | 33 +-
proxmox-ve-config/src/sdn/mod.rs | 2 +
proxmox-ve-config/src/sdn/prefix_list.rs | 220 ++++++
proxmox-ve-config/src/sdn/route_map.rs | 728 ++++++++++++++++++
proxmox-ve-config/tests/prefix_lists/main.rs | 112 +++
proxmox-ve-config/tests/route_maps/main.rs | 146 ++++
12 files changed, 1561 insertions(+), 52 deletions(-)
create mode 100644 proxmox-sdn-types/src/bgp.rs
create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
create mode 100644 proxmox-ve-config/tests/prefix_lists/main.rs
create mode 100644 proxmox-ve-config/tests/route_maps/main.rs
proxmox-perl-rs:
Stefan Hanreich (3):
pve-rs: sdn: add route maps module
pve-rs: sdn: add prefix lists module
sdn: add prefix list / route maps to frr config generation helper
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 2 +
pve-rs/src/bindings/sdn/mod.rs | 30 ++-
pve-rs/src/bindings/sdn/prefix_lists.rs | 192 +++++++++++++++++
pve-rs/src/bindings/sdn/route_maps.rs | 262 ++++++++++++++++++++++++
5 files changed, 484 insertions(+), 3 deletions(-)
create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
pve-network:
Stefan Hanreich (15):
controller: bgp: evpn: adapt to new match / set frr config syntax
sdn: add prefix lists module
api2: add prefix list module
sdn: add route map module
api2: add route maps api module
api2: add route map module
api2: add route map entry module
evpn controller: add route_map_{in,out} parameter
bgp controller: allow configuring custom route maps
sdn: change detection for route maps / prefix lists
sdn: generate route map / prefix list configuration on sdn apply
tests: add simple route map test case
tests: add bgp evpn route map/prefix list testcase
tests: add route map with prefix list testcase
tests: add exit node with custom route map testcase
src/PVE/API2/Network/SDN.pm | 14 +
src/PVE/API2/Network/SDN/Makefile | 13 +-
src/PVE/API2/Network/SDN/PrefixLists.pm | 254 ++++++++++++++++++
src/PVE/API2/Network/SDN/RouteMaps.pm | 140 ++++++++++
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 9 +
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 93 +++++++
.../Network/SDN/RouteMaps/RouteMapEntry.pm | 138 ++++++++++
src/PVE/Network/SDN.pm | 30 ++-
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 24 +-
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 42 +--
src/PVE/Network/SDN/Controllers/Plugin.pm | 14 +
src/PVE/Network/SDN/Makefile | 14 +-
src/PVE/Network/SDN/PrefixLists.pm | 134 +++++++++
src/PVE/Network/SDN/RouteMaps.pm | 192 +++++++++++++
.../expected_controller_config | 80 ++++++
.../expected_sdn_interfaces | 41 +++
.../bgp_evpn_routemap_prefix_list/interfaces | 7 +
.../bgp_evpn_routemap_prefix_list/sdn_config | 86 ++++++
.../evpn/routemap/expected_controller_config | 68 +++++
.../evpn/routemap/expected_sdn_interfaces | 41 +++
src/test/zones/evpn/routemap/interfaces | 7 +
src/test/zones/evpn/routemap/sdn_config | 70 +++++
.../expected_controller_config | 101 +++++++
.../expected_sdn_interfaces | 41 +++
.../zones/evpn/routemap_exit_node/interfaces | 7 +
.../zones/evpn/routemap_exit_node/sdn_config | 71 +++++
.../expected_controller_config | 53 ++++
.../expected_sdn_interfaces | 41 +++
.../evpn/routemap_prefix_list/interfaces | 7 +
.../evpn/routemap_prefix_list/sdn_config | 58 ++++
30 files changed, 1858 insertions(+), 32 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap/interfaces
create mode 100644 src/test/zones/evpn/routemap/sdn_config
create mode 100644 src/test/zones/evpn/routemap_exit_node/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_exit_node/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_exit_node/interfaces
create mode 100644 src/test/zones/evpn/routemap_exit_node/sdn_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
Summary over all repositories:
50 files changed, 3911 insertions(+), 87 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-cluster v2 01/34] cfs: add 'sdn/route-maps.cfg' to observed files
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 02/34] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
` (32 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Used for storing route maps to be used across different SDN entities,
for now BGP / EVPN controllers.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Cluster.pm | 1 +
src/pmxcfs/status.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index c01ceaf..1d1b593 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -83,6 +83,7 @@ my $observed = {
'sdn/mac-cache.json' => 1,
'sdn/dns.cfg' => 1,
'sdn/fabrics.cfg' => 1,
+ 'sdn/route-maps.cfg' => 1,
'sdn/.running-config' => 1,
'virtual-guest/cpu-models.conf' => 1,
'virtual-guest/profiles.cfg' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index bb68445..06f7bba 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -110,6 +110,7 @@ static memdb_change_t memdb_change_array[] = {
{.path = "sdn/pve-ipam-state.json"},
{.path = "sdn/dns.cfg"},
{.path = "sdn/fabrics.cfg"},
+ {.path = "sdn/route-maps.cfg"},
{.path = "sdn/.running-config"},
{.path = "virtual-guest/cpu-models.conf"},
{.path = "virtual-guest/profiles.cfg"},
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-cluster v2 02/34] cfs: add 'sdn/prefix-lists.cfg' to observed files
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 01/34] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-access-control v2 03/34] permissions: add ACL path for prefix-lists and route-maps Stefan Hanreich
` (31 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Used for storing prefix lists to be used inside route maps.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Cluster.pm | 1 +
src/pmxcfs/status.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index 1d1b593..047732b 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -84,6 +84,7 @@ my $observed = {
'sdn/dns.cfg' => 1,
'sdn/fabrics.cfg' => 1,
'sdn/route-maps.cfg' => 1,
+ 'sdn/prefix-lists.cfg' => 1,
'sdn/.running-config' => 1,
'virtual-guest/cpu-models.conf' => 1,
'virtual-guest/profiles.cfg' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index 06f7bba..a567d8f 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -111,6 +111,7 @@ static memdb_change_t memdb_change_array[] = {
{.path = "sdn/dns.cfg"},
{.path = "sdn/fabrics.cfg"},
{.path = "sdn/route-maps.cfg"},
+ {.path = "sdn/prefix-lists.cfg"},
{.path = "sdn/.running-config"},
{.path = "virtual-guest/cpu-models.conf"},
{.path = "virtual-guest/profiles.cfg"},
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-access-control v2 03/34] permissions: add ACL path for prefix-lists and route-maps
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 01/34] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 02/34] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 04/34] frr: add constructor to prefix list name Stefan Hanreich
` (30 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Add new paths for route maps and prefix lists respectively. Route maps
theoretically have multiple entries with an ordering number, but it
doesn't really make sense to make permissions more granular than on a
per-route map basis.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/AccessControl.pm | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index 350e074..05c9d84 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -1293,6 +1293,10 @@ sub check_path {
|/sdn/fabrics/[[:alnum:]]+
|/sdn/ipams
|/sdn/ipams/[[:alnum:]]+
+ |/sdn/prefix-lists
+ |/sdn/prefix-lists/[[:alnum:]]+
+ |/sdn/route-maps
+ |/sdn/route-maps/[[:alnum:]]+
|/sdn/zones
|/sdn/zones/[[:alnum:]\.\-\_]+
|/sdn/zones/[[:alnum:]\.\-\_]+/[[:alnum:]\.\-\_]+
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 04/34] frr: add constructor to prefix list name
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (2 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-access-control v2 03/34] permissions: add ACL path for prefix-lists and route-maps Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types Stefan Hanreich
` (29 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Notes:
Only required if Gabriel's vtysh integration test series isn't
applied.
proxmox-frr/src/ser/route_map.rs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index 1e0dd48..bd3fa4f 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -39,6 +39,12 @@ pub struct AccessListName(String);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct PrefixListName(String);
+impl PrefixListName {
+ pub fn new(name: String) -> PrefixListName {
+ PrefixListName(name)
+ }
+}
+
impl AccessListName {
pub fn new(name: String) -> AccessListName {
AccessListName(name)
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (3 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 04/34] frr: add constructor to prefix list name Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-02 13:36 ` Wolfgang Bumiller
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 06/34] frr: change order type to u16 Stefan Hanreich
` (28 subsequent siblings)
33 siblings, 1 reply; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
The reason for including those types here is that they are used in
proxmox-frr for generating the FRR configuration as well as
proxmox-ve-config for utilizing them in the section config types.
For some values in route maps FRR supports specifying either an
absolute value or a value relative to the existing value. When
modifying the metric of a route via a route map, '123' would set the
value to 123, whereas '+/-123' would add/subtract 123 from the
existing value. IntegerWithSign can be used to represent such a value
in the section config.
A custom deserializer is implemented, so primitive numerical values
can be used in addition to strings that contain the respective signs.
They deserialize into absolute values.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-sdn-types/src/bgp.rs | 62 ++++++++++++
proxmox-sdn-types/src/lib.rs | 179 +++++++++++++++++++++++++++++++++++
2 files changed, 241 insertions(+)
create mode 100644 proxmox-sdn-types/src/bgp.rs
diff --git a/proxmox-sdn-types/src/bgp.rs b/proxmox-sdn-types/src/bgp.rs
new file mode 100644
index 0000000..6df3022
--- /dev/null
+++ b/proxmox-sdn-types/src/bgp.rs
@@ -0,0 +1,62 @@
+use serde::{Deserialize, Serialize};
+
+use crate::IntegerWithSign;
+
+/// Represents a BGP metric value, as used in FRR.
+///
+/// A metric can either be a numeric value, or certain 'magic' values. For more information see the
+/// respective enum variants.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub enum SetMetricValue {
+ /// Set the metric to the round-trip-time.
+ #[serde(rename = "rtt")]
+ Rtt,
+ /// Add the round-trip-time to the metric.
+ #[serde(rename = "+rtt")]
+ AddRtt,
+ /// Subtract the round-trip-time from the metric.
+ #[serde(rename = "-rtt")]
+ SubtractRtt,
+ /// Use the IGP value when importing from another IGP.
+ #[serde(rename = "igp")]
+ Igp,
+ /// Use the accumulated IGP value when importing from another IGP.
+ #[serde(rename = "aigp")]
+ Aigp,
+ /// Set the metric to a fixed numeric value.
+ #[serde(untagged)]
+ Numeric(IntegerWithSign),
+}
+
+impl<T: Into<IntegerWithSign>> From<T> for SetMetricValue {
+ fn from(value: T) -> Self {
+ Self::Numeric(value.into())
+ }
+}
+
+/// An EVPN route-type, as used in the FRR route maps.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum EvpnRouteType {
+ Ead,
+ MacIp,
+ Multicast,
+ #[serde(rename = "es")]
+ EthernetSegment,
+ Prefix,
+}
+
+/// An tag value, as used in the FRR route maps.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum SetTagValue {
+ Untagged,
+ #[serde(untagged)]
+ Numeric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+}
+
+impl SetTagValue {
+ pub fn new(value: u32) -> Self {
+ SetTagValue::Numeric(value)
+ }
+}
diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
index 1656f1d..9795857 100644
--- a/proxmox-sdn-types/src/lib.rs
+++ b/proxmox-sdn-types/src/lib.rs
@@ -1,3 +1,182 @@
pub mod area;
+pub mod bgp;
pub mod net;
pub mod openfabric;
+
+use serde::de::{Error, Visitor};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api;
+
+/// Enum for representing signedness of Integer in [`IntegerWithSign`].
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub enum Sign {
+ #[serde(rename = "-")]
+ Negative,
+ #[serde(rename = "+")]
+ Positive,
+}
+
+proxmox_serde::forward_display_to_serialize!(Sign);
+proxmox_serde::forward_from_str_to_deserialize!(Sign);
+
+/// An Integer with an optional [`Sign`].
+///
+/// This is used for representing certain keys in the FRR route maps (e.g. metric). They can be set
+/// to either a static value (no sign) or to a value relative to the existing value (with sign).
+/// For instance, a value of 50 would set the metric to 50, but a value of +50 would add 50 to the
+/// existing metric value.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct IntegerWithSign {
+ pub(crate) sign: Option<Sign>,
+ pub(crate) n: u32,
+}
+
+impl IntegerWithSign {
+ pub fn new(sign: Option<Sign>, n: u32) -> Self {
+ Self { sign, n }
+ }
+}
+
+impl From<u32> for IntegerWithSign {
+ fn from(n: u32) -> Self {
+ Self { sign: None, n }
+ }
+}
+
+impl std::fmt::Display for IntegerWithSign {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if let Some(sign) = self.sign.as_ref() {
+ write!(f, "{}{}", sign, self.n)
+ } else {
+ self.n.fmt(f)
+ }
+ }
+}
+
+impl std::str::FromStr for IntegerWithSign {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some(n) = s.strip_prefix("+") {
+ return Ok(Self {
+ sign: Some(Sign::Positive),
+ n: n.parse()?,
+ });
+ }
+
+ if let Some(n) = s.strip_prefix("-") {
+ return Ok(Self {
+ sign: Some(Sign::Negative),
+ n: n.parse()?,
+ });
+ }
+
+ Ok(Self {
+ sign: None,
+ n: s.parse()?,
+ })
+ }
+}
+
+impl<'de> Deserialize<'de> for IntegerWithSign {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct V;
+
+ impl<'de> Visitor<'de> for V {
+ type Value = IntegerWithSign;
+
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.write_str("An integer with an optional leading sign")
+ }
+
+ fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+ v.parse().map_err(E::custom)
+ }
+ }
+
+ deserializer.deserialize_any(V)
+ }
+}
+
+proxmox_serde::forward_serialize_to_display!(IntegerWithSign);
+
+#[api(
+ type: Integer,
+ minimum: 1,
+ maximum: 16_777_215,
+)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
+#[repr(transparent)]
+/// Represents a VXLAN VNI (24-bit unsigned integer).
+pub struct Vni(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32);
+
+impl Vni {
+ /// Returns the VNI as u32.
+ pub fn as_u32(&self) -> u32 {
+ self.0
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::str::FromStr;
+
+ #[test]
+ fn test_parse_integer_with_sign() {
+ assert_eq!(
+ IntegerWithSign::from_str("+32").expect("valid IntegerWithSign"),
+ IntegerWithSign::new(Some(Sign::Positive), 32)
+ );
+
+ assert_eq!(
+ IntegerWithSign::from_str("-31322").expect("valid IntegerWithSign"),
+ IntegerWithSign::new(Some(Sign::Negative), 31322)
+ );
+
+ assert_eq!(
+ IntegerWithSign::from_str("32").expect("valid IntegerWithSign"),
+ IntegerWithSign::new(None, 32)
+ );
+ }
+
+ #[test]
+ fn test_display_integer_with_sign() {
+ for s in &["+32", "-1234", "43344"] {
+ let integer_with_sign: IntegerWithSign = s.parse().expect("is a valid IntegerWithSign");
+ assert_eq!(&integer_with_sign.to_string(), s)
+ }
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 06/34] frr: change order type to u16
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (4 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 07/34] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
` (27 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
The order in a route map entry is an u16, not an u32 - adapt this
here to match the actual type inside FRR.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/src/ser/route_map.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index bd3fa4f..636dde2 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -120,7 +120,7 @@ impl RouteMapName {
/// address or adding a metric, bgp community, or local preference.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteMapEntry {
- pub seq: u32,
+ pub seq: u16,
pub action: AccessAction,
#[serde(default)]
pub matches: Vec<RouteMapMatch>,
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 07/34] frr: implement routemap match/set statements via adjacent tagging
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (5 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 06/34] frr: change order type to u16 Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 08/34] frr: implement support for call and exit action Stefan Hanreich
` (26 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Previously, the types used a mix of adjacent / internal tagging and a
nesting of types to represent match and set statements. This has been
simplified by utilizing adjacent tagging on the set / match statements
and using the exact FRR configuration key as the tag. This way a
single enum can be used to represent match / set statements and all
variants can be rendered the same by simply printing the keys /
values.
This commit also adds a lot of new match / set statements that were
previously not supported. The crate supports now almost all match /
set statements that FRR supports - with only a few having been
omitted. Most notably it is not possible to match on community lists,
support for those is planned in a future patch series.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/Cargo.toml | 2 +-
proxmox-frr/debian/control | 2 +
proxmox-frr/src/ser/route_map.rs | 81 ++++++++++++++++++++++----------
3 files changed, 60 insertions(+), 25 deletions(-)
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index bfebfda..aa79909 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -17,7 +17,7 @@ serde = { workspace = true, features = [ "derive" ] }
serde_repr = "0.1"
minijinja = { version = "2.5", features = [ "multi_template", "loader" ] }
-proxmox-network-types = { workspace = true }
+proxmox-network-types = { workspace = true, features = ["api-types"] }
proxmox-sdn-types = { workspace = true }
proxmox-serde = { workspace = true }
proxmox-sortable-macro = "1"
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 265fa70..5978e69 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -10,6 +10,7 @@ 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-sdn-types-0.2+default-dev <!nocheck>,
librust-proxmox-serde-1+default-dev <!nocheck>,
@@ -35,6 +36,7 @@ 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-sdn-types-0.2+default-dev,
librust-proxmox-serde-1+default-dev,
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index 636dde2..3de7c42 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -1,6 +1,10 @@
-use std::net::IpAddr;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use proxmox_network_types::ip_address::Cidr;
+use proxmox_sdn_types::{
+ bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
+ IntegerWithSign, Vni,
+};
use serde::{Deserialize, Serialize};
/// The action for a [`AccessListRule`].
@@ -68,27 +72,36 @@ pub struct PrefixListRule {
/// execute its actions. If we match on an IP, there are two different syntaxes: `match ip ...` or
/// `match ipv6 ...`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "protocol_type")]
+#[serde(tag = "key", content = "value")]
pub enum RouteMapMatch {
- #[serde(rename = "ip")]
- V4(RouteMapMatchInner),
- #[serde(rename = "ipv6")]
- V6(RouteMapMatchInner),
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "list_type", content = "list_name", rename_all = "lowercase")]
-pub enum AccessListOrPrefixList {
- PrefixList(PrefixListName),
- AccessList(AccessListName),
-}
-
-/// A route-map match statement generic on the IP-version.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")]
-pub enum RouteMapMatchInner {
- Address(AccessListOrPrefixList),
- NextHop(String),
+ #[serde(rename = "evpn route-type")]
+ RouteType(EvpnRouteType),
+ #[serde(rename = "evpn vni")]
+ Vni(Vni),
+ #[serde(rename = "ip address")]
+ IpAddressAccessList(AccessListName),
+ #[serde(rename = "ipv6 address")]
+ Ip6AddressAccessList(AccessListName),
+ #[serde(rename = "ip address prefix-list")]
+ IpAddressPrefixList(PrefixListName),
+ #[serde(rename = "ipv6 address prefix-list")]
+ Ip6AddressPrefixList(PrefixListName),
+ #[serde(rename = "ip next-hop prefix-list")]
+ IpNextHopPrefixList(PrefixListName),
+ #[serde(rename = "ipv6 next-hop prefix-list")]
+ Ip6NextHopPrefixList(PrefixListName),
+ #[serde(rename = "ip next-hop address")]
+ IpNextHopAddress(Ipv4Addr),
+ #[serde(rename = "ipv6 next-hop address")]
+ Ip6NextHopAddress(Ipv6Addr),
+ #[serde(rename = "metric")]
+ Metric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "local-preference")]
+ LocalPreference(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "peer")]
+ Peer(String),
+ #[serde(rename = "tag")]
+ Tag(SetTagValue),
}
/// Defines the Action a route-map takes when it matches on a route.
@@ -96,11 +109,31 @@ pub enum RouteMapMatchInner {
/// If the route matches the [`RouteMapMatch`], then a [`RouteMapSet`] action will be executed.
/// We currently only use the IpSrc command which changes the source address of the route.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "set_type", content = "value", rename_all = "kebab-case")]
+#[serde(tag = "key", content = "value")]
pub enum RouteMapSet {
- LocalPreference(u32),
+ #[serde(rename = "ip next-hop peer-address")]
+ IpNextHopPeerAddress,
+ #[serde(rename = "ip next-hop unchanged")]
+ IpNextHopUnchanged,
+ #[serde(rename = "ip next-hop")]
+ IpNextHop(Ipv4Addr),
+ #[serde(rename = "ipv6 next-hop peer-address")]
+ Ip6NextHopPeerAddress,
+ #[serde(rename = "ipv6 next-hop prefer-global")]
+ Ip6NextHopPreferGlobal,
+ #[serde(rename = "ipv6 next-hop global")]
+ Ip6NextHop(Ipv6Addr),
+ #[serde(rename = "local-preference")]
+ LocalPreference(IntegerWithSign),
+ #[serde(rename = "tag")]
+ Tag(SetTagValue),
+ #[serde(rename = "weight")]
+ Weight(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "metric")]
+ Metric(SetMetricValue),
+ #[serde(rename = "src")]
Src(IpAddr),
- Metric(u32),
+ #[serde(rename = "community")]
Community(String),
}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 08/34] frr: implement support for call and exit action
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (6 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 07/34] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 09/34] frr-templates: change route maps template to adapt to new frr types Stefan Hanreich
` (25 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
They're required internally for implementing route maps for EVPN zones
with an exit node configured, but users can also utilize them to
create complex route maps via the pve-network API / section-config
format.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/src/ser/route_map.rs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index 3de7c42..54d88e7 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -137,6 +137,21 @@ pub enum RouteMapSet {
Community(String),
}
+/// The exit action for a route map.
+///
+/// This can be optionally specified to override the default behavior of FRR to terminate
+/// evaluating the route map if an entry matches.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "key", content = "value")]
+pub enum RouteMapExitAction {
+ #[serde(rename = "on-match next")]
+ OnMatchNext,
+ #[serde(rename = "on-match goto")]
+ OnMatchGoto(u16),
+ #[serde(rename = "continue")]
+ Continue(u16),
+}
+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RouteMapName(String);
@@ -160,5 +175,9 @@ pub struct RouteMapEntry {
#[serde(default)]
pub sets: Vec<RouteMapSet>,
#[serde(default)]
+ pub call: Option<RouteMapName>,
+ #[serde(default)]
+ pub exit_action: Option<RouteMapExitAction>,
+ #[serde(default)]
pub custom_frr_config: Vec<String>,
}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 09/34] frr-templates: change route maps template to adapt to new frr types
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (7 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 08/34] frr: implement support for call and exit action Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 10/34] ve-config: fabrics: adapt frr config generation Stefan Hanreich
` (24 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Instead of defining every potential match / set type manually under a
different name, proxmox-frr now uses the Adjacently tagged
representation for representing key/value pairs for match/set actions.
This allows simplifying the route_maps template by simply rendering
the respective key / value fields.
proxmox-frr added support for call actions and exit policies as well.
Add them to the template so they get rendered properly.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../templates/route_maps.jinja | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
index 172c682..dd44153 100644
--- a/proxmox-frr-templates/templates/route_maps.jinja
+++ b/proxmox-frr-templates/templates/route_maps.jinja
@@ -3,17 +3,20 @@
!
route-map {{ name }} {{ routemap.action }} {{ routemap.seq }}
{% for match in routemap.matches %}
-{% if match.value.list_type == "prefixlist" %}
- match {{ match.protocol_type }} {{ match.match_type }} prefix-list {{ match.value.list_name }}
-{% elif match.value.list_type == "accesslist" %}
- match {{ match.protocol_type }} {{ match.match_type }} {{ match.value.list_name }}
-{% elif match.match_type == "next-hop" %}
- match {{ match.protocol_type }} next-hop {{ match.value }}
-{% endif %}
+ match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
+
{% endfor %}
{% for set in routemap.sets %}
- set {{ set.set_type }} {{ set.value }}
+ set {{ set.key }} {% if set.value is defined %} {{ set.value }} {% endif %}
+
{% endfor %}
+{% if routemap.call %}
+ call {{ routemap.call }}
+{% endif %}
+{% if routemap.exit_action %}
+ {{ routemap.exit_action.key }}{% if routemap.exit_action.value is defined %} {{ routemap.exit_action.value }} {% endif %}
+
+{% endif %}
{% for line in routemap.custom_frr_config %}
{{ line }}
{% endfor %}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 10/34] ve-config: fabrics: adapt frr config generation
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (8 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 09/34] frr-templates: change route maps template to adapt to new frr types Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 11/34] ve-config: add prefix list section config Stefan Hanreich
` (23 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
proxmox-frr has changed the representation of match and set
statements. Additionally, the sequence number for route map entries is
an u16 now (the same as in FRR). Adjust the existing fabric FRR config
generation code, so it is compatible with the changes in proxmox-frr.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/frr.rs | 33 +++++++++++--------------
1 file changed, 15 insertions(+), 18 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index f2b7c72..b816ef6 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -5,8 +5,7 @@ use tracing;
use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
use proxmox_frr::ser::route_map::{
- AccessAction, AccessListName, AccessListOrPrefixList, RouteMapEntry, RouteMapMatch,
- RouteMapMatchInner, RouteMapName, RouteMapSet,
+ AccessAction, AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
};
use proxmox_frr::ser::{
self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
@@ -390,7 +389,7 @@ fn build_openfabric_dummy_interface(
fn build_openfabric_routemap(
fabric_id: &FabricId,
router_ip: IpAddr,
- seq: u32,
+ seq: u16,
) -> (RouteMapName, RouteMapEntry) {
let routemap_name = match router_ip {
IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()),
@@ -402,19 +401,17 @@ fn build_openfabric_routemap(
seq,
action: ser::route_map::AccessAction::Permit,
matches: vec![match router_ip {
- IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address(
- AccessListOrPrefixList::AccessList(AccessListName::new(format!(
- "pve_openfabric_{fabric_id}_ips"
- ))),
- )),
- IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(
- AccessListOrPrefixList::AccessList(AccessListName::new(format!(
- "pve_openfabric_{fabric_id}_ip6s"
- ))),
- )),
+ IpAddr::V4(_) => RouteMapMatch::IpAddressAccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ips"
+ ))),
+ IpAddr::V6(_) => RouteMapMatch::Ip6AddressAccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ip6s"
+ ))),
}],
sets: vec![RouteMapSet::Src(router_ip)],
custom_frr_config: Vec::new(),
+ call: None,
+ exit_action: None,
},
)
}
@@ -423,20 +420,20 @@ fn build_openfabric_routemap(
fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
router_ip: Ipv4Addr,
- seq: u32,
+ seq: u16,
) -> Result<(RouteMapName, RouteMapEntry), anyhow::Error> {
let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
// create route-map
let routemap = RouteMapEntry {
seq,
action: AccessAction::Permit,
- matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
- AccessListOrPrefixList::AccessList(AccessListName::new(format!(
- "pve_ospf_{fabric_id}_ips"
- ))),
+ matches: vec![RouteMapMatch::IpAddressAccessList(AccessListName::new(
+ format!("pve_ospf_{fabric_id}_ips"),
))],
sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))],
custom_frr_config: Vec::new(),
+ call: None,
+ exit_action: None,
};
Ok((routemap_name, routemap))
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 11/34] ve-config: add prefix list section config
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (9 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 10/34] ve-config: fabrics: adapt frr config generation Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
` (22 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Those types represent FRR prefix lists inside a section config format.
For an example of the exact format and its FRR representation see the
module-level documentation.
Contrary to most SDN entities, prefix list IDs can be 32 characters
long instead of 8 and support underscores as well as hyphens. This is
because the restriction of having to generate network interface names
does not apply to FRR entities, so we can be more lenient with IDs
here, allowing users to specify more descriptive names.
There is support for specifying the sequence number of the prefix list
entry, but it will be backend-only. FRR supports auto-generating the
sequence numbers, so it is sufficient to write them to the FRR
configuration in the desired order.
The API types are currently equivalent to the section config types,
but are still contained as type aliases in their own module, to make
changing that in the future easier, since it requires no changes to
call sites as long as the exposed API of the struct is the same.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/prefix_list.rs | 160 +++++++++++++++++++++++
2 files changed, 161 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 86dcd93..344c02c 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,6 +1,7 @@
pub mod config;
pub mod fabric;
pub mod ipam;
+pub mod prefix_list;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
new file mode 100644
index 0000000..f4988d9
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -0,0 +1,160 @@
+//! Section config types for FRR Prefix Lists.
+//!
+//! This module contains the API types for representing FRR Prefix Lists as section config. Each
+//! entry in the section config represents a Prefix List and its entries.
+//!
+//! A simple FRR Prefix List looks like this:
+//!
+//! ```text
+//! ip prefix-list example-list permit 192.0.2.0/24 ge 25 le 26
+//! ip prefix-list example-list permit 192.0.2.0/24 le 28 ge 29
+//! ip prefix-list example-list deny 192.0.2.0/24 le 24
+//! ```
+//!
+//! The corresponding section config entry looks like this:
+//!
+//! ```text
+//! prefix-list: example_list
+//! entries action=permit,prefix=192.0.2.0/24,ge=25,le=26
+//! entries action=permit,prefix=192.0.2.0/24,ge=28,le=29
+//! entries action=deny,prefix=192.0.2.0/24,le=24
+//! ```
+
+use const_format::concatcp;
+use serde::{Deserialize, Serialize};
+
+use proxmox_network_types::Cidr;
+use proxmox_schema::{
+ api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat, Updater,
+ UpdaterType,
+};
+
+pub const PREFIX_LIST_ID_REGEX_STR: &str =
+ r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-_]){0,30}(?:[a-zA-Z0-9]){0,1})";
+
+const_regex! {
+ pub PREFIX_LIST_ID_REGEX = concatcp!(r"^", PREFIX_LIST_ID_REGEX_STR, r"$");
+}
+
+pub const PREFIX_LIST_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PREFIX_LIST_ID_REGEX);
+
+api_string_type! {
+ /// ID of a Prefix List.
+ #[api(format: &PREFIX_LIST_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct PrefixListId(String);
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Action for an entry in a Prefix List.
+pub enum PrefixListAction {
+ /// permit
+ Permit,
+ /// deny
+ Deny,
+}
+
+#[api(
+ properties: {
+ entries: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "An entry in a prefix list",
+ format: &ApiStringFormat::PropertyString(&PrefixListEntry::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+/// IP Prefix List
+///
+/// Corresponds to the FRR IP Prefix lists, as described in its [documentation](https://docs.frrouting.org/en/latest/filter.html#ip-prefix-list)
+pub struct PrefixListSection {
+ #[updater(skip)]
+ id: PrefixListId,
+ /// The entries in this prefix list
+ pub entries: Vec<PropertyString<PrefixListEntry>>,
+}
+
+impl PrefixListSection {
+ /// Return the ID of the Prefix List.
+ pub fn id(&self) -> &PrefixListId {
+ &self.id
+ }
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+/// IP Prefix List Entry
+///
+/// Corresponds to the FRR IP Prefix lists, as described in its [documentation](https://docs.frrouting.org/en/latest/filter.html#ip-prefix-list)
+pub struct PrefixListEntry {
+ action: PrefixListAction,
+ prefix: Cidr,
+ /// Prefix length - entry will be applied if the prefix length is less than or equal to this
+ /// value.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ le: Option<u32>,
+ /// Prefix length - entry will be applied if the prefix length is greater than or equal to this
+ /// value.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ge: Option<u32>,
+ /// The sequence number for this prefix list entry.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ seq: Option<u32>,
+}
+
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Prefix List Section ID",
+ format: &PREFIX_LIST_ID_FORMAT,
+ },
+ "type-key": "type",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "type")]
+pub enum PrefixList {
+ PrefixList(PrefixListSection),
+}
+
+pub mod api {
+ use super::*;
+
+ pub type PrefixList = PrefixListSection;
+ pub type PrefixListUpdater = PrefixListSectionUpdater;
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ #[serde(rename_all = "kebab-case")]
+ /// Deletable properties for [`PrefixList`].
+ pub enum PrefixListDeletableProperties {
+ Entries,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_simple_prefix_list() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: somelist
+ entries action=permit,prefix=192.0.2.0/24
+ entries action=permit,prefix=192.0.2.0/24,le=32
+ entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
+ entries action=permit,prefix=192.0.2.0/24,ge=24
+ entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+"#;
+
+ PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (10 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 11/34] ve-config: add prefix list section config Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-03 7:42 ` Wolfgang Bumiller
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 13/34] ve-config: add route map section config Stefan Hanreich
` (21 subsequent siblings)
33 siblings, 1 reply; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Implements conversion traits for all the section config types, so they
can be converted into their respective FRR template counterpart.
Also add a helper that adds a list of prefix lists to an existing FRR
configuration. This will be used by perl-rs to generate the FRR
configuration from the section configuration. The helper will
overwrite existing prefix lists in the FRR configuration, allowing
users to override pre-defined prefix lists generated by our stack.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/prefix_list.rs | 60 ++++++++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
index f4988d9..1876799 100644
--- a/proxmox-ve-config/src/sdn/prefix_list.rs
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -123,6 +123,66 @@ pub enum PrefixList {
PrefixList(PrefixListSection),
}
+#[cfg(feature = "frr")]
+pub mod frr {
+ use super::*;
+
+ use proxmox_frr::ser::{
+ route_map::{
+ self, PrefixListName as FrrPrefixListName, PrefixListRule as FrrPrefixListRule,
+ },
+ FrrConfig,
+ };
+
+ impl From<PrefixListId> for FrrPrefixListName {
+ fn from(value: PrefixListId) -> Self {
+ FrrPrefixListName::new(value.0)
+ }
+ }
+
+ impl From<PrefixListEntry> for FrrPrefixListRule {
+ fn from(value: PrefixListEntry) -> Self {
+ FrrPrefixListRule {
+ action: match value.action {
+ PrefixListAction::Permit => route_map::AccessAction::Permit,
+ PrefixListAction::Deny => route_map::AccessAction::Deny,
+ },
+ network: value.prefix,
+ seq: value.seq,
+ le: value.le,
+ ge: value.ge,
+ is_ipv6: value.prefix.is_ipv6(),
+ }
+ }
+ }
+
+ /// Add a list of Prefix Lists to an [`FrrConfig`].
+ ///
+ /// This will overwrite existing Prefix Lists in the [`FrrConfig`]. Since this will be used for
+ /// generating the FRR configuration from the SDN stack, this enables users to override Prefix
+ /// Lists that are predefined by our stack.
+ pub fn build_frr_prefix_lists(
+ prefix_lists: impl IntoIterator<Item = PrefixList>,
+ frr_config: &mut FrrConfig,
+ ) -> Result<(), anyhow::Error> {
+ for prefix_list in prefix_lists.into_iter() {
+ let PrefixList::PrefixList(prefix_list) = prefix_list;
+ let prefix_list_name = FrrPrefixListName::new(prefix_list.id.0);
+
+ frr_config.prefix_lists.insert(
+ prefix_list_name,
+ prefix_list
+ .entries
+ .into_iter()
+ .map(|prefix_list| prefix_list.into_inner().into())
+ .collect(),
+ );
+ }
+
+ Ok(())
+ }
+}
+
pub mod api {
use super::*;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 13/34] ve-config: add route map section config
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (11 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 14/34] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
` (20 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Those types represent FRR route maps inside a section config format.
For an example of the exact format and its FRR representation see the
module-level documentation.
One section config entry maps to one route map entry. A route map
consists of one or more route map entries inside the section config.
The ID of a section encodes the name of the route map as well as the
order # of the entry.
The route map module exports specific types for the API that handle
converting the section config ID, because currently it is only
possible to deserialize section config IDs to Strings. To avoid
having to implement the parsing logic along every step of the stack
(Perl backend, UI), use specific API types in the public API that
handle parsing the section ID into route map name and order.
Contrary to most SDN entities, route maps IDs can be 32 characters
long instead of 8 and support underscores as well as hyphens. This is
because the restriction of having to generate network interface names
does not apply to FRR entities, so we can be more lenient with IDs
here, allowing users to specify more descriptive names.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/route_map.rs | 581 +++++++++++++++++++++++++
2 files changed, 582 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 344c02c..24069ad 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -2,6 +2,7 @@ pub mod config;
pub mod fabric;
pub mod ipam;
pub mod prefix_list;
+pub mod route_map;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
new file mode 100644
index 0000000..61607d7
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/route_map.rs
@@ -0,0 +1,581 @@
+//! Section config types for FRR Route Maps.
+//!
+//! This module contains the API types required for representing FRR Route Maps as section config.
+//! Each entry in the section config maps to a Route Map entry, *not* a route map as a whole, the
+//! order of the entry is encoded in the ID of the Route Map.
+//!
+//! Route maps in FRR consists of at least one entry, which are ordered by their given sequence
+//! number / order. Each entry has a default matching policy, which is applied if the matching
+//! conditions of the entry are met.
+//!
+//! An example for a simple FRR Route Map entry looks like this:
+//!
+//! ```text
+//! route-map test permit 10
+//! match ip next-hop address 192.0.2.1
+//! set local-preference 200
+//! on-match goto 1234
+//!
+//! route-map test permit 20
+//! call some-other-routemap
+//! ```
+//!
+//! The corresponding representation as a section config entry looks like this:
+//!
+//! ```text
+//! route-map-entry: test_10
+//! action permit
+//! match key=ip-next-hop-address,value=192.0.2.1
+//! set key=local-preference,value=200
+//! exit-action key=on-match-goto,value=1234
+//!
+//! route-map-entry: test_20
+//! call some-other-routemap
+//! ```
+//!
+//! Match / Set Actions and Exit Policies are encoded as an array with a property string that has a
+//! key and an optional value paramter, because some options do not require an additional value.
+
+use std::net::IpAddr;
+
+use anyhow::format_err;
+use const_format::concatcp;
+
+use proxmox_network_types::ip_address::api_types::{Ipv4Addr, Ipv6Addr};
+use proxmox_sdn_types::{
+ bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
+ IntegerWithSign, Vni,
+};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{
+ api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat, ApiType,
+ EnumEntry, ObjectSchema, Schema, StringSchema, Updater, UpdaterType,
+};
+
+use crate::sdn::prefix_list::PrefixListId;
+
+pub const ROUTE_MAP_ID_REGEX_STR: &str =
+ r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-_]){0,30}(?:[a-zA-Z0-9]){0,1})";
+
+pub const ROUTE_MAP_ORDER_REGEX_STR: &str = r"\d+";
+
+const_regex! {
+ pub ROUTE_MAP_ID_REGEX = concatcp!(r"^", ROUTE_MAP_ID_REGEX_STR, r"$");
+ pub ROUTE_MAP_SECTION_ID_REGEX = concatcp!(r"^", ROUTE_MAP_ID_REGEX_STR, r"_", ROUTE_MAP_ORDER_REGEX_STR, r"$");
+}
+
+pub const ROUTE_MAP_SECTION_ID_FORMAT: ApiStringFormat =
+ ApiStringFormat::Pattern(&ROUTE_MAP_SECTION_ID_REGEX);
+
+pub const ROUTE_MAP_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&ROUTE_MAP_ID_REGEX);
+
+api_string_type! {
+ /// ID of a Route Map..
+ #[api(format: &ROUTE_MAP_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct RouteMapId(String);
+}
+
+/// The ID of a Route Map entry in the section config (name + order).
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RouteMapEntryId {
+ /// name of the Route Map
+ route_map_id: RouteMapId,
+ /// seq nr of the Route Map
+ order: u16,
+}
+
+impl RouteMapEntryId {
+ /// Create a new Route Map Entry ID.
+ pub fn new(route_map_id: RouteMapId, order: u16) -> Self {
+ Self {
+ route_map_id,
+ order,
+ }
+ }
+
+ /// Returns the name part of the Route Map section id.
+ pub fn route_map_id(&self) -> &RouteMapId {
+ &self.route_map_id
+ }
+
+ /// Returns the order part of the Route Map section id.
+ pub fn order(&self) -> u16 {
+ self.order
+ }
+}
+
+impl ApiType for RouteMapEntryId {
+ const API_SCHEMA: Schema = StringSchema::new("ID of a SDN node in the section config")
+ .format(&ROUTE_MAP_SECTION_ID_FORMAT)
+ .schema();
+}
+
+impl std::fmt::Display for RouteMapEntryId {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}_{}", self.route_map_id, self.order)
+ }
+}
+
+proxmox_serde::forward_serialize_to_display!(RouteMapEntryId);
+
+impl std::str::FromStr for RouteMapEntryId {
+ type Err = anyhow::Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let (name, order) = value
+ .rsplit_once("_")
+ .ok_or_else(|| format_err!("invalid RouteMap section id: {}", value))?;
+
+ Ok(Self {
+ route_map_id: RouteMapId::from_string(name.to_string())?,
+ order: order.parse()?,
+ })
+ }
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(RouteMapEntryId);
+
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Route Map Section ID",
+ format: &ROUTE_MAP_SECTION_ID_FORMAT,
+ },
+ "type-key": "type",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "type")]
+/// The Route Map section config type.
+pub enum RouteMap {
+ RouteMapEntry(RouteMapEntry),
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Matching policy of a Route Map entry.
+pub enum RouteMapAction {
+ /// Permit
+ Permit,
+ /// Deny
+ Deny,
+}
+
+/// The exit action for a route map.
+///
+/// This can be optionally specified to override the default behavior of FRR to terminate
+/// evaluating the route map if an entry matches.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "key", content = "value", rename_all = "kebab-case")]
+pub enum ExitAction {
+ OnMatchNext,
+ OnMatchGoto(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16),
+ Continue(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16),
+}
+
+impl ApiType for ExitAction {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "Exit action for a FRR route map.",
+ &[
+ (
+ "key",
+ false,
+ &StringSchema::new("The key indicating which value should be set.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new(
+ "on-match-next",
+ "Proceed with the next entry in the route map.",
+ ),
+ EnumEntry::new("continue", "Continue with route map entry with order <value>."),
+ EnumEntry::new(
+ "on-match-goto",
+ "Continue processing the route map with the first entry with order >= <value>.",
+ ),
+ ]))
+ .schema(),
+ ),
+ (
+ "value",
+ true,
+ &StringSchema::new("The value that should be set - depends on the given key.")
+ .schema(),
+ ),
+ ],
+ )
+ .schema();
+}
+
+impl UpdaterType for ExitAction {
+ type Updater = Option<ExitAction>;
+}
+
+#[api(
+ properties: {
+ set: {
+ type: Array,
+ description: "A list of Set actions to perform in this entry.",
+ optional: true,
+ items: {
+ type: String,
+ description: "A specific Set action.",
+ format: &ApiStringFormat::PropertyString(&SetAction::API_SCHEMA),
+ }
+ },
+ "match": {
+ type: Array,
+ description: "A list of Match actions to perform in this entry.",
+ optional: true,
+ items: {
+ type: String,
+ description: "A specific match action.",
+ format: &ApiStringFormat::PropertyString(&MatchAction::API_SCHEMA),
+ }
+ },
+ "exit-action": {
+ type: String,
+ description: "Exit action for the route map.",
+ optional: true,
+ format: &ApiStringFormat::PropertyString(&ExitAction::API_SCHEMA),
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+/// Route Map Entry
+///
+/// Represents one entry in a Route Map. One Route Map is made up of one or more entries, that are
+/// executed in order of their ordering number.
+#[serde(rename_all = "kebab-case")]
+pub struct RouteMapEntry {
+ id: RouteMapEntryId,
+ action: RouteMapAction,
+ #[serde(default, rename = "set")]
+ set_actions: Vec<PropertyString<SetAction>>,
+ #[serde(default, rename = "match")]
+ match_actions: Vec<PropertyString<MatchAction>>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ call: Option<RouteMapId>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ exit_action: Option<PropertyString<ExitAction>>,
+}
+
+impl RouteMapEntry {
+ /// Return the ID of the Route Map.
+ pub fn id(&self) -> &RouteMapEntryId {
+ &self.id
+ }
+
+ /// Sets the action for this entry.
+ pub fn set_action(&mut self, action: RouteMapAction) {
+ self.action = action;
+ }
+
+ /// Set the set actions for this route map entry.
+ pub fn set_set_actions(
+ &mut self,
+ set_actions: impl IntoIterator<Item = PropertyString<SetAction>>,
+ ) {
+ self.set_actions = set_actions.into_iter().collect();
+ }
+
+ /// Set the match actions for this route map entry.
+ pub fn set_match_actions(
+ &mut self,
+ match_actions: impl IntoIterator<Item = PropertyString<MatchAction>>,
+ ) {
+ self.match_actions = match_actions.into_iter().collect();
+ }
+
+ pub fn set_call(&mut self, call: Option<RouteMapId>) {
+ self.call = call;
+ }
+
+ pub fn set_exit_action(&mut self, exit_action: Option<PropertyString<ExitAction>>) {
+ self.exit_action = exit_action;
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "key", content = "value")]
+/// A Route Map set action
+pub enum SetAction {
+ IpNextHopPeerAddress,
+ IpNextHopUnchanged,
+ IpNextHop(Ipv4Addr),
+ Ip6NextHopPeerAddress,
+ Ip6NextHopPreferGlobal,
+ Ip6NextHop(Ipv6Addr),
+ Weight(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ Tag(SetTagValue),
+ Metric(SetMetricValue),
+ LocalPreference(IntegerWithSign),
+ Src(IpAddr),
+}
+
+impl ApiType for SetAction {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "FRR set action",
+ &[
+ (
+ "key",
+ false,
+ &StringSchema::new("The key indicating which value should be set.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new(
+ "ip-next-hop-peer-address",
+ "Sets the BGP nexthop address to the IPv4 peer address.",
+ ),
+ EnumEntry::new("ip-next-hop-unchanged", "Leaves the nexthop unchanged."),
+ EnumEntry::new(
+ "ip-next-hop",
+ "Sets the nexthop to the given IPv4 address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-peer-address",
+ "Sets the BGP nexthop address to the IPv6 peer address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-prefer-global",
+ "If a LLA and GUA are received, prefer the GUA.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop",
+ "Sets the nexthop to the given IPv6 address.",
+ ),
+ EnumEntry::new(
+ "local-preference",
+ "Sets the local preference for this route.",
+ ),
+ EnumEntry::new("tag", "Sets a tag for the route."),
+ EnumEntry::new("weight", "Sets the weight for the route."),
+ EnumEntry::new("metric", "Sets the metric for the route."),
+ EnumEntry::new(
+ "src",
+ "The source address to insert into the kernel routing table.",
+ ),
+ ]))
+ .schema(),
+ ),
+ (
+ "value",
+ true,
+ &StringSchema::new("The value that should be set - depends on the given key.")
+ .schema(),
+ ),
+ ],
+ )
+ .schema();
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "key", content = "value")]
+pub enum MatchAction {
+ RouteType(EvpnRouteType),
+ Vni(Vni),
+ IpAddressPrefixList(PrefixListId),
+ Ip6AddressPrefixList(PrefixListId),
+ IpNextHopPrefixList(PrefixListId),
+ Ip6NextHopPrefixList(PrefixListId),
+ IpNextHopAddress(Ipv4Addr),
+ Ip6NextHopAddress(Ipv6Addr),
+ Tag(SetTagValue),
+ Metric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ LocalPreference(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ Peer(String),
+}
+
+impl ApiType for MatchAction {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "FRR set action",
+ &[
+ (
+ "key",
+ false,
+ &StringSchema::new("The key indicating on which value to match.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new("route-type", "Match the EVPN route type."),
+ EnumEntry::new("vni", "Match the VNI of an EVPN route."),
+ EnumEntry::new(
+ "ip-address-prefix-list",
+ "Match the IPv4 CIDR to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip6-address-prefix-list",
+ "Match the IPv6 CIDR to a prefix-list",
+ ),
+ EnumEntry::new(
+ "ip-next-hop-prefix-list",
+ "Match the IPv4 next-hop to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-prefix-list",
+ "Match the IPv4 next-hop to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip-next-hop-address",
+ "Match the next-hop to an IPv4 address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-address",
+ "Match the next-hop to an IPv6 address.",
+ ),
+ EnumEntry::new("metric", "Match the metric of the route."),
+ EnumEntry::new("local-preference", "Match the local preference."),
+ EnumEntry::new(
+ "peer",
+ "Match the peer IP address, interface name or peer group.",
+ ),
+ ]))
+ .schema(),
+ ),
+ (
+ "value",
+ true,
+ &StringSchema::new("The value that should be matched - depends on the given key.")
+ .schema(),
+ ),
+ ],
+ )
+ .schema();
+}
+
+pub mod api {
+ //! API type for Route Map Entries.
+ //!
+ //! Since Route Map Entries encode information in their ID, these types help converting to /
+ //! from the Section Config types.
+ use super::*;
+
+ #[api(
+ properties: {
+ set: {
+ type: Array,
+ description: "A list of set actions for this Route Map entry",
+ optional: true,
+ items: {
+ type: String,
+ description: "A set action",
+ format: &ApiStringFormat::PropertyString(&SetAction::API_SCHEMA),
+ }
+ },
+ "match": {
+ type: Array,
+ description: "A list of match actions for this Route Map entry",
+ optional: true,
+ items: {
+ type: String,
+ description: "A match action",
+ format: &ApiStringFormat::PropertyString(&MatchAction::API_SCHEMA),
+ }
+ },
+ "exit-action": {
+ type: String,
+ description: "Exit action for the route map.",
+ optional: true,
+ format: &ApiStringFormat::PropertyString(&ExitAction::API_SCHEMA),
+ },
+ }
+ )]
+ #[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+ #[serde(rename_all = "kebab-case")]
+ /// Route Map entry
+ pub struct RouteMapEntry {
+ /// name of the Route Map
+ #[updater(skip)]
+ pub route_map_id: RouteMapId,
+ /// seq nr of the Route Map
+ #[updater(skip)]
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")]
+ pub order: u16,
+ pub action: RouteMapAction,
+ #[serde(default, rename = "set")]
+ pub set_actions: Vec<PropertyString<SetAction>>,
+ #[serde(default, rename = "match")]
+ pub match_actions: Vec<PropertyString<MatchAction>>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub call: Option<RouteMapId>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub exit_action: Option<PropertyString<ExitAction>>,
+ }
+
+ impl RouteMapEntry {
+ /// Return the ID of the Route Map this entry belongs to.
+ pub fn route_map_id(&self) -> &RouteMapId {
+ &self.route_map_id
+ }
+
+ /// Return the order for this Route Map entry.
+ pub fn order(&self) -> u16 {
+ self.order
+ }
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+ #[serde(rename_all = "kebab-case")]
+ /// Deletable properties for Route Map entries.
+ pub enum RouteMapDeletableProperties {
+ Set,
+ Match,
+ Call,
+ ExitAction,
+ }
+
+ impl From<super::RouteMapEntry> for RouteMapEntry {
+ fn from(value: super::RouteMapEntry) -> RouteMapEntry {
+ RouteMapEntry {
+ route_map_id: value.id.route_map_id,
+ order: value.id.order,
+ action: value.action,
+ set_actions: value.set_actions,
+ match_actions: value.match_actions,
+ call: value.call,
+ exit_action: value.exit_action,
+ }
+ }
+ }
+
+ impl From<RouteMapEntry> for super::RouteMapEntry {
+ fn from(value: RouteMapEntry) -> super::RouteMapEntry {
+ super::RouteMapEntry {
+ id: RouteMapEntryId {
+ route_map_id: value.route_map_id,
+ order: value.order,
+ },
+ action: value.action,
+ set_actions: value.set_actions,
+ match_actions: value.match_actions,
+ call: value.call,
+ exit_action: value.exit_action,
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_simple_route_map() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: test_underscore_123
+ action permit
+ set key=tag,value=23487
+ set key=tag,value=untagged
+ set key=metric,value=+rtt
+ set key=local-preference,value=-12345
+ set key=ip-next-hop,value=192.0.2.0
+ match key=vni,value=23487
+ match key=vni,value=23487
+ call some-other-route-map
+ exit-action key=on-match-goto,value=1234
+"#;
+
+ RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 14/34] ve-config: frr: implement frr config generation for route maps
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (12 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 13/34] ve-config: add route map section config Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 15/34] ve-config: add prefix lists integration tests Stefan Hanreich
` (19 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Implements conversion traits for all the section config types, so they
can be converted into their respective FRR template counterpart.
This module contains a helper for adding all route map entries to an
existing FRR configuration. If the pre-existing FRR configuration
contains a route map that has the same name as at least one entry in
the configuration, then the *whole* route map will get overwritten by
the route map defined in the section config.
The helper also automatically re-orders route map entries according to
their ordering number. This allows for deterministic FRR configuration
output, which is required for stable tests and convenient for human
readability.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/route_map.rs | 147 +++++++++++++++++++++++++
1 file changed, 147 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
index 61607d7..8f9c17c 100644
--- a/proxmox-ve-config/src/sdn/route_map.rs
+++ b/proxmox-ve-config/src/sdn/route_map.rs
@@ -441,6 +441,153 @@ impl ApiType for MatchAction {
.schema();
}
+#[cfg(feature = "frr")]
+pub mod frr {
+ //! Route Map Entry FRR types
+ //!
+ //! This module contains implementations of conversion traits for the section config types, so
+ //! they can be converted to the respective proxmox-frr types. This enables easy conversion to
+ //! the proxmox-frr types and makes it possible to generate the FRR configuration for the Route
+ //! Map entries.
+
+ use super::*;
+
+ use std::collections::HashMap;
+
+ use proxmox_frr::ser::{
+ route_map::{
+ RouteMapEntry as FrrRouteMapEntry, RouteMapExitAction as FrrRouteMapExitAction,
+ RouteMapMatch as FrrRouteMapMatch, RouteMapName as FrrRouteMapName,
+ RouteMapSet as FrrRouteMapSet,
+ },
+ FrrConfig,
+ };
+
+ use crate::sdn::route_map::RouteMapAction;
+
+ impl From<MatchAction> for FrrRouteMapMatch {
+ fn from(value: MatchAction) -> Self {
+ match value {
+ MatchAction::RouteType(evpn_route_type) => Self::RouteType(evpn_route_type),
+ MatchAction::Vni(vni) => Self::Vni(vni),
+ MatchAction::IpAddressPrefixList(prefix_list_name) => {
+ Self::IpAddressPrefixList(prefix_list_name.into())
+ }
+ MatchAction::Ip6AddressPrefixList(prefix_list_name) => {
+ Self::Ip6AddressPrefixList(prefix_list_name.into())
+ }
+ MatchAction::IpNextHopPrefixList(prefix_list_name) => {
+ Self::IpNextHopPrefixList(prefix_list_name.into())
+ }
+ MatchAction::Ip6NextHopPrefixList(prefix_list_name) => {
+ Self::Ip6NextHopPrefixList(prefix_list_name.into())
+ }
+ MatchAction::IpNextHopAddress(ipv4_addr) => Self::IpNextHopAddress(*ipv4_addr),
+ MatchAction::Ip6NextHopAddress(ipv6_addr) => Self::Ip6NextHopAddress(*ipv6_addr),
+ MatchAction::Metric(metric) => Self::Metric(metric),
+ MatchAction::LocalPreference(local_preference) => {
+ Self::LocalPreference(local_preference)
+ }
+ MatchAction::Peer(ip_addr) => Self::Peer(ip_addr),
+ MatchAction::Tag(tag) => Self::Tag(tag),
+ }
+ }
+ }
+
+ impl From<SetAction> for FrrRouteMapSet {
+ fn from(value: SetAction) -> Self {
+ match value {
+ SetAction::IpNextHopPeerAddress => Self::IpNextHopPeerAddress,
+ SetAction::IpNextHopUnchanged => Self::IpNextHopUnchanged,
+ SetAction::IpNextHop(ipv4_addr) => Self::IpNextHop(*ipv4_addr),
+ SetAction::Ip6NextHopPeerAddress => Self::Ip6NextHopPeerAddress,
+ SetAction::Ip6NextHopPreferGlobal => Self::Ip6NextHopPreferGlobal,
+ SetAction::Ip6NextHop(ipv6_addr) => Self::Ip6NextHop(*ipv6_addr),
+ SetAction::LocalPreference(local_preference) => {
+ Self::LocalPreference(local_preference)
+ }
+ SetAction::Tag(tag) => Self::Tag(tag),
+ SetAction::Weight(weight) => Self::Weight(weight),
+ SetAction::Metric(metric) => Self::Metric(metric),
+ SetAction::Src(src) => Self::Src(src),
+ }
+ }
+ }
+
+ impl From<ExitAction> for FrrRouteMapExitAction {
+ fn from(value: ExitAction) -> Self {
+ match value {
+ ExitAction::OnMatchNext => FrrRouteMapExitAction::OnMatchNext,
+ ExitAction::OnMatchGoto(n) => FrrRouteMapExitAction::OnMatchGoto(n),
+ ExitAction::Continue(n) => FrrRouteMapExitAction::Continue(n),
+ }
+ }
+ }
+
+ impl From<RouteMapId> for FrrRouteMapName {
+ fn from(value: RouteMapId) -> Self {
+ FrrRouteMapName::new(value.0)
+ }
+ }
+
+ impl From<RouteMapEntry> for FrrRouteMapEntry {
+ fn from(value: RouteMapEntry) -> FrrRouteMapEntry {
+ FrrRouteMapEntry {
+ seq: value.id.order,
+ action: match value.action {
+ RouteMapAction::Permit => proxmox_frr::ser::route_map::AccessAction::Permit,
+ RouteMapAction::Deny => proxmox_frr::ser::route_map::AccessAction::Deny,
+ },
+ matches: value
+ .match_actions
+ .into_iter()
+ .map(|match_action| match_action.into_inner().into())
+ .collect(),
+ sets: value
+ .set_actions
+ .into_iter()
+ .map(|set_action| set_action.into_inner().into())
+ .collect(),
+ call: value.call.map(FrrRouteMapName::from),
+ exit_action: value.exit_action.map(|value| value.into_inner().into()),
+ custom_frr_config: Default::default(),
+ }
+ }
+ }
+
+ /// Add a list of Route Map Entries to a [`FrrConfig`].
+ ///
+ /// This method takes a list of Route Map Entries and adds them to given FRR configuration.
+ /// If a route map with the same name as at least one entry in the config exists in the FRR
+ /// configuration, then the *whole* route map will get overwritten with the route map from the
+ /// configuration.
+ pub fn build_frr_route_maps(
+ config: impl IntoIterator<Item = RouteMap>,
+ frr_config: &mut FrrConfig,
+ ) -> Result<(), anyhow::Error> {
+ let mut config_route_map: HashMap<FrrRouteMapName, Vec<FrrRouteMapEntry>> = HashMap::new();
+
+ for route_map in config.into_iter() {
+ let RouteMap::RouteMapEntry(route_map) = route_map;
+ let route_map_name = FrrRouteMapName::new(route_map.id.route_map_id.to_string());
+
+ if let Some(frr_route_map) = config_route_map.get_mut(&route_map_name) {
+ let idx =
+ frr_route_map.partition_point(|element| element.seq <= route_map.id().order());
+ frr_route_map.insert(idx, route_map.into());
+ } else {
+ config_route_map.insert(route_map_name, vec![route_map.into()]);
+ }
+ }
+
+ for (name, entries) in config_route_map {
+ frr_config.routemaps.insert(name, entries);
+ }
+
+ Ok(())
+ }
+}
+
pub mod api {
//! API type for Route Map Entries.
//!
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 15/34] ve-config: add prefix lists integration tests
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (13 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 14/34] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 16/34] ve-config: add route maps " Stefan Hanreich
` (18 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Add full integration test for reading prefix list configuration from a
section config, parsing it and then writing a new FRR configuration
from the section config file.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/tests/prefix_lists/main.rs | 112 +++++++++++++++++++
1 file changed, 112 insertions(+)
create mode 100644 proxmox-ve-config/tests/prefix_lists/main.rs
diff --git a/proxmox-ve-config/tests/prefix_lists/main.rs b/proxmox-ve-config/tests/prefix_lists/main.rs
new file mode 100644
index 0000000..2ed4894
--- /dev/null
+++ b/proxmox-ve-config/tests/prefix_lists/main.rs
@@ -0,0 +1,112 @@
+#![cfg(feature = "frr")]
+
+use proxmox_ve_config::sdn::prefix_list::{frr::build_frr_prefix_lists, *};
+
+use proxmox_frr::ser::{route_map::PrefixListRule as FrrPrefixListRule, FrrConfig};
+
+use proxmox_frr::ser::route_map::{AccessAction, PrefixListName};
+use proxmox_frr::ser::serializer::dump;
+use proxmox_network_types::Cidr;
+use proxmox_section_config::typed::ApiSectionDataEntry;
+
+#[test]
+fn test_build_prefix_list() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: example-1
+ entries action=permit,prefix=192.0.2.0/24
+ entries action=permit,prefix=192.0.2.0/24,le=32
+ entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
+ entries action=permit,prefix=192.0.2.0/24,ge=24
+ entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+
+prefix-list: example-3
+ entries action=permit,prefix=192.0.2.0/24,seq=333
+ entries action=permit,prefix=198.51.100.0/24,seq=222
+ entries action=permit,prefix=203.0.113.0/24,seq=111
+
+prefix-list: example-2
+ entries action=deny,prefix=192.0.2.0/24,le=25
+ entries action=permit,prefix=192.0.2.0/24
+"#;
+
+ let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_prefix_lists(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+ip prefix-list example-1 permit 192.0.2.0/24
+ip prefix-list example-1 permit 192.0.2.0/24 le 32
+ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
+ip prefix-list example-1 permit 192.0.2.0/24 ge 24
+ip prefix-list example-1 permit 192.0.2.0/24 le 31 ge 24
+!
+ip prefix-list example-2 deny 192.0.2.0/24 le 25
+ip prefix-list example-2 permit 192.0.2.0/24
+!
+ip prefix-list example-3 seq 333 permit 192.0.2.0/24
+ip prefix-list example-3 seq 222 permit 198.51.100.0/24
+ip prefix-list example-3 seq 111 permit 203.0.113.0/24
+"#
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_build_prefix_list_overwrite() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: example-1
+ entries action=permit,prefix=192.0.2.0/24
+"#;
+
+ let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+
+ let example_1_prefix_list = vec![FrrPrefixListRule {
+ action: AccessAction::Deny,
+ network: Cidr::new_v4([198, 51, 100, 0], 24).unwrap(),
+ seq: None,
+ le: None,
+ ge: None,
+ is_ipv6: false,
+ }];
+
+ let mut frr_config = FrrConfig::default();
+
+ frr_config.prefix_lists.insert(
+ PrefixListName::new("example-1".to_string()),
+ example_1_prefix_list.clone(),
+ );
+
+ build_frr_prefix_lists(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ let new_prefix_list = frr_config
+ .prefix_lists
+ .get(&PrefixListName::new("example-1".to_string()))
+ .expect("'example-1' prefix list exists");
+
+ assert_ne!(&example_1_prefix_list, new_prefix_list);
+
+ let generated_frr_config = dump(&frr_config)?;
+
+ assert_eq!(
+ generated_frr_config,
+ r#"!
+ip prefix-list example-1 permit 192.0.2.0/24
+"#
+ );
+
+ Ok(())
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-ve-rs v2 16/34] ve-config: add route maps integration tests
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (14 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 15/34] ve-config: add prefix lists integration tests Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 17/34] pve-rs: sdn: add route maps module Stefan Hanreich
` (17 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Add full integration test for reading route maps configuration from a
section config, parsing it and then writing a new FRR configuration
from the section config file.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/tests/route_maps/main.rs | 146 +++++++++++++++++++++
1 file changed, 146 insertions(+)
create mode 100644 proxmox-ve-config/tests/route_maps/main.rs
diff --git a/proxmox-ve-config/tests/route_maps/main.rs b/proxmox-ve-config/tests/route_maps/main.rs
new file mode 100644
index 0000000..081dfa5
--- /dev/null
+++ b/proxmox-ve-config/tests/route_maps/main.rs
@@ -0,0 +1,146 @@
+#![cfg(feature = "frr")]
+
+use proxmox_ve_config::sdn::route_map::{frr::build_frr_route_maps, *};
+
+use proxmox_frr::ser::{serializer::dump, FrrConfig};
+use proxmox_section_config::typed::ApiSectionDataEntry;
+
+#[test]
+fn test_build_route_map_order() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: another_20
+ action deny
+
+route-map-entry: another_50
+ action deny
+
+route-map-entry: another_60
+ action deny
+
+route-map-entry: another_40
+ action deny
+
+route-map-entry: another_30
+ action deny
+"#;
+
+ let config = RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_route_maps(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+route-map another deny 20
+exit
+!
+route-map another deny 30
+exit
+!
+route-map another deny 40
+exit
+!
+route-map another deny 50
+exit
+!
+route-map another deny 60
+exit
+"#
+ );
+
+ Ok(())
+}
+
+#[test]
+fn test_build_route_map() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: another_67
+ action deny
+ match key=vni,value=313373
+ match key=peer,value=some_peergroup
+
+route-map-entry: example_122
+ action deny
+ match key=route-type,value=es
+ match key=vni,value=313373
+ match key=ip-address-prefix-list,value=some_prefix_list
+ match key=ip-next-hop-prefix-list,value=some_other_prefix_list
+ match key=ip-next-hop-address,value=192.0.2.45
+ match key=metric,value=8347
+ match key=local-preference,value=8347
+ match key=peer,value=some_interface
+ match key=peer,value=some_peergroup
+ set key=ip6-next-hop-peer-address
+ set key=ip6-next-hop-prefer-global
+ set key=ip6-next-hop,value=2001:DB8::1
+
+route-map-entry: example_123
+ action permit
+ match key=ip6-address-prefix-list,value=some_prefix_list
+ match key=ip6-next-hop-prefix-list,value=some_other_prefix_list
+ match key=ip6-next-hop-address,value=2001:DB8:cafe::BeeF
+ set key=ip-next-hop-peer-address
+ set key=ip-next-hop-unchanged
+ set key=ip-next-hop,value=198.51.100.3
+ set key=local-preference,value=1234
+ set key=tag,value=untagged
+ set key=weight,value=20
+ set key=metric,value=+rtt
+"#;
+
+ let config = RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_route_maps(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+route-map another deny 67
+ match evpn vni 313373
+ match peer some_peergroup
+exit
+!
+route-map example deny 122
+ match evpn route-type es
+ match evpn vni 313373
+ match ip address prefix-list some_prefix_list
+ match ip next-hop prefix-list some_other_prefix_list
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+ match peer some_interface
+ match peer some_peergroup
+ set ipv6 next-hop peer-address
+ set ipv6 next-hop prefer-global
+ set ipv6 next-hop global 2001:db8::1
+exit
+!
+route-map example permit 123
+ match ipv6 address prefix-list some_prefix_list
+ match ipv6 next-hop prefix-list some_other_prefix_list
+ match ipv6 next-hop address 2001:db8:cafe::beef
+ set ip next-hop peer-address
+ set ip next-hop unchanged
+ set ip next-hop 198.51.100.3
+ set local-preference 1234
+ set tag untagged
+ set weight 20
+ set metric +rtt
+exit
+"#
+ );
+
+ Ok(())
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-perl-rs v2 17/34] pve-rs: sdn: add route maps module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (15 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 16/34] ve-config: add route maps " Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 18/34] pve-rs: sdn: add prefix lists module Stefan Hanreich
` (16 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Exposes the functionality from ve-config to Perl by providing helpers
for instantiating the Rust configuration from Perl. The module also
contains the implementation for the CRUD API methods, which will be
used in the API methods in pve-network.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/mod.rs | 3 +-
pve-rs/src/bindings/sdn/route_maps.rs | 262 ++++++++++++++++++++++++++
4 files changed, 266 insertions(+), 1 deletion(-)
create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 61be324..c97e1d5 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] }
proxmox-oci = "0.2.1"
proxmox-openid = "1.0.2"
proxmox-resource-scheduling = "1.0.1"
+proxmox-schema = "5"
proxmox-section-config = "3"
proxmox-shared-cache = "1"
proxmox-subscription = "1"
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 3bbc464..d662b00 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -31,6 +31,7 @@ PERLMOD_PACKAGES := \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::SDN::Fabrics \
+ PVE::RS::SDN::RouteMaps \
PVE::RS::SDN \
PVE::RS::TFA
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index fde3138..c571d28 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,4 +1,5 @@
pub(crate) mod fabrics;
+pub(crate) mod route_maps;
#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
pub mod pve_rs_sdn {
@@ -7,7 +8,7 @@ pub mod pve_rs_sdn {
//! This provides general methods for generating the frr config.
use anyhow::Error;
- use proxmox_frr::ser::{FrrConfig, serializer::to_raw_config};
+ use proxmox_frr::ser::{serializer::to_raw_config, FrrConfig};
use proxmox_ve_config::common::valid::Validatable;
use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
diff --git a/pve-rs/src/bindings/sdn/route_maps.rs b/pve-rs/src/bindings/sdn/route_maps.rs
new file mode 100644
index 0000000..efac7e1
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/route_maps.rs
@@ -0,0 +1,262 @@
+#[perlmod::package(name = "PVE::RS::SDN::RouteMaps", lib = "pve_rs")]
+pub mod pve_rs_sdn_route_maps {
+ //! The `PVE::RS::SDN::RouteMaps` package.
+
+ use std::collections::hash_map::Entry;
+ use std::collections::HashMap;
+ use std::ops::Deref;
+ use std::sync::Mutex;
+
+ use anyhow::{anyhow, Error};
+ use openssl::hash::{hash, MessageDigest};
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+
+ use proxmox_schema::Updater;
+ use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+ use proxmox_ve_config::sdn::route_map::api::RouteMapDeletableProperties;
+ use proxmox_ve_config::sdn::route_map::api::RouteMapEntry as ApiRouteMap;
+ use proxmox_ve_config::sdn::route_map::api::RouteMapEntryUpdater;
+ use proxmox_ve_config::sdn::route_map::RouteMap as ConfigRouteMap;
+ use proxmox_ve_config::sdn::route_map::RouteMapEntryId;
+ use proxmox_ve_config::sdn::route_map::RouteMapId;
+
+ /// A SDN RouteMap config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlRouteMapConfig {
+ /// The route map config instance
+ pub route_maps: Mutex<HashMap<String, ConfigRouteMap>>,
+ }
+
+ perlmod::declare_magic!(Box<PerlRouteMapConfig> : &PerlRouteMapConfig as "PVE::RS::SDN::RouteMaps::Config");
+
+ /// Class method: Parse the raw configuration from `/etc/pve/sdn/route-maps.cfg`.
+ #[export]
+ pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
+ let raw_config = std::str::from_utf8(raw_config)?;
+ let config = ConfigRouteMap::parse_section_config("route-maps.cfg", raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
+ route_maps: Mutex::new(config.deref().clone()),
+ })),
+ )
+ }
+
+ /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`.
+ #[export]
+ pub fn running_config(
+ #[raw] class: Value,
+ route_maps: HashMap<String, ConfigRouteMap>,
+ ) -> Result<perlmod::Value, Error> {
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
+ route_maps: Mutex::new(route_maps.clone()),
+ })),
+ )
+ }
+
+ /// Method: Used for writing the running configuration.
+ #[export]
+ pub fn to_sections(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ ) -> Result<HashMap<String, ConfigRouteMap>, Error> {
+ let config = this.route_maps.lock().unwrap();
+ Ok(config.deref().clone())
+ }
+
+ /// Method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/sdn/route-maps.cfg`
+ #[export]
+ pub fn to_raw(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
+ let config = this.route_maps.lock().unwrap();
+ let route_maps: SectionConfigData<ConfigRouteMap> =
+ SectionConfigData::from_iter(config.deref().clone());
+
+ ConfigRouteMap::write_section_config("route-maps.cfg", &route_maps)
+ }
+
+ /// Method: Generate a digest for the whole configuration.
+ #[export]
+ pub fn digest(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
+ let config = to_raw(this)?;
+ let hash = hash(MessageDigest::sha256(), config.as_bytes())?;
+
+ Ok(hex::encode(hash))
+ }
+
+ /// Method: Returns all route map entries as a hash indexed with the IDs of the entries.
+ #[export]
+ pub fn list(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ ) -> Result<HashMap<String, ApiRouteMap>, Error> {
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|(id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+ (id.clone(), route_map.clone().into())
+ })
+ .collect())
+ }
+
+ /// Method: Returns all entries of a given route map as a hash indexed with the IDs of the
+ /// entries.
+ #[export]
+ pub fn list_route_map(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ ) -> Result<HashMap<String, ApiRouteMap>, Error> {
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .iter()
+ .filter_map(|(id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+
+ if route_map.id().route_map_id() == &route_map_id {
+ return Some((id.clone(), route_map.clone().into()));
+ }
+
+ None
+ })
+ .collect())
+ }
+
+ /// Method: Create a new RouteMap entry.
+ #[export]
+ pub fn create(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map: ApiRouteMap,
+ ) -> Result<(), Error> {
+ let mut route_maps = this.route_maps.lock().unwrap();
+
+ let id =
+ RouteMapEntryId::new(route_map.route_map_id().clone(), route_map.order()).to_string();
+
+ match route_maps.entry(id) {
+ Entry::Occupied(entry) => {
+ anyhow::bail!(
+ "route map entry already exists in configuration: {}",
+ entry.key()
+ )
+ }
+ Entry::Vacant(vacancy) => {
+ vacancy.insert(ConfigRouteMap::RouteMapEntry(route_map.into()))
+ }
+ };
+
+ Ok(())
+ }
+
+ /// Method: Returns a specfic entry of a RouteMap.
+ #[export]
+ pub fn get(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u16,
+ ) -> Result<Option<ApiRouteMap>, Error> {
+ let id = RouteMapEntryId::new(route_map_id, order).to_string();
+
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .get(&id)
+ .map(|route_map_entry| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+ route_map.clone().into()
+ }))
+ }
+
+ /// Method: Update a RouteMap entry.
+ #[export]
+ pub fn update(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u16,
+ updater: RouteMapEntryUpdater,
+ delete: Option<Vec<RouteMapDeletableProperties>>,
+ ) -> Result<(), Error> {
+ if updater.is_empty() && delete.is_empty() {
+ return Ok(());
+ }
+
+ let mut route_maps = this.route_maps.lock().unwrap();
+ let id = RouteMapEntryId::new(route_map_id, order).to_string();
+
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_maps
+ .get_mut(&id)
+ .ok_or_else(|| anyhow!("Could not find route map with id: {}", id))?;
+
+ let RouteMapEntryUpdater {
+ action,
+ set_actions,
+ match_actions,
+ exit_action,
+ call,
+ } = updater;
+
+ if let Some(action) = action {
+ route_map.set_action(action);
+ }
+
+ if let Some(match_actions) = match_actions {
+ route_map.set_match_actions(match_actions);
+ }
+
+ if let Some(set_actions) = set_actions {
+ route_map.set_set_actions(set_actions);
+ }
+
+ if exit_action.is_some() {
+ route_map.set_exit_action(exit_action);
+ }
+
+ if call.is_some() {
+ route_map.set_call(call);
+ }
+
+ for deletable_property in delete.unwrap_or_default() {
+ match deletable_property {
+ RouteMapDeletableProperties::Set => {
+ route_map.set_set_actions(Vec::new());
+ }
+ RouteMapDeletableProperties::Match => {
+ route_map.set_match_actions(Vec::new());
+ }
+ RouteMapDeletableProperties::ExitAction => {
+ route_map.set_exit_action(None);
+ }
+ RouteMapDeletableProperties::Call => {
+ route_map.set_call(None);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Method: Delete an entry in a RouteMap.
+ #[export]
+ pub fn delete(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u16,
+ ) -> Result<(), Error> {
+ let id = RouteMapEntryId::new(route_map_id, order).to_string();
+
+ this.route_maps
+ .lock()
+ .unwrap()
+ .remove(&id.to_string())
+ .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?;
+
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-perl-rs v2 18/34] pve-rs: sdn: add prefix lists module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (16 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 17/34] pve-rs: sdn: add route maps module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 19/34] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
` (15 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Exposes the functionality from ve-config to Perl by providing helpers
for instantiating the Rust configuration from Perl. The module also
contains the implementation for the CRUD API methods, which will be
used for implementing the API methods in pve-network.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/mod.rs | 1 +
pve-rs/src/bindings/sdn/prefix_lists.rs | 192 ++++++++++++++++++++++++
3 files changed, 194 insertions(+)
create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index d662b00..e7dfe2e 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -31,6 +31,7 @@ PERLMOD_PACKAGES := \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::SDN::Fabrics \
+ PVE::RS::SDN::PrefixLists \
PVE::RS::SDN::RouteMaps \
PVE::RS::SDN \
PVE::RS::TFA
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index c571d28..9bddf1c 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,4 +1,5 @@
pub(crate) mod fabrics;
+pub(crate) mod prefix_lists;
pub(crate) mod route_maps;
#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
diff --git a/pve-rs/src/bindings/sdn/prefix_lists.rs b/pve-rs/src/bindings/sdn/prefix_lists.rs
new file mode 100644
index 0000000..e3d7598
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/prefix_lists.rs
@@ -0,0 +1,192 @@
+#[perlmod::package(name = "PVE::RS::SDN::PrefixLists", lib = "pve_rs")]
+pub mod pve_rs_sdn_prefix_lists {
+ //! The `PVE::RS::SDN::PrefixLists` package.
+ //!
+ //! 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 core::clone::Clone;
+ use std::collections::hash_map::Entry;
+ use std::collections::HashMap;
+ use std::ops::Deref;
+ use std::sync::Mutex;
+
+ use anyhow::{anyhow, Error};
+ use openssl::hash::{hash, MessageDigest};
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+ use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+ use proxmox_ve_config::sdn::prefix_list::api::{
+ PrefixList as ApiPrefixList, PrefixListDeletableProperties, PrefixListUpdater,
+ };
+ use proxmox_ve_config::sdn::prefix_list::{PrefixList as ConfigPrefixList, PrefixListId};
+
+ /// A SDN PrefixList config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlPrefixListConfig {
+ /// The fabric config instance
+ pub prefix_lists: Mutex<HashMap<String, ConfigPrefixList>>,
+ }
+
+ perlmod::declare_magic!(Box<PerlPrefixListConfig> : &PerlPrefixListConfig as "PVE::RS::SDN::PrefixLists::Config");
+
+ /// Class method: Parse the raw configuration from `/etc/pve/sdn/prefix-lists.cfg`.
+ #[export]
+ pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
+ let raw_config = std::str::from_utf8(raw_config)?;
+ let config = ConfigPrefixList::parse_section_config("prefix-lists.cfg", raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
+ prefix_lists: Mutex::new(config.deref().clone()),
+ })),
+ )
+ }
+
+ /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`.
+ #[export]
+ pub fn running_config(
+ #[raw] class: Value,
+ prefix_lists: HashMap<String, ConfigPrefixList>,
+ ) -> Result<perlmod::Value, Error> {
+ let prefix_lists: SectionConfigData<ConfigPrefixList> =
+ SectionConfigData::from_iter(prefix_lists);
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
+ prefix_lists: Mutex::new(prefix_lists.deref().clone()),
+ })),
+ )
+ }
+
+ /// Method: Used for writing the running configuration.
+ #[export]
+ pub fn to_sections(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ ) -> Result<HashMap<String, ConfigPrefixList>, Error> {
+ let config = this.prefix_lists.lock().unwrap();
+ Ok(config.deref().clone())
+ }
+
+ /// Method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/sdn/prefix-lists.cfg`
+ #[export]
+ pub fn to_raw(#[try_from_ref] this: &PerlPrefixListConfig) -> Result<String, Error> {
+ let config = this.prefix_lists.lock().unwrap();
+
+ let prefix_lists: SectionConfigData<ConfigPrefixList> =
+ SectionConfigData::from_iter(config.deref().clone());
+
+ ConfigPrefixList::write_section_config("prefix-lists.cfg", &prefix_lists)
+ }
+
+ /// Method: Generate a digest for the whole configuration
+ #[export]
+ pub fn digest(#[try_from_ref] this: &PerlPrefixListConfig) -> Result<String, Error> {
+ let config = to_raw(this)?;
+ let hash = hash(MessageDigest::sha256(), config.as_bytes())?;
+
+ Ok(hex::encode(hash))
+ }
+
+ /// Method: Returns all prefix lists as a hash indexed with the IDs of the prefix lists.
+ #[export]
+ pub fn list(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ ) -> Result<HashMap<String, ApiPrefixList>, Error> {
+ Ok(this
+ .prefix_lists
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|(id, prefix_list)| {
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+ (id.clone(), prefix_list.clone())
+ })
+ .collect())
+ }
+
+ /// Method: Create a new PrefixList.
+ #[export]
+ pub fn create(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ prefix_list: ApiPrefixList,
+ ) -> Result<(), Error> {
+ let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+ match prefix_lists.entry(prefix_list.id().to_string()) {
+ Entry::Occupied(_) => anyhow::bail!(
+ "prefix list already exists in configuration: {}",
+ prefix_list.id()
+ ),
+ Entry::Vacant(vacancy) => vacancy.insert(ConfigPrefixList::PrefixList(prefix_list)),
+ };
+
+ Ok(())
+ }
+
+ /// Method: Get a specific PrefixList.
+ #[export]
+ pub fn get(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ ) -> Result<Option<ApiPrefixList>, Error> {
+ Ok(this
+ .prefix_lists
+ .lock()
+ .unwrap()
+ .get(&id.to_string())
+ .map(|prefix_list| {
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+ prefix_list.clone()
+ }))
+ }
+
+ /// Method: Update a PrefixList.
+ #[export]
+ pub fn update(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ updater: PrefixListUpdater,
+ delete: Option<Vec<PrefixListDeletableProperties>>,
+ ) -> Result<(), Error> {
+ let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_lists
+ .get_mut(id.as_str())
+ .ok_or_else(|| anyhow!("Could not find prefix list with id: {}", id))?;
+
+ let PrefixListUpdater { entries } = updater;
+
+ if let Some(entries) = entries {
+ prefix_list.entries = entries;
+ }
+
+ for deletable_property in delete.unwrap_or_default() {
+ match deletable_property {
+ PrefixListDeletableProperties::Entries => {
+ prefix_list.entries = Vec::new();
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Method: Delete a PrefixList.
+ #[export]
+ pub fn delete(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ ) -> Result<(), Error> {
+ this.prefix_lists
+ .lock()
+ .unwrap()
+ .remove(&id.to_string())
+ .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?;
+
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH proxmox-perl-rs v2 19/34] sdn: add prefix list / route maps to frr config generation helper
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (17 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 18/34] pve-rs: sdn: add prefix lists module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 20/34] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
` (14 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
With the addition of prefix lists and route maps, the helper needs to
be adapted so the FRR configuration for prefix lists and route maps
gets generated as well. Prefix lists and route maps are generated
before generating the fabric configuration, so it is possible for the
fabric config generation logic to check for the existence of prefix
lists and route maps. It also ensures that
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/mod.rs | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index 9bddf1c..c6361c3 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -15,17 +15,39 @@ pub mod pve_rs_sdn {
use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
use crate::bindings::pve_rs_sdn_fabrics::PerlFabricConfig;
+ use crate::bindings::sdn::prefix_lists::pve_rs_sdn_prefix_lists::PerlPrefixListConfig;
+ use crate::bindings::sdn::route_maps::pve_rs_sdn_route_maps::PerlRouteMapConfig;
/// Return the FRR configuration for the passed FrrConfig and the FabricsConfig as an array of
/// strings, where each line represents a line in the FRR configuration.
#[export]
pub fn get_frr_raw_config(
mut frr_config: FrrConfig,
- #[try_from_ref] cfg: &PerlFabricConfig,
+ #[try_from_ref] prefix_list_config: &PerlPrefixListConfig,
+ #[try_from_ref] route_map_config: &PerlRouteMapConfig,
+ #[try_from_ref] fabric_config: &PerlFabricConfig,
node_id: NodeId,
) -> Result<Vec<String>, Error> {
- let fabric_config = cfg.fabric_config.lock().unwrap().clone().into_valid()?;
+ let prefix_list_config = prefix_list_config.prefix_lists.lock().unwrap();
+ proxmox_ve_config::sdn::prefix_list::frr::build_frr_prefix_lists(
+ prefix_list_config.values().cloned(),
+ &mut frr_config,
+ )?;
+
+ let route_map_config = route_map_config.route_maps.lock().unwrap();
+ proxmox_ve_config::sdn::route_map::frr::build_frr_route_maps(
+ route_map_config.values().cloned(),
+ &mut frr_config,
+ )?;
+
+ let fabric_config = fabric_config
+ .fabric_config
+ .lock()
+ .unwrap()
+ .clone()
+ .into_valid()?;
proxmox_ve_config::sdn::fabric::frr::build_fabric(node_id, fabric_config, &mut frr_config)?;
+
to_raw_config(&frr_config)
}
}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 20/34] controller: bgp: evpn: adapt to new match / set frr config syntax
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (18 preceding siblings ...)
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 19/34] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 21/34] sdn: add prefix lists module Stefan Hanreich
` (13 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
proxmox-frr has changed the representation of match / set statements
in its structs. It has changed to generic key / value fields, where
the keys specify the literal name of the key in the FRR configuration.
Adapt the controllers to use the new fields so the configuration gets
generated properly.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 7 +++---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 24 ++++++++-----------
2 files changed, 13 insertions(+), 18 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 8891541..d54c9ec 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -136,13 +136,12 @@ sub generate_frr_config {
$config->{frr}->{protocol_routemaps}->{bgp}->{v4} = "correct_src";
my $routemap_config = {
- protocol_type => 'ip',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'loopbacks_ips' },
+ key => 'ip address prefix-list',
+ value => 'loopbacks_ips',
};
my $routemap = {
matches => [$routemap_config],
- sets => [{ set_type => 'src', value => $ifaceip }],
+ sets => [{ key => 'src', value => $ifaceip }],
action => "permit",
seq => 1,
};
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index d2825f5..3e643b1 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -329,9 +329,8 @@ sub generate_zone_frr_config {
if (!$exitnodes_primary || $exitnodes_primary eq $local_node) {
# Filter default route coming from other exit nodes on primary node
my $routemap_config_v6 = {
- protocol_type => 'ipv6',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+ key => 'ipv6 address prefix-list',
+ value => 'only_default_v6',
};
my $routemap_v6 = { seq => 1, matches => [$routemap_config_v6], action => "deny" };
unshift(
@@ -339,23 +338,21 @@ sub generate_zone_frr_config {
);
my $routemap_config = {
- protocol_type => 'ip',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default' },
+ key => 'ip address prefix-list',
+ value => 'only_default',
};
my $routemap = { seq => 1, matches => [$routemap_config], action => "deny" };
unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_IN'} }, $routemap);
} elsif ($exitnodes_primary ne $local_node) {
my $routemap_config_v6 = {
- protocol_type => 'ipv6',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+ key => 'ipv6 address prefix-list',
+ value => 'only_default_v6',
};
my $routemap_v6 = {
seq => 1,
matches => [$routemap_config_v6],
- sets => [{ set_type => 'metric', value => 200 }],
+ sets => [{ key => 'metric', value => 200 }],
action => "permit",
};
unshift(
@@ -363,14 +360,13 @@ sub generate_zone_frr_config {
);
my $routemap_config = {
- protocol_type => 'ip',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default' },
+ key => 'ip address prefix-list',
+ value => 'only_default',
};
my $routemap = {
seq => 1,
matches => [$routemap_config],
- sets => [{ set_type => 'metric', value => 200 }],
+ sets => [{ key => 'metric', value => 200 }],
action => "permit",
};
unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap);
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 21/34] sdn: add prefix lists module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (19 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 20/34] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 22/34] api2: add prefix list module Stefan Hanreich
` (12 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Defines helpers for common operations (reading / writing
configuration) as well as the required formats / schema definitions
for the route map API.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Makefile | 14 ++-
src/PVE/Network/SDN/PrefixLists.pm | 134 +++++++++++++++++++++++++++++
2 files changed, 147 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index d1ffef9..fa6702e 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,16 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm Frr.pm
+SOURCES=Vnets.pm\
+ VnetPlugin.pm\
+ Zones.pm\
+ Controllers.pm\
+ Subnets.pm\
+ SubnetPlugin.pm\
+ Ipams.pm\
+ Dns.pm\
+ Dhcp.pm\
+ Fabrics.pm\
+ Frr.pm\
+ RouteMaps.pm\
+ PrefixLists.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/PrefixLists.pm b/src/PVE/Network/SDN/PrefixLists.pm
new file mode 100644
index 0000000..b59cc7f
--- /dev/null
+++ b/src/PVE/Network/SDN/PrefixLists.pm
@@ -0,0 +1,134 @@
+package PVE::Network::SDN::PrefixLists;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::INotify;
+use PVE::Network::SDN;
+use PVE::RS::SDN::PrefixLists;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-prefix-list-id',
+ sub {
+ my ($id, $noerr) = @_;
+
+ if ($id =~ m/^(only_default|only_default_v6|loopbacks_ips)$/) {
+ return undef if $noerr;
+ die "prefix list ID '$id' is currently reserved and cannot be used\n";
+ }
+
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,30}[a-zA-Z0-9]?$/i) {
+ return undef if $noerr;
+ die "prefix list ID '$id' contains illegal characters\n";
+ }
+
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-prefix-list-id',
+ {
+ description => "The SDN prefix list identifier",
+ type => 'string',
+ format => 'pve-sdn-prefix-list-id',
+ },
+);
+
+cfs_register_file(
+ 'sdn/prefix-lists.cfg', \&parse_prefix_lists_config, \&write_prefix_lists_config,
+);
+
+sub parse_prefix_lists_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_prefix_lists_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # if the config hasn't yet been applied after the introduction of
+ # prefix lists then the key does not exist in the running config so we
+ # default to an empty hash
+ my $prefix_lists_config = $running_config->{'prefix-lists'}->{ids} // {};
+ return PVE::RS::SDN::PrefixLists->running_config($prefix_lists_config);
+ }
+
+ my $prefix_lists_config = cfs_read_file("sdn/prefix-lists.cfg");
+ return PVE::RS::SDN::PrefixLists->config($prefix_lists_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/prefix-lists.cfg", $config->to_raw(), 1);
+}
+
+sub prefix_list_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ digest => get_standard_option('pve-config-digest'),
+ entries => {
+ type => 'array',
+ optional => $update,
+ items => {
+ type => 'string',
+ format => {
+ action => {
+ type => 'string',
+ enum => ['permit', 'deny'],
+ },
+ prefix => {
+ type => 'string',
+ format => 'CIDR',
+ },
+ le => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 128,
+ optional => 1,
+ },
+ ge => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 128,
+ optional => 1,
+ },
+ seq => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 2**32 - 1,
+ optional => 1,
+ },
+ },
+ },
+ },
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['entries'],
+ },
+ };
+ } else {
+ $properties->{id} = get_standard_option('pve-sdn-prefix-list-id');
+ }
+
+ return $properties;
+}
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 22/34] api2: add prefix list module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (20 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 21/34] sdn: add prefix lists module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 23/34] sdn: add route map module Stefan Hanreich
` (11 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Contains the CRUD functionality for prefix lists:
GET /prefix-lists - lists all prefix lists
GET /prefix-lists/<id> - get prefix list <id>
POST /prefix-lists - create a new prefix list
PUT /prefix-lists/<id> - update prefix list <id>
DELETE /prefix-lists/<id> - delete prefix list <id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 7 +
src/PVE/API2/Network/SDN/Makefile | 11 +-
src/PVE/API2/Network/SDN/PrefixLists.pm | 254 ++++++++++++++++++++++++
3 files changed, 270 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index cc5ac25..778a29b 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -22,6 +22,7 @@ use PVE::API2::Network::SDN::Zones;
use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::PrefixLists;
use base qw(PVE::RESTHandler);
@@ -55,6 +56,11 @@ __PACKAGE__->register_method({
path => 'fabrics',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::PrefixLists",
+ path => 'prefix-lists',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -87,6 +93,7 @@ __PACKAGE__->register_method({
{ id => 'ipams' },
{ id => 'dns' },
{ id => 'fabrics' },
+ { id => 'prefix-lists' },
];
return $res;
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 2624d9a..4349c17 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -1,5 +1,12 @@
-SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm Fabrics.pm
-
+SOURCES=Vnets.pm\
+ Zones.pm\
+ Controllers.pm\
+ Subnets.pm\
+ Ipams.pm\
+ Dns.pm\
+ Ips.pm\
+ Fabrics.pm\
+ PrefixLists.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/PrefixLists.pm b/src/PVE/API2/Network/SDN/PrefixLists.pm
new file mode 100644
index 0000000..00a6d14
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/PrefixLists.pm
@@ -0,0 +1,254 @@
+package PVE::API2::Network::SDN::PrefixLists;
+
+use strict;
+use warnings;
+
+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);
+
+__PACKAGE__->register_method({
+ name => 'list_prefix_lists',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description =>
+ "Only returns prefix list entries where you have 'Sys.Audit' or 'Sys.Modify' permissions.",
+ user => 'all',
+ },
+ description => "List Prefix Lists",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {},
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $prefix_lists;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::PrefixLists::config();
+ my $running_config = PVE::Network::SDN::PrefixLists::config(1);
+
+ my $pending_prefix_lists = PVE::Network::SDN::pending_config(
+ { 'prefix-lists' => { ids => $running_config->list() } },
+ { ids => $current_config->list() },
+ 'prefix-lists',
+ );
+
+ $digest = $current_config->digest();
+ $prefix_lists = $pending_prefix_lists->{ids};
+ } elsif ($running) {
+ $prefix_lists = PVE::Network::SDN::PrefixLists::config(1)->list();
+ } else {
+ my $current_config = PVE::Network::SDN::PrefixLists::config();
+
+ $digest = $current_config->digest();
+ $prefix_lists = $current_config->list();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $prefix_list_privs = ['SDN.Audit'];
+
+ my @res;
+ for my $prefix_list_id (sort keys $prefix_lists->%*) {
+ next
+ if !$rpcenv->check_any(
+ $authuser,
+ "/prefix-lists/$prefix_list_id",
+ $prefix_list_privs,
+ 1,
+ );
+ $prefix_lists->{$prefix_list_id}->{digest} = $digest if $digest;
+ push @res, $prefix_lists->{$prefix_list_id};
+ }
+
+ return \@res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'get_prefix_list_entry',
+ path => '{id}',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
+ },
+ description => "Get Prefix List",
+ parameters => {
+ properties => {
+ id => get_standard_option('pve-sdn-prefix-list-id'),
+ },
+ },
+ returns => {
+ type => "object",
+ properties => {},
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $prefix_list_id = extract_param($param, 'id');
+ my $prefix_list_entry = PVE::Network::SDN::PrefixLists::config()->get($prefix_list_id);
+
+ raise_param_exc({ 'id' => "$prefix_list_id doesn't exist" })
+ if !$prefix_list_entry;
+
+ return $prefix_list_entry;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create_prefix_list_entry',
+ path => '',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists', ['SDN.Allocate']],
+ },
+ description => "Create Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::PrefixLists::prefix_list_properties(0)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->create($param);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "creating prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_prefix_list_entry',
+ path => '{id}',
+ method => 'PUT',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+ },
+ description => "Update Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::PrefixLists::prefix_list_properties(1)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $prefix_list_id = extract_param($param, 'id');
+ my $delete = extract_param($param, 'delete');
+
+ $config->update($prefix_list_id, $param, $delete);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "updating prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_prefix_list_entry',
+ path => '{id}',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+ },
+ description => "Delete Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ id => get_standard_option('pve-sdn-prefix-list-id'),
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $prefix_list_id = extract_param($param, 'id');
+
+ $config->delete($prefix_list_id);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "deleting prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 23/34] sdn: add route map module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (21 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 22/34] api2: add prefix list module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 24/34] api2: add route maps api module Stefan Hanreich
` (10 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Defines helpers for common operations (reading / writing
configuration) as well as the required formats / schema definitions
for the route map API.
The Route Map ID format currently rejects all IDs that could be
auto-generated by PVE entities, to prevent accidental overrides of
built-in route maps. Instead of re-defining route maps, users can
create a new custom route map and select that in the EVPN / BGP
controller, if they want to override the auto-generated route map.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Makefile | 4 +-
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 8 +
src/PVE/Network/SDN/RouteMaps.pm | 192 ++++++++++++++++++++
3 files changed, 203 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 4349c17..770eef2 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -6,7 +6,8 @@ SOURCES=Vnets.pm\
Dns.pm\
Ips.pm\
Fabrics.pm\
- PrefixLists.pm
+ PrefixLists.pm\
+ RouteMaps.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
@@ -15,4 +16,5 @@ install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
make -C Fabrics install
make -C Nodes install
+ make -C RouteMaps install
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/Makefile b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
new file mode 100644
index 0000000..3d0a928
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
@@ -0,0 +1,8 @@
+SOURCES=RouteMap.pm\
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/RouteMaps/$$i; done
diff --git a/src/PVE/Network/SDN/RouteMaps.pm b/src/PVE/Network/SDN/RouteMaps.pm
new file mode 100644
index 0000000..bc4a562
--- /dev/null
+++ b/src/PVE/Network/SDN/RouteMaps.pm
@@ -0,0 +1,192 @@
+package PVE::Network::SDN::RouteMaps;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::INotify;
+use PVE::Network::SDN;
+use PVE::RS::SDN::RouteMaps;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-route-map-id',
+ sub {
+ my ($id, $noerr) = @_;
+
+ if ($id =~ m/^(pve_.*|MAP_VTEP_IN|MAP_VTEP_OUT|correct_src)$/) {
+ return undef if $noerr;
+ die "route map ID '$id' is currently reserved and cannot be used\n";
+ }
+
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,30}[a-zA-Z0-9]?$/i) {
+ return undef if $noerr;
+ die "route map ID '$id' contains illegal characters\n";
+ }
+
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-route-map-id',
+ {
+ description => "The SDN route map identifier",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-route-map-order',
+ {
+ description => 'The index of this route map entry',
+ type => 'integer',
+ minimum => 0,
+ maximum => 2**32 - 1,
+ },
+);
+
+cfs_register_file(
+ 'sdn/route-maps.cfg', \&parse_route_maps_config, \&write_route_maps_config,
+);
+
+sub parse_route_maps_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_route_maps_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # if the config hasn't yet been applied after the introduction of
+ # route maps then the key does not exist in the running config so we
+ # default to an empty hash
+ my $route_maps_config = $running_config->{'route-maps'}->{ids} // {};
+ return PVE::RS::SDN::RouteMaps->running_config($route_maps_config);
+ }
+
+ my $route_map_config = cfs_read_file("sdn/route-maps.cfg");
+ return PVE::RS::SDN::RouteMaps->config($route_map_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/route-maps.cfg", $config->to_raw(), 1);
+}
+
+sub route_map_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ digest => get_standard_option('pve-config-digest'),
+ action => {
+ description => 'Matching policy of a route map entry.',
+ type => 'string',
+ enum => ['permit', 'deny'],
+ optional => $update,
+ },
+ set => {
+ type => 'array',
+ items => {
+ type => 'string',
+ format => {
+ key => {
+ type => 'string',
+ enum => [
+ 'ip-next-hop-peer-address',
+ 'ip-next-hop',
+ 'ip-next-hop-unchanged',
+ 'ip6-next-hop-peer-address',
+ 'ip6-next-hop-prefer-global',
+ 'ip6-next-hop',
+ 'local-preference',
+ 'tag',
+ 'weight',
+ 'metric',
+ 'src',
+ ],
+ },
+ value => {
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ optional => 1,
+ },
+ match => {
+ type => 'array',
+ items => {
+ type => 'string',
+ format => {
+ key => {
+ type => 'string',
+ enum => [
+ 'route-type',
+ 'vni',
+ 'ip-address-prefix-list',
+ 'ip6-address-prefix-list',
+ 'ip-next-hop-prefix-list',
+ 'ip6-next-hop-prefix-list',
+ 'ip-next-hop-address',
+ 'ip6-next-hop-address',
+ 'metric',
+ 'local-preference',
+ 'peer',
+ ],
+ },
+ value => {
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ optional => 1,
+ },
+ 'exit-action' => {
+ type => 'string',
+ format => {
+ key => {
+ type => 'string',
+ enum => [
+ 'on-match-goto', 'on-match-next', 'continue',
+ ],
+ },
+ value => {
+ type => 'string',
+ optional => 1,
+ },
+ },
+ optional => 1,
+ },
+ call => get_standard_option('pve-sdn-route-map-id', {
+ optional => 1,
+ }),
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['set', 'match', 'call', 'exit-action'],
+ },
+ };
+ }
+
+ return $properties;
+}
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 24/34] api2: add route maps api module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (22 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 23/34] sdn: add route map module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 25/34] api2: add route map module Stefan Hanreich
` (9 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Contains the following API endpoints:
GET /route-maps - lists all route map entries
POST /route-maps - creates a new route map entry
The following commits contain the API modules for accessing specific
route maps / route map entries.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 7 ++
src/PVE/API2/Network/SDN/RouteMaps.pm | 134 ++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 778a29b..ef64df2 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -23,6 +23,7 @@ use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
use PVE::API2::Network::SDN::PrefixLists;
+use PVE::API2::Network::SDN::RouteMaps;
use base qw(PVE::RESTHandler);
@@ -61,6 +62,11 @@ __PACKAGE__->register_method({
path => 'prefix-lists',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps",
+ path => 'route-maps',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -94,6 +100,7 @@ __PACKAGE__->register_method({
{ id => 'dns' },
{ id => 'fabrics' },
{ id => 'prefix-lists' },
+ { id => 'route-maps' },
];
return $res;
diff --git a/src/PVE/API2/Network/SDN/RouteMaps.pm b/src/PVE/API2/Network/SDN/RouteMaps.pm
new file mode 100644
index 0000000..8fa8c71
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps.pm
@@ -0,0 +1,134 @@
+package PVE::API2::Network::SDN::RouteMaps;
+
+use strict;
+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;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'list_route_maps',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description =>
+ "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
+ user => 'all',
+ },
+ description => "List Route Maps",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ links => [{ rel => 'child', href => "{route-map-id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $route_maps;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+ my $running_config = PVE::Network::SDN::RouteMaps::config(1);
+
+ my $pending_route_maps = PVE::Network::SDN::pending_config(
+ { 'route-maps' => { ids => $running_config->list() } },
+ { ids => $current_config->list() },
+ 'route-maps',
+ );
+
+ $digest = $current_config->digest();
+ $route_maps = $pending_route_maps->{ids};
+ } elsif ($running) {
+ $route_maps = PVE::Network::SDN::RouteMaps::config(1)->list();
+ } else {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+
+ $digest = $current_config->digest();
+ $route_maps = $current_config->list();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $route_map_privs = ['SDN.Audit', 'SDN.Allocate'];
+
+ my @res;
+ for my $route_map_id (sort keys $route_maps->%*) {
+ next
+ if !$rpcenv->check_any($authuser, "/sdn/route-maps/$route_map_id",
+ $route_map_privs, 1);
+ $route_maps->{$route_map_id}->{digest} = $digest if $digest;
+ push @res, $route_maps->{$route_map_id};
+ }
+
+ return \@res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create_route_map_entry',
+ path => '',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/sdn/route-maps', ['SDN.Allocate']],
+ },
+ description => "Create Route Map entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::RouteMaps::route_map_properties(0)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->create($param);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "creating route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 25/34] api2: add route map module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (23 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 24/34] api2: add route maps api module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 26/34] api2: add route map entry module Stefan Hanreich
` (8 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
This module contains the following API endpoint:
GET /route-maps/<id> - lists all route map entries for the route map
<id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/RouteMaps.pm | 6 ++
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 87 +++++++++++++++++++
2 files changed, 93 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
diff --git a/src/PVE/API2/Network/SDN/RouteMaps.pm b/src/PVE/API2/Network/SDN/RouteMaps.pm
index 8fa8c71..8c3a0b8 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::RouteMaps;
use strict;
use warnings;
+use PVE::API2::Network::SDN::RouteMaps::RouteMap;
use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Network::SDN::RouteMaps;
@@ -11,6 +12,11 @@ use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMap",
+ path => '{route-map-id}',
+});
+
__PACKAGE__->register_method({
name => 'list_route_maps',
path => '',
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
new file mode 100644
index 0000000..2dbea09
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
@@ -0,0 +1,87 @@
+package PVE::API2::Network::SDN::RouteMaps::RouteMap;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Exception qw(raise_param_exc);
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'list_route_map_entries',
+ path => '',
+ method => 'GET',
+ permissions => {
+ check =>
+ ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Audit', 'SDN.Allocate'], any => 1],
+ },
+ description => "List all entries for a given Route Map",
+ parameters => {
+ properties => {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ links => [{ rel => 'child', href => "{order}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+ my $route_map_id = extract_param($param, 'route-map-id');
+
+ my $digest;
+ my $route_map_entries;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+ my $running_config = PVE::Network::SDN::RouteMaps::config(1);
+
+ my $pending_route_maps = PVE::Network::SDN::pending_config(
+ { 'route-maps' => { ids => $running_config->list() } },
+ { ids => $current_config->list() },
+ 'route-maps',
+ );
+
+ $digest = $current_config->digest();
+ $route_map_entries = $pending_route_maps->{ids};
+ } elsif ($running) {
+ $route_map_entries =
+ PVE::Network::SDN::RouteMaps::config(1)->list_route_map($route_map_id);
+ } else {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+
+ $digest = $current_config->digest();
+ $route_map_entries = $current_config->list_route_map($route_map_id);
+ }
+
+ my @res;
+ for my $route_map_id (sort keys $route_map_entries->%*) {
+ $route_map_entries->{$route_map_id}->{digest} = $digest if $digest;
+ push @res, $route_map_entries->{$route_map_id};
+ }
+
+ return \@res;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 26/34] api2: add route map entry module
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (24 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 25/34] api2: add route map module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 27/34] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
` (7 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
This module contains the following API endpoints:
GET /route-maps/<id>/<order> - gets the order'th entry in route map
<id>
PUT /route-maps/<id>/<order> - updates the order'th entry in
route map <id>
DELETE /route-maps/<id>/<order> - deletes the order'th
entry in route map <id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 1 +
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 6 +
.../Network/SDN/RouteMaps/RouteMapEntry.pm | 138 ++++++++++++++++++
3 files changed, 145 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/Makefile b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
index 3d0a928..07b45e9 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/Makefile
+++ b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
@@ -1,4 +1,5 @@
SOURCES=RouteMap.pm\
+ RouteMapEntry.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
index 2dbea09..a6be28d 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::RouteMaps::RouteMap;
use strict;
use warnings;
+use PVE::API2::Network::SDN::RouteMaps::RouteMapEntry;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise_param_exc);
use PVE::Tools qw(extract_param);
@@ -10,6 +11,11 @@ use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMapEntry",
+ path => '{order}',
+});
+
__PACKAGE__->register_method({
name => 'list_route_map_entries',
path => '',
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
new file mode 100644
index 0000000..e5975e6
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
@@ -0,0 +1,138 @@
+package PVE::API2::Network::SDN::RouteMaps::RouteMapEntry;
+
+use strict;
+use warnings;
+
+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);
+
+__PACKAGE__->register_method({
+ name => 'get_route_map_entry',
+ path => '',
+ method => 'GET',
+ permissions => {
+ check =>
+ ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Audit', 'SDN.Allocate'], any => 1],
+ },
+ description => "Get Route Map Entry",
+ parameters => {
+ properties => {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ },
+ },
+ returns => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+
+ my $route_map_entry =
+ PVE::Network::SDN::RouteMaps::config()->get($route_map_id, $order);
+
+ raise_param_exc({ 'route-map-id' => "${route_map_id}_${order} doesn't exist" })
+ if !$route_map_entry;
+
+ return $route_map_entry;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_route_map_entry',
+ path => '',
+ method => 'PUT',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
+ },
+ description => "Update Route Map Entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::RouteMaps::route_map_properties(1)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+ my $delete = extract_param($param, 'delete');
+
+ $config->update($route_map_id, $order, $param, $delete);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "updating route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_route_map_entry',
+ path => '',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
+ },
+ description => "Delete Route Map Entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+
+ $config->delete($route_map_id, $order);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "deleting route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 27/34] evpn controller: add route_map_{in,out} parameter
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (25 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 26/34] api2: add route map entry module Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 28/34] bgp controller: allow configuring custom route maps Stefan Hanreich
` (6 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
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>
---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 18 ++++++++++++++----
src/PVE/Network/SDN/Controllers/Plugin.pm | 14 ++++++++++++++
2 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 3e643b1..055a75f 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;
}
diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm
index d70e518..5f9f1ef 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);
@@ -51,6 +53,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,
+ },
},
};
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 28/34] bgp controller: allow configuring custom route maps
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (26 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 27/34] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 29/34] sdn: change detection for route maps / prefix lists Stefan Hanreich
` (5 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Allows specifying custom route maps, as created in our SDN stack, to
the BGP controller. This can e.g. be used for only selectively
exporting routes from the default routing table via BGP instead of
having to export the whole routing table.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index d54c9ec..7cbd436 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -52,6 +52,8 @@ sub options {
'ebgp' => { optional => 1 },
'ebgp-multihop' => { optional => 1 },
'loopback' => { optional => 1 },
+ 'route-map-in' => { optional => 1 },
+ 'route-map-out' => { optional => 1 },
};
}
@@ -111,12 +113,19 @@ sub generate_frr_config {
my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "128" : "32";
my $af_key = "${ipversion}_unicast";
+ my $bgp_neighbor = {
+ name => "BGP",
+ soft_reconfiguration_inbound => 1,
+ };
+
+ $bgp_neighbor->{route_map_in} = $plugin_config->{'route-map-in'}
+ if $plugin_config->{'route-map-in'};
+ $bgp_neighbor->{route_map_out} = $plugin_config->{'route-map-out'}
+ if $plugin_config->{'route-map-out'};
+
$bgp_router->{address_families}->{$af_key} //= {
networks => [],
- neighbors => [{
- name => "BGP",
- soft_reconfiguration_inbound => 1,
- }],
+ neighbors => [$bgp_neighbor],
};
push @{ $bgp_router->{address_families}->{$af_key}->{networks} }, "$ifaceip/$mask"
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 29/34] sdn: change detection for route maps / prefix lists
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (27 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 28/34] bgp controller: allow configuring custom route maps Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 30/34] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
` (4 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Add both configuration files to the has_pending_changes function, in
order to detect changes to the global SDN configuration. Additionally,
some new keys that are not primitive types have been introduced in the
route-maps.cfg and prefix-lists.cfg. Add them to the function that
generates the pending changes in the SDN stack, so we can return them
from the respective endpoints.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 0bb36bf..ab97a59 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -241,6 +241,8 @@ sub has_pending_changes {
vnets => PVE::Network::SDN::Vnets::config(),
subnets => PVE::Network::SDN::Subnets::config(),
controllers => PVE::Network::SDN::Controllers::config(),
+ 'route-maps' => PVE::Network::SDN::RouteMaps::config(),
+ 'prefix-lists' => PVE::Network::SDN::PrefixLists::config(),
};
for my $config_file (keys %$config_files) {
@@ -484,7 +486,15 @@ sub generate_dhcp_config {
sub encode_value {
my ($type, $key, $value) = @_;
- if ($key eq 'nodes' || $key eq 'exitnodes' || $key eq 'dhcp-range' || $key eq 'interfaces') {
+ if (
+ $key eq 'nodes'
+ || $key eq 'exitnodes'
+ || $key eq 'dhcp-range'
+ || $key eq 'interfaces'
+ || $key eq 'entries'
+ || $key eq 'match'
+ || $key eq 'set'
+ ) {
if (ref($value) eq 'HASH') {
return join(',', sort keys(%$value));
} elsif (ref($value) eq 'ARRAY') {
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 30/34] sdn: generate route map / prefix list configuration on sdn apply
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (28 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 29/34] sdn: change detection for route maps / prefix lists Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 31/34] tests: add simple route map test case Stefan Hanreich
` (3 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Commit the newly introduced configuration files to the running
configuration when applying the SDN configuration, so the FRR config
generation logic can use them to generate the FRR configuration for
them.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index ab97a59..2b3dead 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -25,6 +25,8 @@ use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp;
use PVE::Network::SDN::Frr;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::RouteMaps;
+use PVE::Network::SDN::PrefixLists;
my $RUNNING_CFG_FILENAME = "sdn/.running-config";
@@ -207,12 +209,16 @@ sub compile_running_cfg {
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
+ my $route_maps_cfg = PVE::Network::SDN::RouteMaps::config();
+ my $prefix_lists_cfg = PVE::Network::SDN::PrefixLists::config();
my $vnets = { ids => $vnets_cfg->{ids} };
my $zones = { ids => $zones_cfg->{ids} };
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
my $fabrics = { ids => $fabrics_cfg->to_sections() };
+ my $route_maps = { ids => $route_maps_cfg->to_sections() };
+ my $prefix_lists = { ids => $prefix_lists_cfg->to_sections() };
$cfg = {
version => $version,
@@ -221,6 +227,8 @@ sub compile_running_cfg {
controllers => $controllers,
subnets => $subnets,
fabrics => $fabrics,
+ 'route-maps' => $route_maps,
+ 'prefix-lists' => $prefix_lists,
};
return $cfg;
@@ -427,9 +435,11 @@ configuration.
=cut
sub generate_frr_raw_config {
- my ($running_config, $fabric_config) = @_;
+ my ($running_config, $fabric_config, $route_map_config, $prefix_list_config) = @_;
$running_config = PVE::Network::SDN::running_config() if !$running_config;
+ $prefix_list_config = PVE::Network::SDN::PrefixLists::config(1) if !$prefix_list_config;
+ $route_map_config = PVE::Network::SDN::RouteMaps::config(1) if !$route_map_config;
$fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
my $frr_config = {};
@@ -440,7 +450,11 @@ sub generate_frr_raw_config {
my $nodename = PVE::INotify::nodename();
return PVE::RS::SDN::get_frr_raw_config(
- $frr_config->{'frr'}, $fabric_config, $nodename,
+ $frr_config->{'frr'},
+ $prefix_list_config,
+ $route_map_config,
+ $fabric_config,
+ $nodename,
);
}
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 31/34] tests: add simple route map test case
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (29 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 30/34] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 32/34] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
` (2 subsequent siblings)
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
This testcase specifies two simple route maps, map-in and map-out, and
then overrides the default route maps in the EVPN controller with the
custom route maps.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../evpn/routemap/expected_controller_config | 68 ++++++++++++++++++
.../evpn/routemap/expected_sdn_interfaces | 41 +++++++++++
src/test/zones/evpn/routemap/interfaces | 7 ++
src/test/zones/evpn/routemap/sdn_config | 70 +++++++++++++++++++
4 files changed, 186 insertions(+)
create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap/interfaces
create mode 100644 src/test/zones/evpn/routemap/sdn_config
diff --git a/src/test/zones/evpn/routemap/expected_controller_config b/src/test/zones/evpn/routemap/expected_controller_config
new file mode 100644
index 0000000..163ebbe
--- /dev/null
+++ b/src/test/zones/evpn/routemap/expected_controller_config
@@ -0,0 +1,68 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+ call map-in
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ call map-out
+exit
+!
+route-map map-in deny 5
+ set src 192.0.2.1
+exit
+!
+route-map map-in permit 123
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+ set ip next-hop 198.51.100.3
+ set local-preference 1234
+ set tag 999
+exit
+!
+route-map map-in deny 222
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+exit
+!
+route-map map-out permit 999
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/routemap/expected_sdn_interfaces b/src/test/zones/evpn/routemap/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/routemap/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/routemap/interfaces b/src/test/zones/evpn/routemap/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/routemap/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/routemap/sdn_config b/src/test/zones/evpn/routemap/sdn_config
new file mode 100644
index 0000000..8201eaf
--- /dev/null
+++ b/src/test/zones/evpn/routemap/sdn_config
@@ -0,0 +1,70 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ },
+ },
+
+ zones => {
+ ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, } },
+ },
+ controllers => {
+ ids => { evpnctl => { type => "evpn", 'peers' =>
+ '192.168.0.1,192.168.0.2,192.168.0.3', asn => "65000",
+ 'route-map-in' => 'map-in', 'route-map-out' => 'map-out' } },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-in_222' => {
+ id => 'map-in_222',
+ type => 'route-map-entry',
+ action => 'deny',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ },
+ 'map-in_5' => {
+ id => 'map-in_5',
+ type => 'route-map-entry',
+ action => 'deny',
+ set => [
+ 'key=src,value=192.0.2.1'
+ ],
+ },
+ 'map-in_123' => {
+ id => 'map-in_123',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ set => [
+ 'key=ip-next-hop,value=198.51.100.3',
+ 'key=local-preference,value=1234',
+ 'key=tag,value=999',
+ ],
+ },
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 32/34] tests: add bgp evpn route map/prefix list testcase
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (30 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 31/34] tests: add simple route map test case Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 33/34] tests: add route map with prefix " Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 34/34] tests: add exit node with custom route map testcase Stefan Hanreich
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
Uses a EVPN controller in conjunction with a BGP controller. The
testcases overrides the routemap for one direction in either
controller, but leaves the default incoming route map in the EVPN
controller. Additionally the route map utilizes a custom prefix list
in its matching logic.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 80 +++++++++++++++++
.../expected_sdn_interfaces | 41 +++++++++
.../bgp_evpn_routemap_prefix_list/interfaces | 7 ++
.../bgp_evpn_routemap_prefix_list/sdn_config | 86 +++++++++++++++++++
4 files changed, 214 insertions(+)
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
new file mode 100644
index 0000000..f3e159f
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
@@ -0,0 +1,80 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65002
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as external
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as external
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ neighbor BGP route-map map-in in
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ autort as 65000
+ exit-address-family
+exit
+!
+router bgp 65002 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+ !
+ address-family l2vpn evpn
+ route-target import 65000:1000
+ route-target export 65000:1000
+ exit-address-family
+exit
+!
+ip prefix-list some_list deny 192.0.2.0/24 le 25
+ip prefix-list some_list deny 198.51.100.0/25 le 26 ge 25
+ip prefix-list some_list seq 22 permit 203.0.113.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ call map-out
+exit
+!
+route-map map-in permit 9
+ match ip next-hop prefix-list some_list
+exit
+!
+route-map map-in permit 99
+ match ip next-hop prefix-list some_list
+ set src 192.0.2.1
+ set ip next-hop 192.0.2.100
+exit
+!
+route-map map-out permit 999
+ match ip next-hop prefix-list some_list
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
new file mode 100644
index 0000000..24ee624
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
@@ -0,0 +1,86 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ },
+ },
+
+ zones => {
+ ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, } },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ 'route-map-out' => 'map-out'
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ asn => "65002",
+ node => "localhost",
+ 'route-map-in' => 'map-in'
+ },
+ },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'prefix-lists' => {
+ ids => {
+ 'some_list' => {
+ id => 'some_list',
+ type => 'prefix-list',
+ entries => [
+ 'action=deny,prefix=192.0.2.0/24,le=25',
+ 'action=deny,prefix=198.51.100.0/25,ge=25,le=26',
+ 'action=permit,prefix=203.0.113.0/24,seq=22',
+ ]
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-in_99' => {
+ id => 'map-in_99',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ],
+ set => [
+ 'key=src,value=192.0.2.1',
+ 'key=ip-next-hop,value=192.0.2.100'
+ ]
+ },
+ 'map-in_9' => {
+ id => 'map-in_9',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ },
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 33/34] tests: add route map with prefix list testcase
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (31 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 32/34] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 34/34] tests: add exit node with custom route map testcase Stefan Hanreich
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
A simple test case creating a prefix list and then using that prefix
list inside a match statement in a route map. Then that route map is
used for filtering outgoing routes in an EVPN controller.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 53 +++++++++++++++++
.../expected_sdn_interfaces | 41 +++++++++++++
.../evpn/routemap_prefix_list/interfaces | 7 +++
.../evpn/routemap_prefix_list/sdn_config | 58 +++++++++++++++++++
4 files changed, 159 insertions(+)
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
diff --git a/src/test/zones/evpn/routemap_prefix_list/expected_controller_config b/src/test/zones/evpn/routemap_prefix_list/expected_controller_config
new file mode 100644
index 0000000..0c83b35
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/expected_controller_config
@@ -0,0 +1,53 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+ip prefix-list some_list deny 192.0.2.0/24 le 25
+ip prefix-list some_list deny 198.51.100.0/25 le 26 ge 25
+ip prefix-list some_list seq 22 permit 203.0.113.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ call map-out
+exit
+!
+route-map map-out permit 999
+ match ip next-hop prefix-list some_list
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces b/src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/routemap_prefix_list/interfaces b/src/test/zones/evpn/routemap_prefix_list/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/routemap_prefix_list/sdn_config b/src/test/zones/evpn/routemap_prefix_list/sdn_config
new file mode 100644
index 0000000..8f1179a
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/sdn_config
@@ -0,0 +1,58 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ },
+ },
+
+ zones => {
+ ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, } },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ 'route-map-out' => 'map-out'
+ }
+ },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'prefix-lists' => {
+ ids => {
+ 'some_list' => {
+ id => 'some_list',
+ type => 'prefix-list',
+ entries => [
+ 'action=deny,prefix=192.0.2.0/24,le=25',
+ 'action=deny,prefix=198.51.100.0/25,ge=25,le=26',
+ 'action=permit,prefix=203.0.113.0/24,seq=22',
+ ]
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH pve-network v2 34/34] tests: add exit node with custom route map testcase
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (32 preceding siblings ...)
2026-04-01 14:39 ` [PATCH pve-network v2 33/34] tests: add route map with prefix " Stefan Hanreich
@ 2026-04-01 14:39 ` Stefan Hanreich
33 siblings, 0 replies; 37+ messages in thread
From: Stefan Hanreich @ 2026-04-01 14:39 UTC (permalink / raw)
To: pve-devel
This testcase simulates an exit node with a custom route map. It
checks whether the stack still auto-generates the deny rules for
default routes (otherwise traffic will loop between the exit nodes
until TTL is exeeded) and only then jumps into the user-provided
custom route map.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 101 ++++++++++++++++++
.../expected_sdn_interfaces | 41 +++++++
.../zones/evpn/routemap_exit_node/interfaces | 7 ++
.../zones/evpn/routemap_exit_node/sdn_config | 71 ++++++++++++
4 files changed, 220 insertions(+)
create mode 100644 src/test/zones/evpn/routemap_exit_node/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_exit_node/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_exit_node/interfaces
create mode 100644 src/test/zones/evpn/routemap_exit_node/sdn_config
diff --git a/src/test/zones/evpn/routemap_exit_node/expected_controller_config b/src/test/zones/evpn/routemap_exit_node/expected_controller_config
new file mode 100644
index 0000000..b581775
--- /dev/null
+++ b/src/test/zones/evpn/routemap_exit_node/expected_controller_config
@@ -0,0 +1,101 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family ipv4 unicast
+ import vrf vrf_myzone
+ exit-address-family
+ !
+ address-family ipv6 unicast
+ import vrf vrf_myzone
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+ !
+ address-family ipv4 unicast
+ redistribute connected
+ exit-address-family
+ !
+ address-family ipv6 unicast
+ redistribute connected
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ default-originate ipv4
+ default-originate ipv6
+ exit-address-family
+exit
+!
+ip prefix-list only_default seq 1 permit 0.0.0.0/0
+!
+ipv6 prefix-list only_default_v6 seq 1 permit ::/0
+!
+route-map MAP_VTEP_IN deny 1
+ match ip address prefix-list only_default
+exit
+!
+route-map MAP_VTEP_IN deny 2
+ match ipv6 address prefix-list only_default_v6
+exit
+!
+route-map MAP_VTEP_IN permit 3
+ call map-in
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ call map-out
+exit
+!
+route-map map-in deny 5
+ set src 192.0.2.1
+exit
+!
+route-map map-in permit 123
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+ set ip next-hop 198.51.100.3
+ set local-preference 1234
+ set tag 999
+exit
+!
+route-map map-in deny 222
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+exit
+!
+route-map map-out permit 999
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/routemap_exit_node/expected_sdn_interfaces b/src/test/zones/evpn/routemap_exit_node/expected_sdn_interfaces
new file mode 100644
index 0000000..5ab3084
--- /dev/null
+++ b/src/test/zones/evpn/routemap_exit_node/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route del vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/routemap_exit_node/interfaces b/src/test/zones/evpn/routemap_exit_node/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/routemap_exit_node/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/routemap_exit_node/sdn_config b/src/test/zones/evpn/routemap_exit_node/sdn_config
new file mode 100644
index 0000000..812c13b
--- /dev/null
+++ b/src/test/zones/evpn/routemap_exit_node/sdn_config
@@ -0,0 +1,71 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ },
+ },
+
+ zones => {
+ ids => { myzone => { ipam => "pve", type => "evpn", controller =>
+ "evpnctl", 'vrf-vxlan' => 1000, exitnodes => { 'localhost' => 1 } } },
+ },
+ controllers => {
+ ids => { evpnctl => { type => "evpn", 'peers' =>
+ '192.168.0.1,192.168.0.2,192.168.0.3', asn => "65000",
+ 'route-map-in' => 'map-in', 'route-map-out' => 'map-out' } },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-in_222' => {
+ id => 'map-in_222',
+ type => 'route-map-entry',
+ action => 'deny',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ },
+ 'map-in_5' => {
+ id => 'map-in_5',
+ type => 'route-map-entry',
+ action => 'deny',
+ set => [
+ 'key=src,value=192.0.2.1'
+ ],
+ },
+ 'map-in_123' => {
+ id => 'map-in_123',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ set => [
+ 'key=ip-next-hop,value=198.51.100.3',
+ 'key=local-preference,value=1234',
+ 'key=tag,value=999',
+ ],
+ },
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types Stefan Hanreich
@ 2026-04-02 13:36 ` Wolfgang Bumiller
0 siblings, 0 replies; 37+ messages in thread
From: Wolfgang Bumiller @ 2026-04-02 13:36 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On Wed, Apr 01, 2026 at 04:39:14PM +0200, Stefan Hanreich wrote:
> The reason for including those types here is that they are used in
> proxmox-frr for generating the FRR configuration as well as
> proxmox-ve-config for utilizing them in the section config types.
>
> For some values in route maps FRR supports specifying either an
> absolute value or a value relative to the existing value. When
> modifying the metric of a route via a route map, '123' would set the
> value to 123, whereas '+/-123' would add/subtract 123 from the
> existing value. IntegerWithSign can be used to represent such a value
> in the section config.
>
> A custom deserializer is implemented, so primitive numerical values
> can be used in addition to strings that contain the respective signs.
> They deserialize into absolute values.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-sdn-types/src/bgp.rs | 62 ++++++++++++
> proxmox-sdn-types/src/lib.rs | 179 +++++++++++++++++++++++++++++++++++
> 2 files changed, 241 insertions(+)
> create mode 100644 proxmox-sdn-types/src/bgp.rs
>
> diff --git a/proxmox-sdn-types/src/bgp.rs b/proxmox-sdn-types/src/bgp.rs
> new file mode 100644
> index 0000000..6df3022
> --- /dev/null
> +++ b/proxmox-sdn-types/src/bgp.rs
> @@ -0,0 +1,62 @@
> +use serde::{Deserialize, Serialize};
> +
> +use crate::IntegerWithSign;
> +
> +/// Represents a BGP metric value, as used in FRR.
> +///
> +/// A metric can either be a numeric value, or certain 'magic' values. For more information see the
> +/// respective enum variants.
> +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
> +pub enum SetMetricValue {
> + /// Set the metric to the round-trip-time.
> + #[serde(rename = "rtt")]
> + Rtt,
> + /// Add the round-trip-time to the metric.
> + #[serde(rename = "+rtt")]
> + AddRtt,
> + /// Subtract the round-trip-time from the metric.
> + #[serde(rename = "-rtt")]
> + SubtractRtt,
> + /// Use the IGP value when importing from another IGP.
> + #[serde(rename = "igp")]
> + Igp,
> + /// Use the accumulated IGP value when importing from another IGP.
> + #[serde(rename = "aigp")]
> + Aigp,
> + /// Set the metric to a fixed numeric value.
> + #[serde(untagged)]
> + Numeric(IntegerWithSign),
> +}
> +
> +impl<T: Into<IntegerWithSign>> From<T> for SetMetricValue {
> + fn from(value: T) -> Self {
> + Self::Numeric(value.into())
> + }
> +}
> +
> +/// An EVPN route-type, as used in the FRR route maps.
> +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
> +#[serde(rename_all = "lowercase")]
> +pub enum EvpnRouteType {
> + Ead,
> + MacIp,
> + Multicast,
> + #[serde(rename = "es")]
> + EthernetSegment,
> + Prefix,
> +}
> +
> +/// An tag value, as used in the FRR route maps.
> +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
> +#[serde(rename_all = "lowercase")]
> +pub enum SetTagValue {
> + Untagged,
> + #[serde(untagged)]
> + Numeric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
> +}
> +
> +impl SetTagValue {
> + pub fn new(value: u32) -> Self {
> + SetTagValue::Numeric(value)
> + }
> +}
> diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
> index 1656f1d..9795857 100644
> --- a/proxmox-sdn-types/src/lib.rs
> +++ b/proxmox-sdn-types/src/lib.rs
> @@ -1,3 +1,182 @@
> pub mod area;
> +pub mod bgp;
> pub mod net;
> pub mod openfabric;
> +
> +use serde::de::{Error, Visitor};
> +use serde::{Deserialize, Serialize};
> +
> +use proxmox_schema::api;
> +
> +/// Enum for representing signedness of Integer in [`IntegerWithSign`].
> +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
> +pub enum Sign {
> + #[serde(rename = "-")]
> + Negative,
> + #[serde(rename = "+")]
> + Positive,
> +}
> +
> +proxmox_serde::forward_display_to_serialize!(Sign);
> +proxmox_serde::forward_from_str_to_deserialize!(Sign);
> +
> +/// An Integer with an optional [`Sign`].
> +///
> +/// This is used for representing certain keys in the FRR route maps (e.g. metric). They can be set
> +/// to either a static value (no sign) or to a value relative to the existing value (with sign).
> +/// For instance, a value of 50 would set the metric to 50, but a value of +50 would add 50 to the
> +/// existing metric value.
> +#[derive(Debug, Clone, Eq, PartialEq)]
> +pub struct IntegerWithSign {
> + pub(crate) sign: Option<Sign>,
> + pub(crate) n: u32,
> +}
> +
Summary of a short off-list discussion:
- I found the name somewhat arbitrary (and the `Sign` part is optional)
- It's supposed to say "Set to X" or "Modify *by* X", so it's probably
nicer to have an `enum ModifyNumber { Absolute(u32), Relative(i32) }`
(name and exact types to be determined) so the name and content
actually reflect the description found in the doc comment.
Also, since this comes from the API and flows through perl
(integer/string confusion) and the leading plus sign is significant
here, it should be an only-string type (which means the `Deserialize`
impl can be forwarded to `FromStr` and the serde `Visitor` boiler plate
can be dropped).
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
@ 2026-04-03 7:42 ` Wolfgang Bumiller
0 siblings, 0 replies; 37+ messages in thread
From: Wolfgang Bumiller @ 2026-04-03 7:42 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
minor nit
On Wed, Apr 01, 2026 at 04:39:21PM +0200, Stefan Hanreich wrote:
> Implements conversion traits for all the section config types, so they
> can be converted into their respective FRR template counterpart.
>
> Also add a helper that adds a list of prefix lists to an existing FRR
> configuration. This will be used by perl-rs to generate the FRR
> configuration from the section configuration. The helper will
> overwrite existing prefix lists in the FRR configuration, allowing
> users to override pre-defined prefix lists generated by our stack.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-ve-config/src/sdn/prefix_list.rs | 60 ++++++++++++++++++++++++
> 1 file changed, 60 insertions(+)
>
> diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
> index f4988d9..1876799 100644
> --- a/proxmox-ve-config/src/sdn/prefix_list.rs
> +++ b/proxmox-ve-config/src/sdn/prefix_list.rs
> @@ -123,6 +123,66 @@ pub enum PrefixList {
> PrefixList(PrefixListSection),
> }
>
> +#[cfg(feature = "frr")]
> +pub mod frr {
> + use super::*;
> +
> + use proxmox_frr::ser::{
> + route_map::{
> + self, PrefixListName as FrrPrefixListName, PrefixListRule as FrrPrefixListRule,
> + },
> + FrrConfig,
> + };
> +
> + impl From<PrefixListId> for FrrPrefixListName {
> + fn from(value: PrefixListId) -> Self {
> + FrrPrefixListName::new(value.0)
> + }
> + }
> +
> + impl From<PrefixListEntry> for FrrPrefixListRule {
> + fn from(value: PrefixListEntry) -> Self {
> + FrrPrefixListRule {
> + action: match value.action {
> + PrefixListAction::Permit => route_map::AccessAction::Permit,
> + PrefixListAction::Deny => route_map::AccessAction::Deny,
> + },
> + network: value.prefix,
> + seq: value.seq,
> + le: value.le,
> + ge: value.ge,
> + is_ipv6: value.prefix.is_ipv6(),
> + }
> + }
> + }
> +
> + /// Add a list of Prefix Lists to an [`FrrConfig`].
> + ///
> + /// This will overwrite existing Prefix Lists in the [`FrrConfig`]. Since this will be used for
> + /// generating the FRR configuration from the SDN stack, this enables users to override Prefix
> + /// Lists that are predefined by our stack.
> + pub fn build_frr_prefix_lists(
> + prefix_lists: impl IntoIterator<Item = PrefixList>,
> + frr_config: &mut FrrConfig,
> + ) -> Result<(), anyhow::Error> {
> + for prefix_list in prefix_lists.into_iter() {
Superfluous `.into_iter()` call.
> + let PrefixList::PrefixList(prefix_list) = prefix_list;
> + let prefix_list_name = FrrPrefixListName::new(prefix_list.id.0);
> +
> + frr_config.prefix_lists.insert(
> + prefix_list_name,
> + prefix_list
> + .entries
> + .into_iter()
> + .map(|prefix_list| prefix_list.into_inner().into())
> + .collect(),
> + );
> + }
> +
> + Ok(())
> + }
> +}
> +
> pub mod api {
> use super::*;
>
> --
> 2.47.3
^ permalink raw reply [flat|nested] 37+ messages in thread
end of thread, other threads:[~2026-04-03 7:42 UTC | newest]
Thread overview: 37+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-01 14:39 [PATCH access-control/cluster/network/proxmox{-ve-rs,-perl-rs} v2 00/34] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 01/34] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-cluster v2 02/34] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-access-control v2 03/34] permissions: add ACL path for prefix-lists and route-maps Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 04/34] frr: add constructor to prefix list name Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types Stefan Hanreich
2026-04-02 13:36 ` Wolfgang Bumiller
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 06/34] frr: change order type to u16 Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 07/34] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 08/34] frr: implement support for call and exit action Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 09/34] frr-templates: change route maps template to adapt to new frr types Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 10/34] ve-config: fabrics: adapt frr config generation Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 11/34] ve-config: add prefix list section config Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 12/34] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
2026-04-03 7:42 ` Wolfgang Bumiller
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 13/34] ve-config: add route map section config Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 14/34] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 15/34] ve-config: add prefix lists integration tests Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-ve-rs v2 16/34] ve-config: add route maps " Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 17/34] pve-rs: sdn: add route maps module Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 18/34] pve-rs: sdn: add prefix lists module Stefan Hanreich
2026-04-01 14:39 ` [PATCH proxmox-perl-rs v2 19/34] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 20/34] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 21/34] sdn: add prefix lists module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 22/34] api2: add prefix list module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 23/34] sdn: add route map module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 24/34] api2: add route maps api module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 25/34] api2: add route map module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 26/34] api2: add route map entry module Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 27/34] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 28/34] bgp controller: allow configuring custom route maps Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 29/34] sdn: change detection for route maps / prefix lists Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 30/34] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 31/34] tests: add simple route map test case Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 32/34] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 33/34] tests: add route map with prefix " Stefan Hanreich
2026-04-01 14:39 ` [PATCH pve-network v2 34/34] tests: add exit node with custom route map testcase Stefan Hanreich
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox