* [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics
@ 2026-05-12 17:31 Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
` (31 more replies)
0 siblings, 32 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
## Introduction
This patch series introduces WireGuard as fabric protocol. Potential use-cases
include:
* Connecting to remote PBS / PDM instances
* Simple encryption layer for intra-DC VXLAN tunnels
* Secure migration network
* Connecting with remote PVE clusters
It utilizes the wg(8) tool for generating the interface configuration [1] and
the section config format leans heavily into the keys defined there.
## Configuration format
The configuration format is quite similar to OSPF and Openfabric with the main
difference being that WireGuard nodes have been split into two subtypes
(external and internal), in order to support nodes that are not part of the
cluster.
### Nodes
WireGuard nodes have been split into two different types. Those are not distinct
section config types, due to how the internal representation of the FabricConfig
has been structured (which maps exactly one Fabric type to one Node type). So
instead there is one Node type that is an enum. The 'role' field is used for
distinguishing between different WireGuard node types.
#### Internal
This represents a node that is part of the Proxmox VE cluster.
An example configuration looks like this:
wireguard_node: vpn_elementalist
endpoint 192.0.2.1
allowed_ips 203.0.113.128/25
interfaces name=wg0,listen_port=50000,public_key=O+Kzrochm6klMILjSKVw83xb3YyXXLpmZj9n/ICM5xE=,ip=198.51.100.1/24
role internal
The endpoint value will be used by other nodes inside the Proxmox VE cluster for
connecting to the defined node. IPs are defined on a per-interface basis, not a
per-node basis. The interface key represents the [Interface] section in the
WireGuard configuration. All values (except for public key) are overridable in
the peer definition.
#### External
External nodes represent any peer that is not a Proxmox VE node. They provide a
mechanism for defining a reusable peer definition (see below for more details).
This allows for easily re-using and updating the information of an external
peer, without having to re-type all information for every Proxmox VE node that
wants to utilize the definition.
An example configuration looks like this:
wireguard_node: vpn_berserker
endpoint berserker:51337
allowed_ips 203.0.113.0/25
public_key GDPUAnPOY5xGIjYXmcGyXZXbocjBr21dGQ5vwnjmdzA=
role external
Those keys map 1:1 to the peer entries in the respective WireGuard configuration
format and are used for generating the peer definition wherever they are
referenced.
### Peers
Interfaces on Proxmox nodes can have one or more peers. A peer is a reference to
either the interface of an internal node, or an external node. Due to
limitations in dealing with nested data in the section config, peers are an
array field in the node, instead of being configured on the interface directly.
An example configuration for a Proxmox VE node with an interface that has an
internal and external node as peer looks as follows:
wireguard_node: vpn_occultist
endpoint 192.0.2.2
interfaces name=wg0,listen_port=50000,public_key=y0kOpXfo9ff4KoUwO3H1cRuwObbKwsK8mAkwXxNvKUc=,ip=198.51.100.2/24
peers type=internal,node=elementalist,node_iface=wg0,iface=wg0
peers type=external,node=berserker,iface=wg0
role internal
This would generate the following wg0.conf file:
[Interface]
PrivateKey = <some_private_key>
ListenPort = 50000
[Peer]
PublicKey = O+Kzrochm6klMILjSKVw83xb3YyXXLpmZj9n/ICM5xE=
AllowedIPs = 198.51.100.1/32
Endpoint = 192.0.2.1:50000
AllowedIPs = 203.0.113.128/25
[Peer]
PublicKey = GDPUAnPOY5xGIjYXmcGyXZXbocjBr21dGQ5vwnjmdzA=
Endpoint = berserker:51337
AllowedIPs = 203.0.113.0/25
Peer definitions allow overriding properties from the node definition (e.g.
endpoint). This is currently not implemented in the frontend. This is also the
main reason for choosing to store peers as an array in a different key.
Referencing peer defintions by id would have been possible in the interface
property string, but if the possibility of overriding certain attributes should
be available, then a separate key with property strings is required.
## Key handling
Keys are automatically generated in the backend on demand, whenever an interface
is created. Keys are deleted upon applying the SDN configuration. After a
key has been generated, the respective public key gets stored in the section
config.
The WireGuard configuration files are stored locally on the node in the newly
established '/etc/wireguard/proxmox' folder, and managed by the node itself.
## Open questions / issues
### Peers
The main issue I see with the configuration format is that peers reference
arbitrary node sections / interface definitions in the fabric config. This poses
some problems, particularly when updating the referenced entities. For instance,
users could delete a referenced interface, invalidating the configuration. This
is quite similar to the problems we currently encounter with firewall ipsets and
aliases.
In order to avoid re-creating the same issues there are a few restrictions in
the UI that should prevent the most common mistakes:
* Renaming nodes and interfaces is not allowed.
* The configuration is validated after every modification and invalid
configurations are outright rejected. This is particularly important for
delete operations.
In the future we could lift some restrictions by implementing smarter CRUD
operations. For instance, when deleting an interface all peer entries, that
reference that interface, could be deleted as well. Even for accidental
deletions this isn't too bad imo, since we have a mechanism of restoring the
current running configuration, which users can always use.
For updates to the interfaces of a node this is harder, since it is impossible
to say whether an interface has been renamed or an interface has been deleted
and another one created. I don't really see a good heuristic (even when tracking
this in the UI) that works particularly well for all potential cases.
### Section Types
The split of one section type ('wireguard_node') into two different subtypes is
breaking a bit with section config principles. Another solution would be to
introduce two section config types (e.g. wireguard_node_{external,internal}),
although that would require quite some refactoring effort.
## Future work
* implement status reporting
* provide QoL features for easier config (e.g. auto-"fullmeshify" PVE cluster)
* Implement some backend-only features in the UI (e.g. per-peer overrides,
pre-shared keys)
* Integration into PDM / PBS
## Dependencies
* proxmox-ve-config depends on proxmox-sdn-types
* proxmox-ve-config depends on proxmox-network-types
* proxmox-ve-config depends on proxmox-wireguard
* proxmox-perl-rs depends on proxmox-ve-config
* pve-network depends on proxmox-perl-rs
* pve-network depends on pve-cluster
* pve-manager depends on pve-network
Changes from v4 (Thanks @Arthur):
* removed already applied commits
* rebased on top of master
* auto-create /etc/wireguard/proxmox if it doesn't exist already
* improved descriptions of some properties in the JSONSchema
* add documentation for WireGuard fabric
* reject configurations where two interfaces have the same IP
* fix validating existence of referenced external nodes
* improve the task log warning when wireguard-tools is not installed.
* do not leave wg-keys.cfg in an invalid state when interface validation fails
on updating a node
* rename auto_generate_routes to skip_route_generation and invert logic
* expose skip_route_generation in UI
* remove wireguard configuration files that were removed
* only print warning for non-existing wireguard-tools if there are wireguard
fabrics configured
* fix fabric view fail to render if the running configuration for a node
contains the interfaces property, but the pending configuration does not
* always show status ok for fabrics, until status reporting is actually
implemented
Changes from v3 (Thanks @Thomas):
* rebased on top of current master
* use x25519 instead of ed25519 for public key derivation (which is the correct
algorithm)
* moved keys to pmxcfs into a section config file under /etc/pve/priv
* delete keys on applying the SDN config, not when calling DELETE API call
* fix error message when referenced interface does not exist
* fix validating the existence of interfaces
* fix editing an external node
* fix some doc-comments in the Rust code
Changes from v2 (Thanks @Gabriel):
* rebased branches on top of current master + route-maps series
* added backend-only option to skip auto-generating routes
* added possibility to include wireguard interfaces when selecting interfaces
for nodes in other fabric types
* show auto-generated public key in Web UI
* improved validation error messages
* added better descriptions in the UI for the endpoint / allowed ips options
* added newline to generated ifupdown2 config stanza
* added early failure in case wireguard-tools isn't installed
Changes from RFC:
* rebased on top of current master branches
[1] https://man7.org/linux/man-pages/man8/wg.8.html
pve-cluster:
Stefan Hanreich (1):
cfs: add 'priv/wg-keys.cfg' to observed files
src/PVE/Cluster.pm | 1 +
src/pmxcfs/status.c | 1 +
2 files changed, 2 insertions(+)
proxmox-ve-rs:
Christoph Heiss (2):
sdn-types: add wireguard-specific PersistentKeepalive api type
ve-config: fabric: refactor fabric config entry impl using macro
Stefan Hanreich (6):
ve-config: fabrics: split interface name regex into two parts
ve-config: fabrics: add protocol-specific properties for wireguard
ve-config: wireguard: add private keys section config
ve-config: sdn: fabrics: add wireguard to the fabric config
ve-config: fabrics: wireguard add validation for wireguard config
ve-config: fabrics: implement wireguard config generation
proxmox-sdn-types/src/lib.rs | 1 +
proxmox-sdn-types/src/wireguard.rs | 43 +
proxmox-ve-config/Cargo.toml | 3 +
proxmox-ve-config/debian/control | 6 +
proxmox-ve-config/src/sdn/fabric/frr.rs | 1 +
proxmox-ve-config/src/sdn/fabric/mod.rs | 447 ++++++++--
.../src/sdn/fabric/section_config/fabric.rs | 25 +
.../sdn/fabric/section_config/interface.rs | 5 +-
.../src/sdn/fabric/section_config/mod.rs | 58 ++
.../src/sdn/fabric/section_config/node.rs | 32 +-
.../sdn/fabric/section_config/protocol/mod.rs | 1 +
.../section_config/protocol/wireguard.rs | 810 ++++++++++++++++++
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/wireguard.rs | 309 +++++++
14 files changed, 1671 insertions(+), 71 deletions(-)
create mode 100644 proxmox-sdn-types/src/wireguard.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
create mode 100644 proxmox-ve-config/src/sdn/wireguard.rs
proxmox-perl-rs:
Christoph Heiss (1):
pve-rs: fabrics: wireguard: generate ifupdown2 configuration
Stefan Hanreich (2):
pve-rs: fabrics: add helpers for parsing interface property strings
pve-rs: sdn: wireguard: add private keys module
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/fabrics.rs | 217 +++++++++++++++++++++++----
pve-rs/src/bindings/sdn/mod.rs | 1 +
pve-rs/src/bindings/sdn/wireguard.rs | 103 +++++++++++++
pve-rs/src/sdn/status.rs | 29 +++-
6 files changed, 316 insertions(+), 36 deletions(-)
create mode 100644 pve-rs/src/bindings/sdn/wireguard.rs
pve-network:
Christoph Heiss (1):
sdn: add wireguard helper module
Stefan Hanreich (2):
fabrics: wireguard: add schema definitions for wireguard
fabrics: wireguard: implement wireguard key auto-generation
src/PVE/API2/Network/SDN.pm | 4 +-
.../API2/Network/SDN/Fabrics/FabricNode.pm | 106 ++++++++++-
src/PVE/Network/SDN.pm | 2 +
src/PVE/Network/SDN/Fabrics.pm | 180 +++++++++++++++++-
src/PVE/Network/SDN/Makefile | 3 +-
src/PVE/Network/SDN/WireGuard.pm | 176 +++++++++++++++++
6 files changed, 457 insertions(+), 14 deletions(-)
create mode 100644 src/PVE/Network/SDN/WireGuard.pm
pve-manager:
Christoph Heiss (2):
ui: fabrics: edit: make ipv4/6 support generic over fabric panels
ui: fabrics: interface: make ipv4/6 support generic over edit panels
Stefan Hanreich (11):
network: sdn: generate wireguard configuration on apply
ui: fix parsing of property-strings when values contain =
ui: fabrics: i18n: make node loading string translatable
sdn: fabrics view: handle case where interfaces are deleted
ui: fabrics: split node selector creation and config
ui: fabrics: node: make ipv4/6 support generic over edit panels
ui: fabrics: wireguard: add interface edit panel
ui: fabrics: wireguard: add node edit panel
ui: fabrics: wireguard: add fabric edit panel
ui: fabrics: hook up wireguard components
fabrics: node edit: add option to include wireguard interfaces
PVE/API2/Network.pm | 1 +
www/manager6/Makefile | 3 +
www/manager6/Parser.js | 7 +-
www/manager6/sdn/FabricsView.js | 16 +
www/manager6/sdn/fabrics/FabricEdit.js | 68 ++-
www/manager6/sdn/fabrics/InterfacePanel.js | 18 +
www/manager6/sdn/fabrics/NodeEdit.js | 107 +++-
.../sdn/fabrics/openfabric/FabricEdit.js | 32 --
.../sdn/fabrics/openfabric/InterfacePanel.js | 13 -
.../sdn/fabrics/openfabric/NodeEdit.js | 14 -
www/manager6/sdn/fabrics/ospf/FabricEdit.js | 2 +
.../sdn/fabrics/ospf/InterfacePanel.js | 2 +
www/manager6/sdn/fabrics/ospf/NodeEdit.js | 3 +
.../sdn/fabrics/wireguard/FabricEdit.js | 29 +
.../sdn/fabrics/wireguard/InterfacePanel.js | 518 ++++++++++++++++++
.../sdn/fabrics/wireguard/NodeEdit.js | 202 +++++++
16 files changed, 941 insertions(+), 94 deletions(-)
create mode 100644 www/manager6/sdn/fabrics/wireguard/FabricEdit.js
create mode 100644 www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
create mode 100644 www/manager6/sdn/fabrics/wireguard/NodeEdit.js
pve-docs:
Stefan Hanreich (1):
sdn: fabrics: add section about wireguard
pvesdn.adoc | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)
Summary over all repositories:
45 files changed, 3487 insertions(+), 215 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 35+ messages in thread
* [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 02/29] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
` (30 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Used for storing the private keys used in the WireGuard fabric.
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 047732b..034b78c 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -63,6 +63,7 @@ my $observed = {
'priv/tfa.cfg' => 1,
'priv/token.cfg' => 1,
'priv/acme/plugins.cfg' => 1,
+ 'priv/wg-keys.cfg' => 1,
'/qemu-server/' => 1,
'/openvz/' => 1,
'/lxc/' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index a567d8f..12a6c46 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -88,6 +88,7 @@ static memdb_change_t memdb_change_array[] = {
{.path = "priv/acme/plugins.cfg"},
{.path = "priv/tfa.cfg"},
{.path = "priv/token.cfg"},
+ {.path = "priv/wg-keys.cfg"},
{.path = "datacenter.cfg"},
{.path = "vzdump.cron"},
{.path = "vzdump.conf"},
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 02/29] sdn-types: add wireguard-specific PersistentKeepalive api type
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 03/29] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
` (29 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-sdn-types/src/lib.rs | 1 +
proxmox-sdn-types/src/wireguard.rs | 43 ++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 proxmox-sdn-types/src/wireguard.rs
diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
index 7254593..cff4491 100644
--- a/proxmox-sdn-types/src/lib.rs
+++ b/proxmox-sdn-types/src/lib.rs
@@ -2,6 +2,7 @@ pub mod area;
pub mod bgp;
pub mod net;
pub mod openfabric;
+pub mod wireguard;
use serde::{Deserialize, Serialize};
diff --git a/proxmox-sdn-types/src/wireguard.rs b/proxmox-sdn-types/src/wireguard.rs
new file mode 100644
index 0000000..4c79b50
--- /dev/null
+++ b/proxmox-sdn-types/src/wireguard.rs
@@ -0,0 +1,43 @@
+//! API types for the WireGuard fabric.
+
+use std::fmt::Display;
+
+use proxmox_schema::{api, UpdaterType};
+use serde::{Deserialize, Serialize};
+
+/// Persistent keep-alive interval. Specifies how often a authenticated, empty
+/// packet will be sent to the peer to keep e.g. stateful firewall open or NAT
+/// mappings.
+///
+/// Interval in seconds, between 1 and 65536 inclusive.
+#[api(
+ type: Integer,
+ minimum: 1,
+)]
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct PersistentKeepalive(
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16,
+);
+
+impl PersistentKeepalive {
+ /// Determines whether the given `PersistentKeepalive` value means that it is
+ /// turned off.
+ pub fn is_off(&self) -> bool {
+ self.0 == 0
+ }
+
+ pub fn raw(&self) -> u16 {
+ self.0
+ }
+}
+
+impl UpdaterType for PersistentKeepalive {
+ type Updater = Option<PersistentKeepalive>;
+}
+
+impl Display for PersistentKeepalive {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 03/29] ve-config: fabrics: split interface name regex into two parts
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 02/29] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 04/29] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
` (28 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Split out the non-anchored part, analogous to the other regexes
defined in this crate - so they can be used in both ways (anchored and
not anchored).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/section_config/interface.rs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs b/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
index 4374f38..f0db8aa 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
@@ -1,9 +1,12 @@
+use const_format::concatcp;
use serde::{Deserialize, Serialize};
use proxmox_schema::{api, api_string_type, const_regex, ApiStringFormat, UpdaterType};
+pub const INTERFACE_NAME_REGEX_STR: &str = "[[:ascii:]]+";
+
const_regex! {
- pub INTERFACE_NAME_REGEX = r"^[[:ascii:]]+$";
+ pub INTERFACE_NAME_REGEX = concatcp!(r"^", INTERFACE_NAME_REGEX_STR, r"$");
}
pub const INTERFACE_NAME_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&INTERFACE_NAME_REGEX);
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 04/29] ve-config: fabric: refactor fabric config entry impl using macro
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (2 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 03/29] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 05/29] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
` (27 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
It's always the same, so simplify future additions using a simple
macro.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/mod.rs | 76 +++++++------------------
1 file changed, 22 insertions(+), 54 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index ab369ec..a14c8ac 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -162,66 +162,34 @@ where
}
}
-impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
- /// Get the OpenFabric fabric config.
- ///
- /// This method is implemented for [`Entry<OpenfabricProperties, OpenfabricNodeProperties>`],
- /// so it is guaranteed that a [`FabricSection<OpenfabricProperties>`] is returned.
- pub fn fabric_section(&self) -> &FabricSection<OpenfabricProperties> {
- if let Fabric::Openfabric(section) = &self.fabric {
- return section;
- }
-
- unreachable!();
- }
-
- /// Get the OpenFabric node config for the given node_id.
- ///
- /// This method is implemented for [`Entry<OpenfabricProperties, OpenfabricNodeProperties>`],
- /// so it is guaranteed that a [`NodeSection<OpenfabricNodeProperties>`] is returned.
- /// An error is returned if the node is not found.
- pub fn node_section(
- &self,
- id: &NodeId,
- ) -> Result<&NodeSection<OpenfabricNodeProperties>, FabricConfigError> {
- if let Node::Openfabric(section) = self.get_node(id)? {
- return Ok(section);
- }
-
- unreachable!();
- }
-}
+macro_rules! impl_entry {
+ ($variant:ident, $propty:ty, $nodepropty:ty) => {
+ impl Entry<$propty, $nodepropty> {
+ pub fn fabric_section(&self) -> &FabricSection<$propty> {
+ if let Fabric::$variant(section) = &self.fabric {
+ return section;
+ }
-impl Entry<OspfProperties, OspfNodeProperties> {
- /// Get the OSPF fabric config.
- ///
- /// This method is implemented for [`Entry<OspfProperties, OspfNodeProperties>`],
- /// so it is guaranteed that a [`FabricSection<OspfProperties>`] is returned.
- pub fn fabric_section(&self) -> &FabricSection<OspfProperties> {
- if let Fabric::Ospf(section) = &self.fabric {
- return section;
- }
+ unreachable!();
+ }
- unreachable!();
- }
+ pub fn node_section(
+ &self,
+ id: &NodeId,
+ ) -> Result<&NodeSection<$nodepropty>, FabricConfigError> {
+ if let Node::$variant(section) = self.get_node(id)? {
+ return Ok(section);
+ }
- /// Get the OSPF node config for the given node_id.
- ///
- /// This method is implemented for [`Entry<OspfProperties, OspfNodeProperties>`],
- /// so it is guaranteed that a [`NodeSection<OspfNodeProperties>`] is returned.
- /// An error is returned if the node is not found.
- pub fn node_section(
- &self,
- id: &NodeId,
- ) -> Result<&NodeSection<OspfNodeProperties>, FabricConfigError> {
- if let Node::Ospf(section) = self.get_node(id)? {
- return Ok(section);
+ unreachable!();
+ }
}
-
- unreachable!();
- }
+ };
}
+impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties);
+impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
+
/// All possible entries in a [`FabricConfig`].
///
/// It utilizes the [`Entry`] struct to validate proper combinations of [`FabricSection`] and
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 05/29] ve-config: fabrics: add protocol-specific properties for wireguard
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (3 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 04/29] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 06/29] ve-config: wireguard: add private keys section config Stefan Hanreich
` (26 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Introduce the types representing the wireguard entities in the fabric
configuration. WireGuard nodes can have two different subtypes
(internal or external), depending on whether they are cluster members
or not. They are not implemented as distinct section types, but rather
as variants of the same section type due to how the FabricConfig is
structured. It associates one type of fabric section with one type of
node section, so the internal/external nodes are modeled as an enum.
Contrary to OSPF and Openfabric, interfaces do not reference existing
interfaces on the node but rather which interfaces should get created
on a node.
WireGuard interfaces can define peers, which are references to the
interfaces of internal nodes or to external nodes itself. This schema
allows for easily re-using the same peer definition across multiple
nodes and also makes it easy to create WireGuard setups that connect
Proxmox VE cluster nodes. Since interfaces require a public key, which
gets automatically generated when creating the interface, introduce an
additional struct that models the interface definitions from the
create call, which can be used for deserializing the interface
definitions in the create call, before a public key has been
generated.
Due to peers being able to reference other node sections, validation
of those invariants needs to happen at a higher level, for the whole
configuration file and will be added in a later commit that adds the
WireGuard variants to the FabricConfig.
For additional information, also consult the module-level
documentation in the wireguard module.
Originally-by: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/debian/control | 4 +
.../sdn/fabric/section_config/protocol/mod.rs | 1 +
.../section_config/protocol/wireguard.rs | 515 ++++++++++++++++++
4 files changed, 521 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index c4e6269..08a4a99 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -26,6 +26,7 @@ proxmox-section-config = { version = "3" }
proxmox-serde = { workspace = true, features = [ "perl" ]}
proxmox-sys = "1"
proxmox-sortable-macro = "1"
+proxmox-wireguard = { version = "0.1", features = [ "api-types" ] }
[features]
frr = ["dep:proxmox-frr"]
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index 18db1bb..d004806 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -20,6 +20,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-proxmox-serde-1+perl-dev <!nocheck>,
librust-proxmox-sortable-macro-1+default-dev <!nocheck>,
librust-proxmox-sys-1+default-dev <!nocheck>,
+ librust-proxmox-wireguard-0.1+api-types-dev <!nocheck>,
+ librust-proxmox-wireguard-0.1+default-dev <!nocheck>,
librust-regex-1+default-dev (>= 1.7-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
@@ -51,6 +53,8 @@ Depends:
librust-proxmox-serde-1+perl-dev,
librust-proxmox-sortable-macro-1+default-dev,
librust-proxmox-sys-1+default-dev,
+ librust-proxmox-wireguard-0.1+api-types-dev,
+ librust-proxmox-wireguard-0.1+default-dev,
librust-regex-1+default-dev (>= 1.7-~~),
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index c1ec847..fd77426 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1,2 +1,3 @@
pub mod openfabric;
pub mod ospf;
+pub mod wireguard;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
new file mode 100644
index 0000000..7f5d18b
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -0,0 +1,515 @@
+//! WireGuard fabric properties
+//!
+//! The main building blocks of the WireGuard section configuration are Fabrics, Nodes, Interfaces
+//! and Peers.
+//!
+//! ## Nodes
+//!
+//! There are two types of Nodes inside a WireGuard fabric:
+//! * Internal - which represents a Proxmox VE node
+//! * External - which represents anything that is not a Proxmox VE node
+//!
+//! For internal nodes, WireGuard interfaces can be configured, which will create a respective
+//! WireGuard interface on the node.
+//!
+//! External nodes can only contain the public key + endpoint - so even if there are multiple
+//! WireGuard interfaces on the same external peer they have to be configured as separate nodes,
+//! since there is no notion of interfaces for external nodes.
+//!
+//! The main purpose of external nodes is to provide reusable peer definitions for configuring
+//! WireGuard interfaces. For instance, a remote PDM instance can be configured as an external peer
+//! and then referenced in the interface defintions.
+//!
+//! ## Peers
+//!
+//! For every WireGuard interface, peers can be configured. A peer can either reference the
+//! interface of an internal node or an external node. The peer definition is generated
+//! automatically from the information contained in the node section. Specific fields from the node
+//! definition can be overridden in the peer definition, if e.g. a different endpoint is required
+//! for connecting to a node.
+
+use std::ops::{Deref, DerefMut};
+
+use anyhow::Result;
+
+use const_format::concatcp;
+use proxmox_network_types::endpoint::{HostnameOrIpAddr, ServiceEndpoint};
+use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
+use proxmox_schema::api_types::CIDR_SCHEMA;
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater, UpdaterType};
+use proxmox_schema::{
+ api_string_type, const_regex, ApiType, ArraySchema, ObjectSchema, Schema, StringSchema,
+};
+use proxmox_sdn_types::wireguard::PersistentKeepalive;
+use proxmox_wireguard::PublicKey;
+use serde::{Deserialize, Serialize};
+
+use crate::sdn::fabric::section_config::node::NodeId;
+
+pub const WIREGUARD_INTERFACE_NAME_REGEX_STR: &str = "[a-zA-Z0-9][a-zA-Z0-9-]{0,6}[a-zA-Z0-9]?";
+
+const_regex! {
+ pub WIREGUARD_INTERFACE_NAME_REGEX = concatcp!(r"^", WIREGUARD_INTERFACE_NAME_REGEX_STR, r"$");
+}
+
+pub const WIREGUARD_INTERFACE_NAME_FORMAT: ApiStringFormat =
+ ApiStringFormat::Pattern(&WIREGUARD_INTERFACE_NAME_REGEX);
+
+api_string_type! {
+ /// Name of a WireGuard network interface.
+ ///
+ /// The interface name can have a maximum of 8 characters. The characterset is restricted (as
+ /// opposed to the other fabric types which can reference arbitrary interfaces on the host),
+ /// since this name is used in filenames - among other places.
+ #[api(
+ min_length: 1,
+ max_length: 8,
+ format: &WIREGUARD_INTERFACE_NAME_FORMAT,
+ )]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct WireGuardInterfaceName(String);
+}
+
+/// Global properties for a WireGuard fabric.
+#[api]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater, Hash)]
+pub struct WireGuardProperties {
+ /// Persistent keepalive interval.
+ #[serde(skip_serializing_if = "persistent_keepalive_is_off")]
+ pub(crate) persistent_keepalive: Option<PersistentKeepalive>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum WireGuardDeletableProperties {
+ PersistentKeepalive,
+}
+
+/// A node in the WireGuard fabric config.
+///
+/// Can be either internal (= PVE node that is part of the current cluster) or external (= any
+/// other peer that is running WireGuard). For more information see the respective structs or
+/// module-level documentation.
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "role")]
+pub enum WireGuardNode {
+ Internal(InternalWireGuardNode),
+ External(ExternalWireGuardNode),
+}
+
+impl WireGuardNode {
+ /// An iterator over the subnets that are allowed for this WireGuard node.
+ pub fn allowed_ips(&self) -> impl Iterator<Item = &Cidr> {
+ match self {
+ WireGuardNode::Internal(internal_wire_guard_node) => {
+ internal_wire_guard_node.allowed_ips.iter()
+ }
+ WireGuardNode::External(external_wire_guard_node) => {
+ external_wire_guard_node.allowed_ips.iter()
+ }
+ }
+ }
+}
+
+impl ApiType for WireGuardNode {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "Wireguard Node",
+ &[
+ (
+ "allowed_ips",
+ true,
+ &ArraySchema::new(
+ "A list of CIDRs that are routed via this WireGuard node.",
+ &CIDR_SCHEMA,
+ )
+ .schema(),
+ ),
+ (
+ "interfaces",
+ true,
+ &ArraySchema::new(
+ "The WireGuard interfaces that should be created on this node.",
+ &StringSchema::new("WireGuard Interface definition.")
+ .format(&ApiStringFormat::PropertyString(
+ &WireGuardInterfaceProperties::API_SCHEMA,
+ ))
+ .schema(),
+ )
+ .schema(),
+ ),
+ (
+ "peers",
+ true,
+ &ArraySchema::new(
+ "The peers that should be created on this node.",
+ &StringSchema::new("wireguard iface")
+ .format(&ApiStringFormat::PropertyString(
+ &WireGuardNodePeer::API_SCHEMA,
+ ))
+ .schema(),
+ )
+ .schema(),
+ ),
+ ],
+ )
+ // TODO: not using a OneOf schema here, because it currently cannot handle properties that are
+ // optional on one variant, but not on the other. To work around this we have to use
+ // ObjectSchema with additional_properties until fixed in proxmox-schema.
+ .additional_properties(true)
+ .schema();
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "role")]
+pub enum WireGuardNodeUpdater {
+ Internal(InternalWireGuardNodeUpdater),
+ External(ExternalWireGuardNodeUpdater),
+}
+
+impl Updater for WireGuardNodeUpdater {
+ fn is_empty(&self) -> bool {
+ match self {
+ WireGuardNodeUpdater::Internal(updater) => updater.is_empty(),
+ WireGuardNodeUpdater::External(updater) => updater.is_empty(),
+ }
+ }
+}
+
+impl UpdaterType for WireGuardNode {
+ type Updater = WireGuardNodeUpdater;
+}
+
+#[api(
+ properties: {
+ allowed_ips: {
+ type: Array,
+ optional: true,
+ items: {
+ type: Cidr,
+ }
+ }
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash, Updater)]
+/// A node that represents an external Wireguard peer.
+///
+/// It can be used to store the configuration of a peer and reuse it across multiple nodes, without
+/// having to re-enter the peer information for every Wireguard interface.
+pub struct ExternalWireGuardNode {
+ /// The public key used by this node.
+ pub(crate) public_key: PublicKey,
+
+ /// The endpoint used for connecting to this node.
+ pub(crate) endpoint: ServiceEndpoint,
+
+ /// a list of IPs that are allowed for this peer
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) allowed_ips: Vec<Cidr>,
+}
+
+/// A node that represents a member of the current cluster.
+///
+/// It contains information about the interfaces that should be created on the node, as well as
+/// their peers.
+///
+/// The additional properties, like endpoint or allowed_ips, can be used to define the settings
+/// when using this node as a peer inside the fabric.
+#[api(
+ properties: {
+ interfaces: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "WireGuard interface properties.",
+ format: &ApiStringFormat::PropertyString(&WireGuardInterfaceProperties::API_SCHEMA),
+ }
+ },
+ peers: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "WireGuard peer properties.",
+ format: &ApiStringFormat::PropertyString(&WireGuardNodePeer::API_SCHEMA),
+ }
+ },
+ allowed_ips: {
+ type: Array,
+ optional: true,
+ items: {
+ type: Cidr,
+ }
+ },
+ }
+)]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater, Hash)]
+#[serde(rename_all = "snake_case")]
+pub struct InternalWireGuardNode {
+ /// The endpoint used for connecting to this node.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) endpoint: Option<HostnameOrIpAddr>,
+
+ /// The interfaces that should get created on this node.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) interfaces: Vec<PropertyString<WireGuardInterfaceProperties>>,
+
+ /// The peers that should get created for interfaces on this node.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) peers: Vec<PropertyString<WireGuardNodePeer>>,
+
+ /// A list of IPs that are routable via this node in the WireGuard fabric.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) allowed_ips: Vec<Cidr>,
+}
+
+impl InternalWireGuardNode {
+ /// Returns an iterator over all wireguard interfaces on this node.
+ pub fn peers(&self) -> impl Iterator<Item = &WireGuardNodePeer> {
+ self.peers
+ .iter()
+ .map(|property_string| property_string.deref())
+ }
+
+ /// Returns an iterator over all wireguard interfaces on this node.
+ pub fn interfaces(&self) -> impl Iterator<Item = &WireGuardInterfaceProperties> {
+ self.interfaces
+ .iter()
+ .map(|property_string| property_string.deref())
+ }
+
+ /// Returns an iterator over all wireguard interfaces on this node (mutable).
+ pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut WireGuardInterfaceProperties> {
+ self.interfaces
+ .iter_mut()
+ .map(|property_string| property_string.deref_mut())
+ }
+}
+
+#[api(
+ properties: {
+ allowed_ips: {
+ type: Array,
+ optional: true,
+ items: {
+ type: Cidr,
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash, Updater)]
+/// A peer definition for a internal WireGuard node.
+///
+/// It references the interface of an internal node. Settings are then automatically taken from the
+/// respective node configuration. Additional properties can be set here to override the
+/// information for the node for this specific peering instance.
+pub struct InternalPeer {
+ /// The name of the node
+ pub(crate) node: NodeId,
+ /// The name of the interface on the node
+ pub(crate) node_iface: WireGuardInterfaceName,
+ /// The local interface that uses this peering definition
+ pub(crate) iface: WireGuardInterfaceName,
+ /// Override for the endpoint settings in the node section.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) endpoint: Option<HostnameOrIpAddr>,
+ /// Additional allowed IPs for this peer
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) allowed_ips: Vec<Cidr>,
+ /// whether to auto-generate routes for the allowed IPs
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ )]
+ #[updater(serde(
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ ))]
+ pub(crate) skip_route_generation: Option<bool>,
+}
+
+#[api(
+ properties: {
+ allowed_ips: {
+ type: Array,
+ optional: true,
+ items: {
+ type: Cidr,
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash, Updater)]
+/// A peer definition for a external WireGuard node.
+///
+/// They reference an external node via the name. The properties here can be used to override the
+/// settings in the node definition.
+pub struct ExternalPeer {
+ /// The name of the external peer.
+ pub(crate) node: NodeId,
+ /// Override for the endpoint settings in the node section.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) endpoint: Option<ServiceEndpoint>,
+ /// The local interface that uses this peering definition
+ pub(crate) iface: WireGuardInterfaceName,
+ /// Additional allowed IPs for this peer
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ #[updater(serde(skip_serializing_if = "Option::is_none"))]
+ pub(crate) allowed_ips: Vec<Cidr>,
+ /// whether to auto-generate routes for the allowed IPs
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ )]
+ #[updater(serde(
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ ))]
+ pub(crate) skip_route_generation: Option<bool>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Hash, Updater)]
+#[serde(rename_all = "snake_case", tag = "type")]
+/// A peer entry in a node's section config.
+///
+/// References either an internal peer or an external peer from the node sections.
+pub enum WireGuardNodePeer {
+ Internal(InternalPeer),
+ External(ExternalPeer),
+}
+
+impl ApiType for WireGuardNodePeer {
+ const API_SCHEMA: Schema = ObjectSchema::new("wireguard node peer", &[])
+ .additional_properties(true)
+ .schema();
+}
+
+impl WireGuardNodePeer {
+ pub fn iface(&self) -> &WireGuardInterfaceName {
+ match self {
+ WireGuardNodePeer::Internal(internal_peer) => &internal_peer.iface,
+ WireGuardNodePeer::External(external_peer) => &external_peer.iface,
+ }
+ }
+
+ pub fn node(&self) -> &NodeId {
+ match self {
+ WireGuardNodePeer::Internal(internal_peer) => &internal_peer.node,
+ WireGuardNodePeer::External(external_peer) => &external_peer.node,
+ }
+ }
+
+ pub fn skip_route_generation(&self) -> bool {
+ match self {
+ WireGuardNodePeer::Internal(internal_peer) => &internal_peer.skip_route_generation,
+ WireGuardNodePeer::External(external_peer) => &external_peer.skip_route_generation,
+ }
+ .unwrap_or_default()
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum WireGuardNodeDeletableProperties {
+ Interfaces,
+ Endpoint,
+ Peers,
+ AllowedIps,
+}
+
+/// Properties of a WireGuard interface.
+#[api()]
+#[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+pub struct WireGuardInterfaceProperties {
+ /// Name for this WireGuard interface.
+ pub(crate) name: WireGuardInterfaceName,
+
+ /// Listen port of the WireGuard interface.
+ pub(crate) listen_port: u16,
+
+ /// Public Key of this interface
+ pub(crate) public_key: PublicKey,
+
+ /// If ip and ip6 are unset, then this is an point-to-point interface.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Cidr>,
+
+ /// If ip6 and ip are unset, then this is an point-to-point interface.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6: Option<Ipv6Cidr>,
+
+ /// whether to generate an IPv6 link-local address for this interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6_ll: Option<bool>,
+}
+
+impl WireGuardInterfaceProperties {
+ /// Get the name of the interface.
+ pub fn name(&self) -> &WireGuardInterfaceName {
+ &self.name
+ }
+
+ /// Set the name of the interface.
+ pub fn set_name(&mut self, name: WireGuardInterfaceName) {
+ self.name = name
+ }
+
+ /// Get the ip (IPv4) of the interface.
+ pub fn ip(&self) -> Option<&Ipv4Cidr> {
+ self.ip.as_ref()
+ }
+
+ /// Get the ip6 (IPv6) of the interface.
+ pub fn ip6(&self) -> Option<&Ipv6Cidr> {
+ self.ip6.as_ref()
+ }
+}
+
+/// Determines whether the given `PersistentKeepalive` value means that it is
+/// turned off. Useful for usage with serde's `skip_serializing_if`.
+fn persistent_keepalive_is_off(value: &Option<PersistentKeepalive>) -> bool {
+ value
+ .as_ref()
+ .map(PersistentKeepalive::is_off)
+ .unwrap_or(true)
+}
+
+/// Properties of a WireGuard interface, when creating it from the API.
+///
+/// This makes public_key optional, since it isn't included for new interfaces, because it gets
+/// generated automatically when creating the interface.
+#[api()]
+#[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+pub struct WireGuardInterfaceCreateProperties {
+ /// Name for this WireGuard interface.
+ pub(crate) name: WireGuardInterfaceName,
+
+ /// Listen port of the WireGuard interface.
+ pub(crate) listen_port: u16,
+
+ /// Public Key of this interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) public_key: Option<PublicKey>,
+
+ /// If ip and ip6 are unset, then this is an point-to-point interface.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Cidr>,
+
+ /// If ip6 and ip are unset, then this is an point-to-point interface.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6: Option<Ipv6Cidr>,
+
+ /// whether to generate an IPv6 link-local address for this interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6_ll: Option<bool>,
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 06/29] ve-config: wireguard: add private keys section config
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (4 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 05/29] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 07/29] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
` (25 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
This section configuration file acts as the key storage for all nodes
in all wireguard fabrics. This is possible, because interface names
are required to be unique for a node across all fabrics (since they
will be created with the respective name).
There is also a helper struct, that can be used for further parsing
the section config format into a more structured version. This struct
can be used for performing CRUD operations, and will be exposed to
perl via pve-rs.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../section_config/protocol/wireguard.rs | 234 ++++++++++++++++++
1 file changed, 234 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
index 7f5d18b..4005f31 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -513,3 +513,237 @@ pub struct WireGuardInterfaceCreateProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) ip6_ll: Option<bool>,
}
+
+pub mod private_keys {
+ use std::collections::btree_map::Entry;
+ use std::collections::{BTreeMap, HashMap, HashSet};
+
+ use anyhow::Error;
+ use serde::{Deserialize, Serialize};
+
+ use proxmox_schema::{api, ApiStringFormat, PropertyString};
+ use proxmox_section_config::typed::SectionConfigData;
+ use proxmox_wireguard::{PrivateKey, PublicKey};
+
+ use crate::sdn::fabric::section_config::{
+ node::{Node, NodeId, NODE_ID_FORMAT},
+ protocol::wireguard::{WireGuardInterfaceName, WireGuardNode},
+ };
+ use crate::sdn::fabric::FabricConfig;
+
+ #[api()]
+ #[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+ /// A private key for a wireguard interface
+ pub struct InterfacePrivateKey {
+ name: WireGuardInterfaceName,
+ key: PrivateKey,
+ }
+
+ impl InterfacePrivateKey {
+ pub fn new(name: WireGuardInterfaceName, key: PrivateKey) -> Self {
+ Self { name, key }
+ }
+ }
+
+ #[api(
+ properties: {
+ private_keys: {
+ type: Array,
+ description: "A list of private keys for this node.",
+ items: {
+ type: String,
+ description: "A private key for a wireguard interface.",
+ format: &ApiStringFormat::PropertyString(&InterfacePrivateKey::API_SCHEMA),
+ }
+ }
+ }
+ )]
+ #[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+ /// The private keys for a node in a wireguard fabric.
+ pub struct NodePrivateKeysSection {
+ private_keys: Vec<PropertyString<InterfacePrivateKey>>,
+ }
+
+ impl FromIterator<InterfacePrivateKey> for NodePrivateKeysSection {
+ fn from_iter<T: IntoIterator<Item = InterfacePrivateKey>>(iter: T) -> Self {
+ Self {
+ private_keys: iter.into_iter().map(PropertyString::new).collect(),
+ }
+ }
+ }
+
+ #[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Route Map Section ID",
+ format: &NODE_ID_FORMAT,
+ },
+ "type-key": "type",
+ )]
+ #[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+ /// The private key config for wireguard.
+ #[serde(tag = "type", rename_all = "kebab-case")]
+ pub enum FabricPrivateKeysSectionConfig {
+ /// Private keys for a node.
+ Node(NodePrivateKeysSection),
+ }
+
+ impl From<NodePrivateKeysSection> for FabricPrivateKeysSectionConfig {
+ fn from(value: NodePrivateKeysSection) -> Self {
+ Self::Node(value)
+ }
+ }
+
+ #[derive(Clone, Debug, Serialize, Deserialize, Hash)]
+ pub struct WireGuardPrivateKeys(
+ pub(crate) BTreeMap<NodeId, BTreeMap<WireGuardInterfaceName, PrivateKey>>,
+ );
+
+ impl WireGuardPrivateKeys {
+ /// Creates a Private key for the given (node, interface) if it doesn't exist - then
+ /// returns the public key of the stored private key.
+ pub fn upsert(
+ &mut self,
+ node: NodeId,
+ interface: WireGuardInterfaceName,
+ ) -> Result<PublicKey, anyhow::Error> {
+ Ok(match self.0.entry(node).or_default().entry(interface) {
+ Entry::Vacant(vacant_entry) => {
+ let private_key = PrivateKey::generate()?;
+ let public_key = private_key.public_key();
+
+ vacant_entry.insert(private_key);
+ public_key
+ }
+ Entry::Occupied(occupied_entry) => occupied_entry.get().public_key(),
+ })
+ }
+
+ /// Removes a private key.
+ pub fn remove(
+ &mut self,
+ node: &NodeId,
+ interface: &WireGuardInterfaceName,
+ ) -> Option<PrivateKey> {
+ if let Some(node_config) = self.0.get_mut(node) {
+ let removed_interface = node_config.remove(interface);
+
+ if node_config.is_empty() {
+ self.0.remove(node);
+ }
+
+ return removed_interface;
+ }
+
+ None
+ }
+
+ /// Return a private key.
+ pub fn get(
+ &self,
+ node: &NodeId,
+ interface: &WireGuardInterfaceName,
+ ) -> Option<&PrivateKey> {
+ self.0.get(node)?.get(interface)
+ }
+
+ /// Removes all entries in the private key configuration that do not exist in the given [`FabricConfig`].
+ pub fn cleanup(&mut self, fabric_config: &FabricConfig) -> Result<(), Error> {
+ let mut private_keys_nodes = HashSet::new();
+ let mut private_keys_interfaces = HashSet::new();
+
+ let mut fabric_config_nodes = HashSet::new();
+ let mut fabric_config_interfaces = HashSet::new();
+
+ for (node_id, node) in fabric_config.all_nodes() {
+ let Node::WireGuard(node) = node else {
+ continue;
+ };
+
+ let WireGuardNode::Internal(node) = node.properties() else {
+ continue;
+ };
+
+ fabric_config_nodes.insert(node_id.clone());
+
+ fabric_config_interfaces.extend(
+ node.interfaces()
+ .map(|interface| (node_id.clone(), interface.name().clone())),
+ );
+ }
+
+ for (node_id, interfaces) in &self.0 {
+ private_keys_nodes.insert(node_id.clone());
+
+ private_keys_interfaces.extend(
+ interfaces
+ .keys()
+ .map(|interface_name| (node_id.clone(), interface_name.clone())),
+ );
+ }
+
+ for node_id in private_keys_nodes.difference(&fabric_config_nodes) {
+ self.0.remove(node_id);
+ }
+
+ for (node_id, interface_id) in
+ private_keys_interfaces.difference(&fabric_config_interfaces)
+ {
+ self.remove(node_id, interface_id);
+ }
+
+ Ok(())
+ }
+ }
+
+ impl From<WireGuardPrivateKeys> for SectionConfigData<FabricPrivateKeysSectionConfig> {
+ fn from(value: WireGuardPrivateKeys) -> Self {
+ let mut data = HashMap::new();
+
+ for (node_id, interfaces) in value.0.into_iter() {
+ data.insert(
+ node_id.to_string(),
+ NodePrivateKeysSection::from_iter(
+ interfaces
+ .into_iter()
+ .map(|(name, key)| InterfacePrivateKey::new(name, key)),
+ )
+ .into(),
+ );
+ }
+
+ Self::from(data)
+ }
+ }
+
+ impl TryFrom<SectionConfigData<FabricPrivateKeysSectionConfig>> for WireGuardPrivateKeys {
+ type Error = anyhow::Error;
+
+ fn try_from(
+ value: SectionConfigData<FabricPrivateKeysSectionConfig>,
+ ) -> Result<Self, Self::Error> {
+ let mut data = BTreeMap::new();
+
+ for (section_id, FabricPrivateKeysSectionConfig::Node(node)) in value {
+ let node_id = NodeId::from_string(section_id)?;
+
+ let interfaces: &mut BTreeMap<WireGuardInterfaceName, PrivateKey> =
+ data.entry(node_id.clone()).or_default();
+
+ for interface in node.private_keys {
+ let interface = interface.into_inner();
+
+ if interfaces
+ .insert(interface.name.clone(), interface.key)
+ .is_some()
+ {
+ anyhow::bail!("duplicate interface {} for node {node_id}", interface.name);
+ }
+ }
+ }
+
+ Ok(Self(data))
+ }
+ }
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 07/29] ve-config: sdn: fabrics: add wireguard to the fabric config
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (5 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 06/29] ve-config: wireguard: add private keys section config Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 08/29] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
` (24 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Use the newly created WireGuard section types to add the WireGuard
variants to the existing fabrics section config enum. By adding the
new WireGuard variants, they are automatically exposed via the API
methods defined in proxmox-perl-rs and are available for the existing
CRUD endpoints in pve-network.
Originally-by: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/frr.rs | 1 +
proxmox-ve-config/src/sdn/fabric/mod.rs | 168 ++++++++++++++++++
.../src/sdn/fabric/section_config/fabric.rs | 25 +++
.../src/sdn/fabric/section_config/mod.rs | 58 ++++++
.../src/sdn/fabric/section_config/node.rs | 32 +++-
5 files changed, 280 insertions(+), 4 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index c4602b5..ca43cf9 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -278,6 +278,7 @@ pub fn build_fabric(
protocol_routemap.v4 = Some(routemap_name);
}
+ FabricEntry::WireGuard(_) => {} // not a frr fabric
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index a14c8ac..e062e50 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -7,6 +7,7 @@ use std::marker::PhantomData;
use std::ops::Deref;
use anyhow::Error;
+use section_config::protocol::wireguard::WireGuardProperties;
use serde::{Deserialize, Serialize};
use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
@@ -28,6 +29,10 @@ use crate::sdn::fabric::section_config::protocol::ospf::{
OspfDeletableProperties, OspfNodeDeletableProperties, OspfNodeProperties,
OspfNodePropertiesUpdater, OspfProperties, OspfPropertiesUpdater,
};
+use crate::sdn::fabric::section_config::protocol::wireguard::{
+ WireGuardDeletableProperties, WireGuardNode, WireGuardNodeDeletableProperties,
+ WireGuardNodeUpdater, WireGuardPropertiesUpdater,
+};
use crate::sdn::fabric::section_config::{FabricOrNode, Section};
#[derive(thiserror::Error, Debug)]
@@ -189,6 +194,7 @@ macro_rules! impl_entry {
impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties);
impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
+impl_entry!(WireGuard, WireGuardProperties, WireGuardNode);
/// All possible entries in a [`FabricConfig`].
///
@@ -198,6 +204,7 @@ impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
pub enum FabricEntry {
Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
Ospf(Entry<OspfProperties, OspfNodeProperties>),
+ WireGuard(Entry<WireGuardProperties, WireGuardNode>),
}
impl FabricEntry {
@@ -209,6 +216,9 @@ impl FabricEntry {
entry.add_node(node_section)
}
(FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+ (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => {
+ entry.add_node(node_section)
+ }
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
@@ -219,6 +229,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.get_node(id),
FabricEntry::Ospf(entry) => entry.get_node(id),
+ FabricEntry::WireGuard(entry) => entry.get_node(id),
}
}
@@ -228,6 +239,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+ FabricEntry::WireGuard(entry) => entry.get_node_mut(id),
}
}
@@ -307,6 +319,109 @@ impl FabricEntry {
Ok(())
}
+ (Node::WireGuard(node_section), NodeUpdater::WireGuard(updater)) => {
+ let NodeDataUpdater::<WireGuardNodeUpdater, WireGuardNodeDeletableProperties> {
+ ip,
+ ip6,
+ properties,
+ delete,
+ } = updater;
+
+ if let Some(ip) = ip {
+ node_section.ip = Some(ip);
+ }
+
+ if let Some(ip) = ip6 {
+ node_section.ip6 = Some(ip);
+ }
+
+ for property in &delete {
+ match property {
+ NodeDeletableProperties::Ip => node_section.ip = None,
+ NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+ // handled below, since internal / external nodes have different properties
+ NodeDeletableProperties::Protocol(_) => continue,
+ }
+ }
+
+ match (node_section.properties_mut(), properties) {
+ (
+ WireGuardNode::Internal(internal_wireguard_node),
+ WireGuardNodeUpdater::Internal(internal_wireguard_node_updater),
+ ) => {
+ if let Some(interfaces) = internal_wireguard_node_updater.interfaces {
+ internal_wireguard_node.interfaces = interfaces;
+ }
+
+ if let Some(endpoint) = internal_wireguard_node_updater.endpoint {
+ internal_wireguard_node.endpoint = Some(endpoint);
+ }
+
+ if let Some(peers) = internal_wireguard_node_updater.peers {
+ internal_wireguard_node.peers = peers;
+ }
+
+ if let Some(allowed_ips) = internal_wireguard_node_updater.allowed_ips {
+ internal_wireguard_node.allowed_ips = allowed_ips;
+ }
+
+ for property in &delete {
+ match property {
+ NodeDeletableProperties::Protocol(protocol_property) => {
+ match protocol_property {
+ WireGuardNodeDeletableProperties::Interfaces => {
+ internal_wireguard_node.interfaces = Vec::new()
+ }
+ WireGuardNodeDeletableProperties::Endpoint => {
+ internal_wireguard_node.endpoint = None
+ }
+ WireGuardNodeDeletableProperties::Peers => {
+ internal_wireguard_node.peers = Vec::new()
+ }
+ WireGuardNodeDeletableProperties::AllowedIps => {
+ internal_wireguard_node.allowed_ips = Vec::new()
+ }
+ }
+ }
+ _ => continue,
+ }
+ }
+ }
+ (
+ WireGuardNode::External(external_wire_guard_node),
+ WireGuardNodeUpdater::External(external_wire_guard_node_updater),
+ ) => {
+ if let Some(endpoint) = external_wire_guard_node_updater.endpoint {
+ external_wire_guard_node.endpoint = endpoint;
+ }
+
+ if let Some(public_key) = external_wire_guard_node_updater.public_key {
+ external_wire_guard_node.public_key = public_key;
+ }
+
+ if let Some(allowed_ips) = external_wire_guard_node_updater.allowed_ips {
+ external_wire_guard_node.allowed_ips = allowed_ips;
+ }
+
+ for property in &delete {
+ match property {
+ NodeDeletableProperties::Protocol(protocol_property) => {
+ match protocol_property {
+ WireGuardNodeDeletableProperties::AllowedIps => {
+ external_wire_guard_node.allowed_ips = Vec::new()
+ }
+ _ => return Err(FabricConfigError::ProtocolMismatch),
+ }
+ }
+ _ => continue,
+ }
+ }
+ }
+ _ => return Err(FabricConfigError::ProtocolMismatch),
+ }
+
+ Ok(())
+ }
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
@@ -316,6 +431,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.nodes.iter(),
FabricEntry::Ospf(entry) => entry.nodes.iter(),
+ FabricEntry::WireGuard(entry) => entry.nodes.iter(),
}
}
@@ -324,6 +440,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.delete_node(id),
FabricEntry::Ospf(entry) => entry.delete_node(id),
+ FabricEntry::WireGuard(entry) => entry.delete_node(id),
}
}
@@ -333,6 +450,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.into_pair(),
FabricEntry::Ospf(entry) => entry.into_pair(),
+ FabricEntry::WireGuard(entry) => entry.into_pair(),
}
}
@@ -341,6 +459,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => &entry.fabric,
FabricEntry::Ospf(entry) => &entry.fabric,
+ FabricEntry::WireGuard(entry) => &entry.fabric,
}
}
@@ -349,6 +468,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => &mut entry.fabric,
FabricEntry::Ospf(entry) => &mut entry.fabric,
+ FabricEntry::WireGuard(entry) => &mut entry.fabric,
}
}
}
@@ -360,6 +480,7 @@ impl From<Fabric> for FabricEntry {
FabricEntry::Openfabric(Entry::new(fabric_section))
}
Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+ Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)),
}
}
}
@@ -541,6 +662,9 @@ impl Validatable for FabricConfig {
return Err(FabricConfigError::DuplicateInterface);
}
}
+ Node::WireGuard(_node_section) => {
+ return Ok(());
+ }
}
}
@@ -712,6 +836,50 @@ impl FabricConfig {
Ok(())
}
+ (Fabric::WireGuard(fabric_section), FabricUpdater::WireGuard(updater)) => {
+ let FabricSectionUpdater::<
+ WireGuardPropertiesUpdater,
+ WireGuardDeletableProperties,
+ > {
+ ip_prefix,
+ ip6_prefix,
+ properties:
+ WireGuardPropertiesUpdater {
+ persistent_keepalive,
+ },
+ delete,
+ } = updater;
+
+ if let Some(prefix) = ip_prefix {
+ fabric_section.ip_prefix = Some(prefix);
+ }
+
+ if let Some(prefix) = ip6_prefix {
+ fabric_section.ip6_prefix = Some(prefix);
+ }
+
+ if let Some(keepalive) = persistent_keepalive {
+ fabric_section.properties.persistent_keepalive = Some(keepalive);
+ }
+
+ for property in delete {
+ match property {
+ FabricDeletableProperties::IpPrefix => {
+ fabric_section.ip_prefix = None;
+ }
+ FabricDeletableProperties::Ip6Prefix => {
+ fabric_section.ip6_prefix = None;
+ }
+ FabricDeletableProperties::Protocol(
+ WireGuardDeletableProperties::PersistentKeepalive,
+ ) => {
+ fabric_section.properties.persistent_keepalive = None;
+ }
+ }
+ }
+
+ Ok(())
+ }
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 38911a6..e92074c 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -16,6 +16,10 @@ use crate::sdn::fabric::section_config::protocol::ospf::{
};
use crate::sdn::fabric::FabricConfigError;
+use super::protocol::wireguard::{
+ WireGuardDeletableProperties, WireGuardProperties, WireGuardPropertiesUpdater,
+};
+
pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
const_regex! {
@@ -139,6 +143,10 @@ impl UpdaterType for FabricSection<OspfProperties> {
type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
}
+impl UpdaterType for FabricSection<WireGuardProperties> {
+ type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>;
+}
+
/// Enum containing all types of fabrics.
///
/// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
@@ -159,6 +167,8 @@ impl UpdaterType for FabricSection<OspfProperties> {
pub enum Fabric {
Openfabric(FabricSection<OpenfabricProperties>),
Ospf(FabricSection<OspfProperties>),
+ #[serde(rename = "wireguard")]
+ WireGuard(FabricSection<WireGuardProperties>),
}
impl UpdaterType for Fabric {
@@ -173,6 +183,7 @@ impl Fabric {
match self {
Self::Openfabric(fabric_section) => fabric_section.id(),
Self::Ospf(fabric_section) => fabric_section.id(),
+ Self::WireGuard(fabric_section) => fabric_section.id(),
}
}
@@ -183,6 +194,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
+ Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(),
}
}
@@ -193,6 +205,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
+ Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
}
}
@@ -203,6 +216,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
+ Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(),
}
}
@@ -213,6 +227,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
+ Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
}
}
}
@@ -225,6 +240,7 @@ impl Validatable for Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.validate(),
Fabric::Ospf(fabric_section) => fabric_section.validate(),
+ Fabric::WireGuard(_fabric_section) => Ok(()),
}
}
}
@@ -241,12 +257,20 @@ impl From<FabricSection<OspfProperties>> for Fabric {
}
}
+impl From<FabricSection<WireGuardProperties>> for Fabric {
+ fn from(section: FabricSection<WireGuardProperties>) -> Self {
+ Fabric::WireGuard(section)
+ }
+}
+
/// Enum containing all updater types for fabrics
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum FabricUpdater {
Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
+ #[serde(rename = "wireguard")]
+ WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater),
}
impl Updater for FabricUpdater {
@@ -254,6 +278,7 @@ impl Updater for FabricUpdater {
match self {
FabricUpdater::Openfabric(updater) => updater.is_empty(),
FabricUpdater::Ospf(updater) => updater.is_empty(),
+ FabricUpdater::WireGuard(updater) => updater.is_empty(),
}
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index d02d4ae..f47a522 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -4,6 +4,7 @@ pub mod node;
pub mod protocol;
use const_format::concatcp;
+use protocol::wireguard::WireGuardProperties;
use serde::{Deserialize, Serialize};
use crate::sdn::fabric::section_config::{
@@ -12,6 +13,7 @@ use crate::sdn::fabric::section_config::{
protocol::{
openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
ospf::{OspfNodeProperties, OspfProperties},
+ wireguard::WireGuardNode,
},
};
@@ -31,8 +33,10 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
match section {
Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
+ Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()),
Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
Section::OspfNode(node_section) => Self::Node(node_section.into()),
+ Section::WireGuardNode(node_section) => Self::Node(node_section.into()),
}
}
}
@@ -62,8 +66,12 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION
pub enum Section {
OpenfabricFabric(FabricSection<OpenfabricProperties>),
OspfFabric(FabricSection<OspfProperties>),
+ #[serde(rename = "wireguard_fabric")]
+ WireGuardFabric(FabricSection<WireGuardProperties>),
OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
OspfNode(NodeSection<OspfNodeProperties>),
+ #[serde(rename = "wireguard_node")]
+ WireGuardNode(NodeSection<WireGuardNode>),
}
impl From<FabricSection<OpenfabricProperties>> for Section {
@@ -78,6 +86,12 @@ impl From<FabricSection<OspfProperties>> for Section {
}
}
+impl From<FabricSection<WireGuardProperties>> for Section {
+ fn from(section: FabricSection<WireGuardProperties>) -> Self {
+ Self::WireGuardFabric(section)
+ }
+}
+
impl From<NodeSection<OpenfabricNodeProperties>> for Section {
fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
Self::OpenfabricNode(section)
@@ -90,11 +104,18 @@ impl From<NodeSection<OspfNodeProperties>> for Section {
}
}
+impl From<NodeSection<WireGuardNode>> for Section {
+ fn from(section: NodeSection<WireGuardNode>) -> Self {
+ Self::WireGuardNode(section)
+ }
+}
+
impl From<Fabric> for Section {
fn from(fabric: Fabric) -> Self {
match fabric {
Fabric::Openfabric(fabric_section) => fabric_section.into(),
Fabric::Ospf(fabric_section) => fabric_section.into(),
+ Fabric::WireGuard(fabric_section) => fabric_section.into(),
}
}
}
@@ -104,6 +125,43 @@ impl From<Node> for Section {
match node {
Node::Openfabric(node_section) => node_section.into(),
Node::Ospf(node_section) => node_section.into(),
+ Node::WireGuard(node_section) => node_section.into(),
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use crate::sdn::fabric::FabricConfig;
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_wireguard_fabric() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_external
+ role external
+ endpoint 192.0.2.1:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+
+wireguard_node: wireg_pve1
+ role internal
+ endpoint 192.0.2.2
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ peers type=internal,node=pve2,node_iface=wg0,iface=wg0
+
+wireguard_node: wireg_pve2
+ role internal
+ endpoint 192.0.2.3
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ peers type=internal,node=pve1,node_iface=wg0,iface=wg0
+"#;
+ let parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ FabricConfig::from_section_config(parsed_config).expect("valid wireguard configuration");
+
+ Ok(())
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index e6d41d9..408a256 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -10,6 +10,7 @@ use proxmox_schema::{
};
use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::wireguard::WireGuardNode;
use crate::sdn::fabric::section_config::{
fabric::{FabricId, FABRIC_ID_REGEX_STR},
protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
@@ -149,8 +150,8 @@ impl<T> NodeSection<T> {
/// Get the IPv4 address (Router-ID) of the [`NodeSection`].
///
/// Either the [`NodeSection::ip`] (IPv4) address or the [`NodeSection::ip6`] (IPv6) address *must*
- /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
- /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+ /// be set. This is checked during the validation, so it's guaranteed. OpenFabric and WireGuard
+ /// can also be used dual-stack, so both IPv4 and IPv6 addresses can be set.
pub fn ip(&self) -> Option<std::net::Ipv4Addr> {
self.ip.as_deref().copied()
}
@@ -158,8 +159,8 @@ impl<T> NodeSection<T> {
/// Get the IPv6 address (Router-ID) of the [`NodeSection`].
///
/// Either the [`NodeSection::ip`] (IPv4) address or the [`NodeSection::ip6`] (IPv6) address *must*
- /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
- /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+ /// be set. This is checked during the validation, so it's guaranteed. OpenFabric and WireGuard
+ /// can also be used dual-stack, so both IPv4 and IPv6 addresses can be set.
pub fn ip6(&self) -> Option<std::net::Ipv6Addr> {
self.ip6.as_deref().copied()
}
@@ -188,6 +189,8 @@ impl<T: ApiType> ApiType for NodeSection<T> {
pub enum Node {
Openfabric(NodeSection<OpenfabricNodeProperties>),
Ospf(NodeSection<OspfNodeProperties>),
+ #[serde(rename = "wireguard")]
+ WireGuard(NodeSection<WireGuardNode>),
}
impl Node {
@@ -196,6 +199,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.id(),
Node::Ospf(node_section) => node_section.id(),
+ Node::WireGuard(node_section) => node_section.id(),
}
}
@@ -204,6 +208,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.ip(),
Node::Ospf(node_section) => node_section.ip(),
+ Node::WireGuard(node_section) => node_section.ip(),
}
}
@@ -212,6 +217,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.ip6(),
Node::Ospf(node_section) => node_section.ip6(),
+ Node::WireGuard(node_section) => node_section.ip6(),
}
}
}
@@ -223,6 +229,7 @@ impl Validatable for Node {
match self {
Node::Openfabric(node_section) => node_section.validate(),
Node::Ospf(node_section) => node_section.validate(),
+ Node::WireGuard(_node_section) => Ok(()),
}
}
}
@@ -239,6 +246,12 @@ impl From<NodeSection<OspfNodeProperties>> for Node {
}
}
+impl From<NodeSection<WireGuardNode>> for Node {
+ fn from(value: NodeSection<WireGuardNode>) -> Self {
+ Self::WireGuard(value)
+ }
+}
+
/// API types for SDN fabric node configurations.
///
/// This module provides specialized types that are used for API interactions when retrieving,
@@ -265,6 +278,7 @@ pub mod api {
OpenfabricNodePropertiesUpdater,
},
ospf::{OspfNodeDeletableProperties, OspfNodeProperties, OspfNodePropertiesUpdater},
+ wireguard::{WireGuardNodeDeletableProperties, WireGuardNodeUpdater},
};
use super::*;
@@ -322,6 +336,8 @@ pub mod api {
pub enum Node {
Openfabric(NodeData<OpenfabricNodeProperties>),
Ospf(NodeData<OspfNodeProperties>),
+ #[serde(rename = "wireguard")]
+ WireGuard(NodeData<WireGuardNode>),
}
impl From<super::Node> for Node {
@@ -329,6 +345,7 @@ pub mod api {
match value {
super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
}
}
}
@@ -338,6 +355,7 @@ pub mod api {
match value {
Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
}
}
}
@@ -351,6 +369,10 @@ pub mod api {
type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
}
+ impl UpdaterType for NodeData<WireGuardNode> {
+ type Updater = NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>;
+ }
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeDataUpdater<T, D> {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -386,6 +408,8 @@ pub mod api {
NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
),
Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+ #[serde(rename = "wireguard")]
+ WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 08/29] ve-config: fabrics: wireguard add validation for wireguard config
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (6 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 07/29] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 09/29] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
` (23 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Implement validation for the invariants of the wireguard
configuration:
* All interfaces referenced in peer definitions must exist
* Listen ports cannot be duplicated
* Interface names must be unique on a node
Wireguard Interface names are validated separately for uniqueness,
since they can be referenced by other fabrics and this would trigger
the duplicate check.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/src/sdn/fabric/mod.rs | 209 ++++++++++++++++--
.../src/sdn/fabric/section_config/node.rs | 2 +-
.../section_config/protocol/wireguard.rs | 63 +++++-
4 files changed, 258 insertions(+), 17 deletions(-)
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 08a4a99..ab7e481 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -33,3 +33,4 @@ frr = ["dep:proxmox-frr"]
[dev-dependencies]
insta = "1.21"
+pretty_assertions = "1.4.0"
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index e062e50..e4ab830 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -31,7 +31,7 @@ use crate::sdn::fabric::section_config::protocol::ospf::{
};
use crate::sdn::fabric::section_config::protocol::wireguard::{
WireGuardDeletableProperties, WireGuardNode, WireGuardNodeDeletableProperties,
- WireGuardNodeUpdater, WireGuardPropertiesUpdater,
+ WireGuardNodePeer, WireGuardNodeUpdater, WireGuardPropertiesUpdater,
};
use crate::sdn::fabric::section_config::{FabricOrNode, Section};
@@ -73,6 +73,14 @@ pub enum FabricConfigError {
OverlappingIp4Prefix(String, String, String, String),
#[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")]
OverlappingIp6Prefix(String, String, String, String),
+ #[error("peer configuration references non-existing local interface '{0}'")]
+ InvalidLocalInterfaceReference(String),
+ #[error("peer configuration references non-existing interface '{0}' on node '{1}'")]
+ InvalidRemoteInterfaceReference(String, String),
+ #[error("peer configuration references non-existing external node '{0}'")]
+ InvalidExternalNodeReference(String),
+ #[error("WireGuard interface listen port duplicated in node configuration: {0}")]
+ DuplicatePort(String),
}
/// An entry in a [`FabricConfig`].
@@ -500,7 +508,92 @@ impl Validatable for FabricEntry {
let mut ips = HashSet::new();
let mut ip6s = HashSet::new();
+ if let FabricEntry::WireGuard(entry) = self {
+ // check if all interfaces referenced by the peer definitions exist inside the
+ // fabric
+ let mut all_interfaces = HashSet::new();
+ let mut all_external_nodes = HashSet::new();
+
+ let mut internal_peers = HashSet::new();
+ let mut external_peers = HashSet::new();
+
+ let mut ipv4_addresses = HashSet::new();
+ let mut ipv6_addresses = HashSet::new();
+
+ for node_id in entry.nodes.keys() {
+ let node_section = entry.node_section(node_id)?;
+
+ match node_section.properties() {
+ WireGuardNode::Internal(node) => {
+ for interface in node.interfaces() {
+ all_interfaces.insert((&node_section.id.node_id, &interface.name));
+
+ // reject any duplicate IPs on interfaces
+ if !interface
+ .ip()
+ .map(|ip| ipv4_addresses.insert(ip))
+ .unwrap_or(true)
+ {
+ return Err(FabricConfigError::DuplicateNodeIp(
+ fabric.id().to_string(),
+ ));
+ }
+
+ if !interface
+ .ip6()
+ .map(|ip6| ipv6_addresses.insert(ip6))
+ .unwrap_or(true)
+ {
+ return Err(FabricConfigError::DuplicateNodeIp(
+ fabric.id().to_string(),
+ ));
+ }
+ }
+
+ for peer in node.peers() {
+ match peer {
+ WireGuardNodePeer::Internal(peer) => {
+ internal_peers.insert((&peer.node, &peer.node_iface))
+ }
+ WireGuardNodePeer::External(peer) => {
+ external_peers.insert(&peer.node)
+ }
+ };
+ }
+ }
+ WireGuardNode::External(_node) => {
+ all_external_nodes.insert(node_section.id().node_id());
+ }
+ }
+ }
+
+ for (node_id, interface) in internal_peers.difference(&all_interfaces) {
+ return Err(FabricConfigError::InvalidRemoteInterfaceReference(
+ interface.to_string(),
+ node_id.to_string(),
+ ));
+ }
+
+ for node_id in external_peers.difference(&all_external_nodes) {
+ return Err(FabricConfigError::InvalidExternalNodeReference(
+ node_id.to_string(),
+ ));
+ }
+ }
+
for (_id, node) in self.nodes() {
+ node.validate()?;
+
+ // Node IPs need to be unique inside a fabric
+ if !node.ip().map(|ip| ips.insert(ip)).unwrap_or(true) {
+ return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
+ }
+
+ // Node IPs need to be unique inside a fabric
+ if !node.ip6().map(|ip| ip6s.insert(ip)).unwrap_or(true) {
+ return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
+ }
+
// Check IPv4 prefix and ip
match (fabric.ip_prefix(), node.ip()) {
(None, Some(ip)) => {
@@ -554,18 +647,6 @@ impl Validatable for FabricEntry {
}
_ => {}
}
-
- // Node IPs need to be unique inside a fabric
- if !node.ip().map(|ip| ips.insert(ip)).unwrap_or(true) {
- return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
- }
-
- // Node IPs need to be unique inside a fabric
- if !node.ip6().map(|ip| ip6s.insert(ip)).unwrap_or(true) {
- return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
- }
-
- node.validate()?;
}
fabric.validate()
@@ -600,6 +681,7 @@ impl Validatable for FabricConfig {
/// - all the ospf fabrics have different areas
/// - IP prefixes of fabrics do not overlap
fn validate(&self) -> Result<(), FabricConfigError> {
+ let mut wireguard_interfaces = HashSet::new();
let mut node_interfaces = HashSet::new();
let mut ospf_area = HashSet::new();
@@ -634,6 +716,7 @@ impl Validatable for FabricConfig {
}
// validate that each (node, interface) combination exists only once across all fabrics
+ // additionally, for wireguard check the listen ports of the interfaces as well
for entry in self.fabrics.values() {
if let FabricEntry::Ospf(entry) = entry {
if !ospf_area.insert(
@@ -662,8 +745,14 @@ impl Validatable for FabricConfig {
return Err(FabricConfigError::DuplicateInterface);
}
}
- Node::WireGuard(_node_section) => {
- return Ok(());
+ Node::WireGuard(node_section) => {
+ if let WireGuardNode::Internal(internal_node) = node_section.properties() {
+ if !internal_node.interfaces().all(|interface| {
+ wireguard_interfaces.insert((node_id, interface.name.as_str()))
+ }) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+ }
}
}
}
@@ -969,3 +1058,93 @@ impl Valid<FabricConfig> {
Section::write_section_config("fabrics.cfg", &self.into_section_config())
}
}
+
+#[cfg(test)]
+mod tests {
+ use crate::sdn::fabric::FabricConfig;
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_wireguard_validation_duplicate_interface() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_internal
+ role internal
+ endpoint 192.0.2.1:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51112,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+"#;
+ let parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ FabricConfig::from_section_config(parsed_config)
+ .expect_err("duplicate interface name on node");
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_wireguard_validation_duplicate_listen_port() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_internal
+ role internal
+ endpoint 192.0.2.1:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg1,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+"#;
+ let parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ FabricConfig::from_section_config(parsed_config)
+ .expect_err("duplicate listen_port on node");
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_wireguard_validation_node_interface_does_not_exist() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_internal
+ role internal
+ endpoint 192.0.2.1:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ peers type=internal,node=invalid,node_iface=invalid,iface=wg0
+"#;
+ let parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ FabricConfig::from_section_config(parsed_config)
+ .expect_err("interface referenced in peer definition does not exist");
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_wireguard_validation_local_interface_does_not_exist() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_internal
+ role internal
+ endpoint 192.0.2.1:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+
+wireguard_node: wireg_internal2
+ role internal
+ endpoint 192.0.2.2:123
+ public_key Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ interfaces name=wg0,listen_port=51111,public_key=Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+ peers type=internal,node=internal,node_iface=wg0,iface=wg1
+"#;
+ let parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ FabricConfig::from_section_config(parsed_config)
+ .expect_err("local interface in peer definition does not exist");
+
+ Ok(())
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 408a256..f2300ac 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -229,7 +229,7 @@ impl Validatable for Node {
match self {
Node::Openfabric(node_section) => node_section.validate(),
Node::Ospf(node_section) => node_section.validate(),
- Node::WireGuard(_node_section) => Ok(()),
+ Node::WireGuard(node_section) => node_section.validate(),
}
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
index 4005f31..f621fb0 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -28,6 +28,7 @@
//! definition can be overridden in the peer definition, if e.g. a different endpoint is required
//! for connecting to a node.
+use std::collections::HashSet;
use std::ops::{Deref, DerefMut};
use anyhow::Result;
@@ -44,7 +45,10 @@ use proxmox_sdn_types::wireguard::PersistentKeepalive;
use proxmox_wireguard::PublicKey;
use serde::{Deserialize, Serialize};
-use crate::sdn::fabric::section_config::node::NodeId;
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::fabric::FabricSection;
+use crate::sdn::fabric::section_config::node::{NodeId, NodeSection};
+use crate::sdn::fabric::FabricConfigError;
pub const WIREGUARD_INTERFACE_NAME_REGEX_STR: &str = "[a-zA-Z0-9][a-zA-Z0-9-]{0,6}[a-zA-Z0-9]?";
@@ -79,6 +83,14 @@ pub struct WireGuardProperties {
pub(crate) persistent_keepalive: Option<PersistentKeepalive>,
}
+impl Validatable for FabricSection<WireGuardProperties> {
+ type Error = FabricConfigError;
+
+ fn validate(&self) -> Result<(), Self::Error> {
+ Ok(())
+ }
+}
+
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WireGuardDeletableProperties {
@@ -159,6 +171,18 @@ impl ApiType for WireGuardNode {
.schema();
}
+impl Validatable for NodeSection<WireGuardNode> {
+ type Error = FabricConfigError;
+
+ fn validate(&self) -> Result<(), Self::Error> {
+ if let WireGuardNode::Internal(node) = self.properties() {
+ return node.validate();
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
#[serde(rename_all = "snake_case", tag = "role")]
pub enum WireGuardNodeUpdater {
@@ -291,6 +315,43 @@ impl InternalWireGuardNode {
}
}
+impl Validatable for InternalWireGuardNode {
+ type Error = FabricConfigError;
+
+ /// Validates the [FabricSection<WireGuardNodeProperties>].
+ fn validate(&self) -> Result<(), Self::Error> {
+ let mut local_interfaces = HashSet::new();
+ let mut listen_ports = HashSet::new();
+
+ for interface in self.interfaces() {
+ // check if interface names are unique
+ if !local_interfaces.insert(&interface.name) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+
+ // check if listen ports are unique
+ if !listen_ports.insert(interface.listen_port) {
+ return Err(FabricConfigError::DuplicatePort(
+ interface.listen_port.to_string(),
+ ));
+ }
+ }
+
+ for peer in self.peers() {
+ if let WireGuardNodePeer::Internal(peer) = peer {
+ // check if referenced local interface exists
+ if !local_interfaces.contains(&peer.iface) {
+ return Err(FabricConfigError::InvalidLocalInterfaceReference(
+ peer.iface.to_string(),
+ ));
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
#[api(
properties: {
allowed_ips: {
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-ve-rs v5 09/29] ve-config: fabrics: implement wireguard config generation
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (7 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 08/29] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 10/29] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
` (22 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Introduce a ConfigBuilder for WireGuard, analogous to the existing
configuration builder for FRR. It uses the proxmox-wireguard crate for
generating WireGuard configuration files, compatible with the `wg(8)`
tool contained in the wireguard-tools package. The
WireGuardConfigBuilder utilizes a loader callback for tests, so the
private key loading can be mocked by test functions.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/debian/control | 2 +
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/wireguard.rs | 309 +++++++++++++++++++++++++
4 files changed, 313 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/wireguard.rs
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index ab7e481..d336b05 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -18,6 +18,7 @@ tracing = "0.1.37"
serde = { workspace = true, features = [ "derive" ] }
serde_json = "1"
+proxmox-base64 = "1"
proxmox-frr = { workspace = true, optional = true }
proxmox-network-types = { workspace = true, features = [ "api-types" ] }
proxmox-schema = { workspace = true, features = [ "api-types" ] }
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index d004806..bd794cc 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -10,6 +10,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-const-format-0.2+default-dev <!nocheck>,
librust-log-0.4+default-dev <!nocheck>,
librust-nix-0.29+default-dev <!nocheck>,
+ librust-proxmox-base64-1+default-dev <!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-schema-5+api-types-dev <!nocheck>,
@@ -43,6 +44,7 @@ Depends:
librust-const-format-0.2+default-dev,
librust-log-0.4+default-dev,
librust-nix-0.29+default-dev,
+ librust-proxmox-base64-1+default-dev,
librust-proxmox-network-types-1+api-types-dev (>= 1.0.1-~~),
librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~),
librust-proxmox-schema-5+api-types-dev,
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 24069ad..2133396 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -3,6 +3,7 @@ pub mod fabric;
pub mod ipam;
pub mod prefix_list;
pub mod route_map;
+pub mod wireguard;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/src/sdn/wireguard.rs b/proxmox-ve-config/src/sdn/wireguard.rs
new file mode 100644
index 0000000..61be336
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/wireguard.rs
@@ -0,0 +1,309 @@
+use std::collections::HashMap;
+use std::ops::Deref;
+
+use anyhow::bail;
+
+use proxmox_network_types::endpoint::ServiceEndpoint;
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use proxmox_sdn_types::wireguard::PersistentKeepalive;
+use proxmox_wireguard::{WireGuardConfig, WireGuardInterface, WireGuardPeer};
+
+use crate::common::valid::Valid;
+use crate::sdn::fabric::section_config::fabric::Fabric;
+use crate::sdn::fabric::section_config::node::Node;
+use crate::sdn::fabric::section_config::protocol::wireguard::private_keys::WireGuardPrivateKeys;
+use crate::sdn::fabric::section_config::protocol::wireguard::{WireGuardNode, WireGuardNodePeer};
+use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
+
+pub struct WireGuardConfigBuilder {
+ fabrics: Valid<FabricConfig>,
+ private_keys: WireGuardPrivateKeys,
+}
+
+impl WireGuardConfigBuilder {
+ pub fn new(fabrics: Valid<FabricConfig>, private_keys: WireGuardPrivateKeys) -> Self {
+ Self {
+ fabrics,
+ private_keys,
+ }
+ }
+
+ pub fn build(
+ self,
+ current_node: NodeId,
+ ) -> Result<HashMap<String, WireGuardConfig>, anyhow::Error> {
+ let mut wireguard_config = HashMap::new();
+
+ for fabric_entry in self.fabrics.values() {
+ let Fabric::WireGuard(fabric_config) = fabric_entry.fabric() else {
+ continue;
+ };
+
+ let Ok(Node::WireGuard(node_config)) = fabric_entry.get_node(¤t_node) else {
+ continue;
+ };
+
+ let WireGuardNode::Internal(node_properties) = node_config.properties() else {
+ continue;
+ };
+
+ for interface in node_properties.interfaces() {
+ let private_key = self
+ .private_keys
+ .get(¤t_node, interface.name())
+ .ok_or_else(|| anyhow::anyhow!("could not find private key for node"))?;
+
+ let wireguard_interface = WireGuardInterface {
+ private_key: *private_key,
+ listen_port: Some(interface.listen_port),
+ fw_mark: None,
+ };
+
+ let mut wireguard_peers = Vec::new();
+
+ for peer in &node_properties.peers {
+ let peer = peer.deref();
+
+ let Ok(Node::WireGuard(referenced_node)) = fabric_entry.get_node(peer.node())
+ else {
+ bail!(
+ "could not find node referenced in peer definition: {}",
+ peer.node()
+ )
+ };
+
+ let peer_config = match (referenced_node.properties(), peer) {
+ (
+ WireGuardNode::Internal(wireguard_node),
+ WireGuardNodePeer::Internal(peer),
+ ) => {
+ if peer.iface != interface.name {
+ continue;
+ }
+
+ let peer_interface = wireguard_node
+ .interfaces()
+ .find(|interface| interface.name == peer.node_iface)
+ .ok_or_else(|| {
+ anyhow::format_err!("could not find referenced iface")
+ })?;
+
+ let endpoint = peer
+ .endpoint
+ .as_ref()
+ .or(wireguard_node.endpoint.as_ref())
+ .map(|endpoint| {
+ ServiceEndpoint::new(
+ endpoint.to_string(),
+ peer_interface.listen_port,
+ )
+ });
+
+ let mut allowed_ips = Vec::new();
+
+ if let Some(ip) = referenced_node.ip() {
+ allowed_ips.push(Ipv4Cidr::from(ip).into())
+ }
+
+ if let Some(ip) = peer_interface.ip() {
+ allowed_ips.push(Ipv4Cidr::new(*ip.address(), 32)?.into())
+ }
+
+ if let Some(ip) = peer_interface.ip6() {
+ allowed_ips.push(Ipv6Cidr::new(*ip.address(), 128)?.into())
+ }
+
+ allowed_ips.extend(&wireguard_node.allowed_ips);
+ allowed_ips.extend(&peer.allowed_ips);
+
+ WireGuardPeer {
+ public_key: peer_interface.public_key,
+ preshared_key: None,
+ allowed_ips,
+ endpoint,
+ persistent_keepalive: fabric_config
+ .properties()
+ .persistent_keepalive
+ .as_ref()
+ .map(PersistentKeepalive::raw),
+ }
+ }
+ (
+ WireGuardNode::External(referenced_node),
+ WireGuardNodePeer::External(peer),
+ ) => {
+ if peer.iface != interface.name {
+ continue;
+ }
+
+ let mut allowed_ips = Vec::new();
+ allowed_ips.extend(&referenced_node.allowed_ips);
+ allowed_ips.extend(&peer.allowed_ips);
+
+ let endpoint = peer
+ .endpoint
+ .clone()
+ .unwrap_or_else(|| referenced_node.endpoint.clone());
+
+ WireGuardPeer {
+ public_key: referenced_node.public_key,
+ preshared_key: None,
+ allowed_ips,
+ endpoint: Some(endpoint),
+ persistent_keepalive: fabric_config
+ .properties()
+ .persistent_keepalive
+ .as_ref()
+ .map(PersistentKeepalive::raw),
+ }
+ }
+ _ => {
+ bail!("invalid combination of peer / node types")
+ }
+ };
+
+ wireguard_peers.push(peer_config);
+ }
+
+ wireguard_config.insert(
+ interface.name.to_string(),
+ WireGuardConfig {
+ interface: wireguard_interface,
+ peers: wireguard_peers,
+ },
+ );
+ }
+ }
+
+ Ok(wireguard_config)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{collections::BTreeMap, str::FromStr};
+
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+ use proxmox_wireguard::PrivateKey;
+
+ use crate::sdn::fabric::{
+ section_config::{protocol::wireguard::WireGuardInterfaceName, Section},
+ FabricConfig,
+ };
+
+ use super::*;
+
+ fn mock_private_key() -> PrivateKey {
+ let bytes: [u8; 32] =
+ proxmox_base64::decode("qGXl+84iE1teMyQeL1DgkuLivKKYasx6fOYqBfr3QEI=")
+ .expect("valid base64")
+ .try_into()
+ .expect("is 32 byte array");
+
+ PrivateKey::from(bytes)
+ }
+
+ fn mock_private_key_data(interfaces: &[&(&str, &str)]) -> WireGuardPrivateKeys {
+ let mut private_keys = BTreeMap::new();
+
+ for (node_id, interface_name) in interfaces {
+ let interfaces: &mut BTreeMap<WireGuardInterfaceName, PrivateKey> = private_keys
+ .entry(NodeId::from_str(node_id).unwrap())
+ .or_default();
+
+ interfaces.insert(
+ WireGuardInterfaceName::from_str(interface_name).unwrap(),
+ mock_private_key(),
+ );
+ }
+
+ WireGuardPrivateKeys(private_keys)
+ }
+
+ #[test]
+ fn test_wireguard_config_generation() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+wireguard_fabric: wireg
+
+wireguard_node: wireg_external
+ role external
+ endpoint 192.0.2.1:123
+ allowed_ips 198.51.0.123/32
+ public_key O+Kzrochm6klMILjSKVw83xb3YyXXLpmZj9n/ICM5xE=
+
+wireguard_node: wireg_pve1
+ role internal
+ endpoint 192.0.2.2
+ allowed_ips 203.0.113.0/25
+ interfaces name=wg0,listen_port=51111,public_key=GDPUAnPOY5xGIjYXmcGyXZXbocjBr21dGQ5vwnjmdzA=,ip=198.51.100.1/24
+ peers type=internal,node=pve2,node_iface=wg0,iface=wg0
+
+wireguard_node: wireg_pve2
+ role internal
+ endpoint 192.0.2.3
+ interfaces name=wg0,listen_port=51111,public_key=y0kOpXfo9ff4KoUwO3H1cRuwObbKwsK8mAkwXxNvKUc=
+ peers type=internal,node=pve1,node_iface=wg0,iface=wg0
+ peers type=external,node=external,iface=wg0
+"#;
+ let mut parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+
+ let mut fabric_config = FabricConfig::from_section_config(parsed_config)
+ .expect("valid wireguard configuration");
+
+ let private_keys = mock_private_key_data(&[&("pve1", "wg0"), &("pve2", "wg0")]);
+
+ let mut builder = WireGuardConfigBuilder::new(fabric_config, private_keys.clone());
+
+ let pve1_wg0_config = r#"[Interface]
+PrivateKey = qGXl+84iE1teMyQeL1DgkuLivKKYasx6fOYqBfr3QEI=
+ListenPort = 51111
+
+[Peer]
+PublicKey = y0kOpXfo9ff4KoUwO3H1cRuwObbKwsK8mAkwXxNvKUc=
+Endpoint = 192.0.2.3:51111
+"#;
+
+ pretty_assertions::assert_eq!(
+ pve1_wg0_config,
+ builder
+ .build("pve1".parse()?)?
+ .remove("wg0")
+ .expect("wg0 config has been generated")
+ .to_raw_config()
+ .expect("wireguard config can be serialized")
+ );
+
+ parsed_config = Section::parse_section_config("fabrics.cfg", section_config)?;
+ fabric_config = FabricConfig::from_section_config(parsed_config)
+ .expect("valid wireguard configuration");
+
+ builder = WireGuardConfigBuilder::new(fabric_config, private_keys);
+
+ let pve2_wg0_config = r#"[Interface]
+PrivateKey = qGXl+84iE1teMyQeL1DgkuLivKKYasx6fOYqBfr3QEI=
+ListenPort = 51111
+
+[Peer]
+PublicKey = GDPUAnPOY5xGIjYXmcGyXZXbocjBr21dGQ5vwnjmdzA=
+AllowedIPs = 198.51.100.1/32, 203.0.113.0/25
+Endpoint = 192.0.2.2:51111
+
+[Peer]
+PublicKey = O+Kzrochm6klMILjSKVw83xb3YyXXLpmZj9n/ICM5xE=
+AllowedIPs = 198.51.0.123/32
+Endpoint = 192.0.2.1:123
+"#;
+
+ pretty_assertions::assert_eq!(
+ pve2_wg0_config,
+ builder
+ .build("pve2".parse()?)?
+ .remove("wg0")
+ .expect("wg0 config has been generated")
+ .to_raw_config()
+ .expect("wireguard config can be serialized")
+ );
+
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-perl-rs v5 10/29] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (8 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 09/29] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 11/29] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
` (21 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
Add support for generating the respective sections in
/etc/network/interfaces.d/sdn for WireGuard interfaces. The method
automatically generates the ifupdown2 configuration for creating the
WireGuard interfaces on the host. The WireGuard specific configuration
is implemented via a post-up command that syncs the generated
configuration via wg(8).
Additionally, routes are created for every configured AdditionalIp in
the WireGuard configuration, when auto_generate_routes is set to 1
(default) in the peer configuration. This happens via respective
post-up / down commands in the ifupdown2 configuration file. This does
not work with duplicate Allowed IPs, two routes are created instead
where the first one 'wins'. This is analogous to the behavior when
configuring the same subnet multiple times in the ifupdown2
configuration. In the future it would be possible to implement ECMP
for this case, by auto-generating next-hop groups for duplicated
allowed IPs. If ECMP via WireGuard is desired, it is currently only
possible via utilizing another routing protocol on top of WireGuard
(BGP or OSPF).
For the new WireGuard variants, stubs are implemented for the status
reporting. This makes all status calls return empty values for
everything. Status reporting will be implemented in a future patch
series.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 179 ++++++++++++++++++++++++-----
pve-rs/src/sdn/status.rs | 29 ++++-
2 files changed, 172 insertions(+), 36 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 18848c4..8eb48a6 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -24,15 +24,21 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_ve_config::common::valid::{Valid, Validatable};
use proxmox_ve_config::sdn::config::{SdnConfig, ZoneConfig};
- use proxmox_ve_config::sdn::fabric::section_config::Section;
use proxmox_ve_config::sdn::fabric::section_config::fabric::{
- Fabric as ConfigFabric, FabricId,
api::{Fabric, FabricUpdater},
+ Fabric as ConfigFabric, FabricId,
};
use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName;
use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId};
+ use proxmox_ve_config::sdn::fabric::section_config::Section;
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
+ use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder;
+ use proxmox_ve_config::sdn::fabric::section_config::protocol::wireguard::{
+ WireGuardInterfaceCreateProperties, WireGuardInterfaceProperties, WireGuardNode,
+ };
+
+ use crate::bindings::sdn::wireguard::pve_rs_sdn_wireguard::PerlWireguardPrivateKeyConfig;
use crate::sdn::status::{self, RunningConfig};
/// A SDN Fabric config instance.
@@ -361,6 +367,7 @@ pub mod pve_rs_sdn_fabrics {
}
}
}
+ ConfigNode::WireGuard(_) => {}
}
}
@@ -453,14 +460,78 @@ pub mod pve_rs_sdn_fabrics {
FabricEntry::Openfabric(_) => {
daemons.insert("fabricd");
}
+ FabricEntry::WireGuard(_) => {} // not a frr fabric
};
}
daemons.into_iter().map(String::from).collect()
}
+ /// Returns the WireGuard configuration for the interfaces of the given node,
+ ///
+ /// It is a hash with the interface name as key and the configuration for that interface
+ /// as a string in INI-style as accepted by wg(8).
+ #[export]
+ pub fn get_wireguard_raw_config(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ #[try_from_ref] private_keys: &PerlWireguardPrivateKeyConfig,
+ ) -> Result<HashMap<String, String>, Error> {
+ let config = this.fabric_config.lock().unwrap().clone().into_valid()?;
+ let private_keys = private_keys.private_keys.lock().unwrap().clone();
+
+ let configs = WireGuardConfigBuilder::new(config, private_keys).build(node_id)?;
+
+ let mut result = HashMap::new();
+
+ for (id, config) in configs {
+ result.insert(id.clone(), config.to_raw_config()?);
+ }
+
+ Ok(result)
+ }
+
+ /// Helper function to generate the section for a WireGuard interface in `/etc/network/interfaces.d/sdn`.
+ fn render_wireguard_interface<'a>(
+ wireguard_interface: &WireGuardInterfaceProperties,
+ allowed_ips: impl Iterator<Item = &'a Cidr>,
+ ) -> Result<String, Error> {
+ let mut interface = String::new();
+ let name = wireguard_interface.name();
+
+ writeln!(interface)?;
+ writeln!(interface, "auto {name}")?;
+
+ if let Some(ip) = wireguard_interface.ip() {
+ writeln!(interface, "iface {name} inet static")?;
+ writeln!(interface, "\taddress {ip}")?;
+ } else {
+ writeln!(interface, "iface {name} inet manual")?;
+ }
+
+ writeln!(interface, "\tlink-type wireguard")?;
+ writeln!(interface, "\tip-forward 1")?;
+ writeln!(
+ interface,
+ "\tpost-up wg syncconf {name} /etc/wireguard/proxmox/{name}.conf"
+ )?;
+
+ for ip in allowed_ips {
+ writeln!(interface, "\tpost-up ip route add {ip} dev {name}")?;
+ writeln!(interface, "\tdown ip route del {ip} dev {name}")?;
+ }
+
+ if let Some(ip) = wireguard_interface.ip6() {
+ writeln!(interface)?;
+ writeln!(interface, "iface {name} inet6 static")?;
+ writeln!(interface, "\taddress {ip}")?;
+ }
+
+ Ok(interface)
+ }
+
/// Helper function to generate the default `/etc/network/interfaces` config for a given CIDR.
- fn render_interface(name: &str, cidr: Cidr, is_dummy: bool) -> Result<String, Error> {
+ fn render_interface(name: &str, cidr: Cidr, link_type: Option<&str>) -> Result<String, Error> {
let mut interface = String::new();
writeln!(interface, "auto {name}")?;
@@ -469,14 +540,42 @@ pub mod pve_rs_sdn_fabrics {
Cidr::Ipv6(_) => writeln!(interface, "iface {name} inet6 static")?,
}
writeln!(interface, "\taddress {cidr}")?;
- if is_dummy {
- writeln!(interface, "\tlink-type dummy")?;
+ if let Some(link_type) = link_type {
+ writeln!(interface, "\tlink-type {link_type}")?;
}
writeln!(interface, "\tip-forward 1")?;
Ok(interface)
}
+ fn render_dummy_interfaces(
+ interfaces: &mut String,
+ fabric: &Fabric,
+ node: &ConfigNode,
+ ) -> Result<(), Error> {
+ if let Some(ip) = node.ip() {
+ let interface = render_interface(
+ &format!("dummy_{}", fabric.id()),
+ Cidr::new_v4(ip, 32)?,
+ Some("dummy"),
+ )?;
+ writeln!(interfaces)?;
+ write!(interfaces, "{interface}")?;
+ }
+
+ if let Some(ip6) = node.ip6() {
+ let interface = render_interface(
+ &format!("dummy_{}", fabric.id()),
+ Cidr::new_v6(ip6, 128)?,
+ Some("dummy"),
+ )?;
+ writeln!(interfaces)?;
+ write!(interfaces, "{interface}")?;
+ }
+
+ Ok(())
+ }
+
/// Method: Generate the ifupdown2 configuration for a given node.
#[export]
pub fn get_interfaces_etc_network_config(
@@ -494,37 +593,20 @@ pub mod pve_rs_sdn_fabrics {
});
for (fabric, node) in node_fabrics {
- // dummy interface
- if let Some(ip) = node.ip() {
- let interface = render_interface(
- &format!("dummy_{}", fabric.id()),
- Cidr::new_v4(ip, 32)?,
- true,
- )?;
- writeln!(interfaces)?;
- write!(interfaces, "{interface}")?;
- }
- if let Some(ip6) = node.ip6() {
- let interface = render_interface(
- &format!("dummy_{}", fabric.id()),
- Cidr::new_v6(ip6, 128)?,
- true,
- )?;
- writeln!(interfaces)?;
- write!(interfaces, "{interface}")?;
- }
+ render_dummy_interfaces(&mut interfaces, fabric, node)?;
+
match node {
ConfigNode::Openfabric(node_section) => {
for interface in node_section.properties().interfaces() {
if let Some(ip) = interface.ip() {
let interface =
- render_interface(interface.name(), Cidr::from(ip), false)?;
+ render_interface(interface.name(), Cidr::from(ip), None)?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
if let Some(ip) = interface.ip6() {
let interface =
- render_interface(interface.name(), Cidr::from(ip), false)?;
+ render_interface(interface.name(), Cidr::from(ip), None)?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
@@ -541,7 +623,7 @@ pub mod pve_rs_sdn_fabrics {
} else {
anyhow::bail!("there has to be a ipv4 or ipv6 node address");
});
- let interface = render_interface(interface.name(), cidr, false)?;
+ let interface = render_interface(interface.name(), cidr, None)?;
writeln!(interfaces)?;
write!(interfaces, "{interface}")?;
}
@@ -549,10 +631,10 @@ pub mod pve_rs_sdn_fabrics {
}
ConfigNode::Ospf(node_section) => {
for interface in node_section.properties().interfaces() {
+ writeln!(interfaces)?;
if let Some(ip) = interface.ip() {
let interface =
- render_interface(interface.name(), Cidr::from(ip), false)?;
- writeln!(interfaces)?;
+ render_interface(interface.name(), Cidr::from(ip), None)?;
write!(interfaces, "{interface}")?;
} else {
let interface = render_interface(
@@ -560,9 +642,41 @@ pub mod pve_rs_sdn_fabrics {
Cidr::from(IpAddr::from(node.ip().ok_or_else(|| {
anyhow::anyhow!("there has to be a ipv4 address")
})?)),
- false,
+ None,
)?;
- writeln!(interfaces)?;
+ write!(interfaces, "{interface}")?;
+ }
+ }
+ }
+ ConfigNode::WireGuard(node_section) => {
+ if let WireGuardNode::Internal(node_properties) = node_section.properties() {
+ for interface in node_properties.interfaces() {
+ let entry = config
+ .get_fabric(fabric.id())
+ // safe because we use the fabric we obtained earlier from the same config
+ .expect("entry for fabric exists in fabric config");
+
+ let allowed_ips = node_properties
+ .peers()
+ .filter_map(|peer| {
+ if peer.skip_route_generation()
+ || peer.iface() != interface.name()
+ {
+ return None;
+ }
+
+ let ConfigNode::WireGuard(node) =
+ entry.get_node(peer.node()).ok()?
+ else {
+ return None;
+ };
+
+ Some(node.properties().allowed_ips())
+ })
+ .flatten();
+
+ let interface = render_wireguard_interface(interface, allowed_ips)?;
+
write!(interfaces, "{interface}")?;
}
}
@@ -659,6 +773,7 @@ pub mod pve_rs_sdn_fabrics {
status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename())
}
+ FabricEntry::WireGuard(_) => Ok(Vec::new()),
}
}
@@ -717,6 +832,7 @@ pub mod pve_rs_sdn_fabrics {
)
.map(|v| v.into())
}
+ FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
}
}
@@ -776,6 +892,7 @@ pub mod pve_rs_sdn_fabrics {
)
.map(|v| v.into())
}
+ FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())),
}
}
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index e1e3362..a54668b 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -80,12 +80,22 @@ mod openfabric {
}
}
+mod wireguard {
+ use serde::Serialize;
+
+ #[derive(Debug, Serialize)]
+ pub struct NeighborStatus;
+ #[derive(Debug, Serialize)]
+ pub struct InterfaceStatus;
+}
+
/// Common NeighborStatus that contains either OSPF or Openfabric neighbors
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum NeighborStatus {
Openfabric(Vec<openfabric::NeighborStatus>),
Ospf(Vec<ospf::NeighborStatus>),
+ WireGuard(Vec<wireguard::NeighborStatus>),
}
impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
@@ -105,6 +115,7 @@ impl From<Vec<ospf::NeighborStatus>> for NeighborStatus {
pub enum InterfaceStatus {
Openfabric(Vec<openfabric::InterfaceStatus>),
Ospf(Vec<ospf::InterfaceStatus>),
+ WireGuard(Vec<wireguard::InterfaceStatus>),
}
impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus {
@@ -135,6 +146,8 @@ pub enum Protocol {
Openfabric,
/// OSPF
Ospf,
+ /// WireGuard
+ WireGuard,
}
/// The status of a fabric.
@@ -217,6 +230,7 @@ pub fn get_routes(
.interfaces()
.map(|i| i.name().as_str())
.collect(),
+ ConfigNode::WireGuard(_) => HashSet::new(),
};
let dummy_interface = format!("dummy_{}", fabric_id.as_str());
@@ -429,6 +443,7 @@ pub fn get_status(
let (current_protocol, all_routes) = match &node {
ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0),
+ ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()),
};
// get interfaces
@@ -443,6 +458,7 @@ pub fn get_status(
.interfaces()
.map(|i| i.name().as_str())
.collect(),
+ ConfigNode::WireGuard(_n) => HashSet::new(),
};
// determine status by checking if any routes exist for our interfaces
@@ -458,13 +474,16 @@ pub fn get_status(
})
});
+ let status = match current_protocol {
+ Protocol::Openfabric if has_routes => FabricStatus::Ok,
+ Protocol::Ospf if has_routes => FabricStatus::Ok,
+ Protocol::WireGuard => FabricStatus::Ok,
+ _ => FabricStatus::NotOk,
+ };
+
let fabric = Status {
ty: "network".to_owned(),
- status: if has_routes {
- FabricStatus::Ok
- } else {
- FabricStatus::NotOk
- },
+ status,
protocol: current_protocol,
network: fabric_id.clone(),
network_type: "fabric".to_string(),
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-perl-rs v5 11/29] pve-rs: fabrics: add helpers for parsing interface property strings
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (9 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 10/29] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 12/29] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
` (20 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
The API methods in pve-network need to be able to parse the property
strings in the CRUD methods. This is because the API methods are
handling key generation for WireGuard interfaces, which require the
name of the interface to detect changes in the interfaces key of
nodes.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 38 ++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 8eb48a6..cac116f 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -20,6 +20,7 @@ pub mod pve_rs_sdn_fabrics {
use perlmod::Value;
use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
+ use proxmox_schema::property_string::PropertyString;
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::{Valid, Validatable};
@@ -335,6 +336,43 @@ pub mod pve_rs_sdn_fabrics {
}
}
+ #[export]
+ /// Parses a wireguard interface property-string (create-only parameters) and returns it as a struct.
+ ///
+ /// Helper for pve-network API methods, that need to parse the property strings for
+ /// handling the WireGuard key generation logic.
+ pub fn parse_wireguard_create_interface(
+ property_string: &str,
+ ) -> Result<WireGuardInterfaceCreateProperties, Error> {
+ Ok(property_string
+ .parse::<PropertyString<WireGuardInterfaceCreateProperties>>()?
+ .into_inner())
+ }
+
+ #[export]
+ /// Parse a wireguard interface property-string and returns it as a struct.
+ ///
+ /// Helper for pve-network API methods, that need to parse the property strings for
+ /// handling the WireGuard key generation logic.
+ pub fn parse_wireguard_interface(
+ property_string: &str,
+ ) -> Result<WireGuardInterfaceProperties, Error> {
+ Ok(property_string
+ .parse::<PropertyString<WireGuardInterfaceProperties>>()?
+ .into_inner())
+ }
+
+ #[export]
+ /// Formats a given wireguard interface as a property string.
+ ///
+ /// Helper for pve-network API methods, that need to print the property strings for
+ /// handling the WireGuard key generation logic.
+ pub fn print_wireguard_interface(
+ wireguard_interface: WireGuardInterfaceProperties,
+ ) -> Result<String, Error> {
+ Ok(PropertyString::new(wireguard_interface).to_property_string()?)
+ }
+
/// Method: Map all interface names of a node to a different one, according to the given
/// mapping.
///
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH proxmox-perl-rs v5 12/29] pve-rs: sdn: wireguard: add private keys module
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (10 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 11/29] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 13/29] sdn: add wireguard helper module Stefan Hanreich
` (19 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
This exposes the implementation of the WireGuard private key storage
to Perl. It can be used to create / delete and list the private keys
stored in the section config file under /etc/pve/priv/wg-keys.cfg.
It also provides a helper method that can be used for cleaning up the
private keys file. It removes all private keys from the section config
that are no longer contained in the fabric config. This can be used
for cleaning up the auto-generated WireGuard private keys when
applying the SDN configuration.
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 | 1 +
pve-rs/src/bindings/sdn/wireguard.rs | 103 +++++++++++++++++++++++++++
4 files changed, 106 insertions(+)
create mode 100644 pve-rs/src/bindings/sdn/wireguard.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 5fbb337..82b4671 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -50,6 +50,7 @@ proxmox-sys = "1"
proxmox-tfa = { version = "6.0.3", features = ["api"] }
proxmox-time = "2"
proxmox-ve-config = { version = "0.7", features = [ "frr" ] }
+proxmox-wireguard = { version = "0.1" }
# [patch.crates-io]
# pbs-api-types = { path = "../../proxmox/pbs-api-types" }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 25642cd..d458293 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -34,6 +34,7 @@ PERLMOD_PACKAGES := \
PVE::RS::SDN::Fabrics \
PVE::RS::SDN::PrefixLists \
PVE::RS::SDN::RouteMaps \
+ PVE::RS::SDN::WireGuard::PrivateKeys \
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 c6361c3..0776ebb 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,6 +1,7 @@
pub(crate) mod fabrics;
pub(crate) mod prefix_lists;
pub(crate) mod route_maps;
+pub(crate) mod wireguard;
#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
pub mod pve_rs_sdn {
diff --git a/pve-rs/src/bindings/sdn/wireguard.rs b/pve-rs/src/bindings/sdn/wireguard.rs
new file mode 100644
index 0000000..ba1ad3f
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/wireguard.rs
@@ -0,0 +1,103 @@
+#[perlmod::package(name = "PVE::RS::SDN::WireGuard::PrivateKeys", lib = "pve_rs")]
+pub mod pve_rs_sdn_wireguard {
+ //! The `PVE::RS::SDN::WireGuard` package.
+ //!
+ //! This provides an abstraction for the WireGuard private key storage
+
+ use std::{ops::Deref, sync::Mutex};
+
+ use anyhow::Error;
+ use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+ use proxmox_wireguard::PublicKey;
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+ use proxmox_ve_config::sdn::fabric::section_config::{
+ node::NodeId,
+ protocol::wireguard::{
+ private_keys::{FabricPrivateKeysSectionConfig, WireGuardPrivateKeys},
+ WireGuardInterfaceName,
+ },
+ };
+
+ use crate::bindings::pve_rs_sdn_fabrics::PerlFabricConfig;
+
+ /// A WireGuard private key config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlWireguardPrivateKeyConfig {
+ /// The fabric config instance
+ pub private_keys: Mutex<WireGuardPrivateKeys>,
+ }
+
+ /// Class method: Parse the raw configuration from `/etc/pve/priv/wg-keys.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 =
+ FabricPrivateKeysSectionConfig::parse_section_config("wg-keys.cfg", raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlWireguardPrivateKeyConfig {
+ private_keys: Mutex::new(config.try_into()?),
+ })),
+ )
+ }
+
+ /// Method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/priv/wg-keys.cfg`
+ #[export]
+ pub fn to_raw(#[try_from_ref] this: &PerlWireguardPrivateKeyConfig) -> Result<String, Error> {
+ let private_keys = this.private_keys.lock().unwrap();
+
+ let raw_config: SectionConfigData<FabricPrivateKeysSectionConfig> =
+ private_keys.deref().clone().into();
+
+ FabricPrivateKeysSectionConfig::write_section_config("wg-keys.cfg", &raw_config)
+ }
+
+ /// Method: Create a WireGuard key, if it doesn't exist.
+ ///
+ /// Returns the public key of the created / existing private key.
+ #[export]
+ pub fn upsert(
+ #[try_from_ref] this: &PerlWireguardPrivateKeyConfig,
+ node: NodeId,
+ interface: WireGuardInterfaceName,
+ ) -> Result<PublicKey, Error> {
+ this.private_keys.lock().unwrap().upsert(node, interface)
+ }
+
+ /// Method: Delete a WireGuard private key.
+ #[export]
+ pub fn delete(
+ #[try_from_ref] this: &PerlWireguardPrivateKeyConfig,
+ node: NodeId,
+ interface: WireGuardInterfaceName,
+ ) -> Result<(), Error> {
+ this.private_keys
+ .lock()
+ .unwrap()
+ .remove(&node, &interface)
+ .map(|_| ())
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "could not find private_key for node {node} and interface {interface}"
+ )
+ })
+ }
+
+ #[export]
+ /// Method: Deletes all private keys from `this` that do not exist in the `fabric_config`.
+ pub fn cleanup(
+ #[try_from_ref] this: &PerlWireguardPrivateKeyConfig,
+ #[try_from_ref] fabric_config: &PerlFabricConfig,
+ ) -> Result<(), Error> {
+ let mut private_key_config = this.private_keys.lock().unwrap();
+ let fabric_config = fabric_config.fabric_config.lock().unwrap();
+
+ private_key_config.cleanup(&fabric_config)
+ }
+
+ perlmod::declare_magic!(Box<PerlWireguardPrivateKeyConfig> : &PerlWireguardPrivateKeyConfig as "PVE::RS::SDN::WireGuard::Config");
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-network v5 13/29] sdn: add wireguard helper module
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (11 preceding siblings ...)
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 12/29] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 14/29] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
` (18 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
A new module that contains helper functions for dealing with WireGuard
config and key generation. They are later used in the API methods, as
well as the SDN commit_config.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
src/PVE/Network/SDN/Makefile | 3 +-
src/PVE/Network/SDN/WireGuard.pm | 176 +++++++++++++++++++++++++++++++
2 files changed, 178 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/Network/SDN/WireGuard.pm
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index 2a476ce9..d0b4bce2 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -10,7 +10,8 @@ SOURCES=Vnets.pm\
Fabrics.pm\
Frr.pm\
PrefixLists.pm\
- RouteMaps.pm
+ RouteMaps.pm\
+ WireGuard.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/WireGuard.pm b/src/PVE/Network/SDN/WireGuard.pm
new file mode 100644
index 00000000..9566616e
--- /dev/null
+++ b/src/PVE/Network/SDN/WireGuard.pm
@@ -0,0 +1,176 @@
+package PVE::Network::SDN::WireGuard;
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+C<PVE::Network::SDN::WireGuard> - Helper module for WireGuard
+
+=head1 DESCRIPTION
+
+This module contains helpers for handling and applying WireGuard configuration.
+
+=cut
+
+use File::Basename;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
+use PVE::File;
+use PVE::INotify;
+use PVE::RESTEnvironment qw(log_warn);
+use PVE::RS::SDN::WireGuard::PrivateKeys;
+use PVE::Tools qw(file_get_contents file_set_contents run_command);
+
+use PVE::Network::SDN::Fabrics;
+
+my $local_wireguard_lock = "/var/lock/proxmox_wg.lock";
+
+my $wireguard_config_folder = "/etc/wireguard/proxmox";
+my $wireguard_private_key_file = "/etc/pve/priv/wg-keys.cfg";
+
+cfs_register_file(
+ 'priv/wg-keys.cfg', \&parse_wg_keys, \&write_wg_keys,
+);
+
+sub parse_wg_keys {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_wg_keys {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+=head3 private_keys()
+
+Reads and returns the private key configuration for WireGuard.
+
+=cut
+
+sub private_keys {
+ my $private_key_config = cfs_read_file('priv/wg-keys.cfg');
+ return PVE::RS::SDN::WireGuard::PrivateKeys->config($private_key_config);
+}
+
+=head3 write_private_keys($private_key_config)
+
+Writes the given private key configuration to the section config file.
+
+It is the callers responsibility to only call this function when the SDN domain
+lock has been acquired.
+
+=cut
+
+sub write_private_keys {
+ my ($config) = @_;
+ cfs_write_file("priv/wg-keys.cfg", $config->to_raw(), 1);
+}
+
+=head3 cleanup_private_keys($private_keys, $fabric_config)
+
+Removes all keys in $private_keys that are not contained in $fabric_config. This
+is used for cleaning up the WireGuard keys after applying the SDN configuration.
+
+=cut
+
+sub cleanup_private_keys {
+ my ($private_keys, $fabric_config) = @_;
+
+ my $nodename = PVE::INotify::nodename();
+
+ $private_keys = private_keys() if !$private_keys;
+ $fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
+
+ $private_keys->cleanup($fabric_config);
+ write_private_keys($private_keys);
+}
+
+=head3 generate_wireguard_config($apply)
+
+Generates the WireGuard configuration files on the current node, based on the
+current running SDN configuration. If $apply is passed, then the WireGuard
+configuration will be applied via the syncconf command of wg(8).
+
+=cut
+
+sub generate_wireguard_config {
+ my ($apply) = @_;
+
+ my $nodename = PVE::INotify::nodename();
+
+ my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
+ my $private_keys = private_keys();
+
+ my $raw_config = $fabric_config->get_wireguard_raw_config($nodename, $private_keys);
+
+ write_wireguard_config($raw_config, $apply);
+}
+
+=head3 write_wireguard_config($raw_config)
+
+Takes a raw_config of the following format:
+
+ interface_name => "<configuration>"
+
+and generates the respective configuration files in $wireguard_config_folder. If
+$apply is set, then the configuration will be synced via the syncconf command of
+wg(8). This requires the interfaces to exist on the node, otherwise the wg
+command will fail. A warning is emitted in that case.
+
+=cut
+
+sub write_wireguard_config {
+ my ($raw_config, $apply) = @_;
+
+ my $code = sub {
+ mkdir '/etc/wireguard' if !-e '/etc/wireguard';
+ mkdir $wireguard_config_folder if !-e $wireguard_config_folder;
+
+ my $has_wireguard_config = scalar($raw_config->%*);
+ my $is_wireguard_installed = -e "/usr/bin/wg";
+
+ if (!$is_wireguard_installed && $has_wireguard_config) {
+ log_warn("In order to apply the generated WireGuard configuration the package 'wireguard-tools' needs to be installed.\n");
+ }
+
+ PVE::Tools::dir_glob_foreach(
+ $wireguard_config_folder,
+ '.*\.conf',
+ sub {
+ my ($file) = @_;
+ unlink "$wireguard_config_folder/$file";
+ },
+ );
+
+ for my $interface (keys $raw_config->%*) {
+ PVE::File::file_set_contents(
+ "$wireguard_config_folder/$interface.conf",
+ $raw_config->{$interface},
+ 400,
+ );
+
+ if ($apply && $is_wireguard_installed) {
+ eval {
+ PVE::Tools::run_command(
+ [
+ 'wg',
+ 'syncconf',
+ $interface,
+ "/etc/wireguard/proxmox/$interface.conf",
+ ],
+ );
+ };
+
+ log_warn($@) if $@;
+ }
+ }
+ };
+
+ PVE::Tools::lock_file($local_wireguard_lock, 10, $code);
+ die $@ if $@;
+
+ return;
+}
+
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-network v5 14/29] fabrics: wireguard: add schema definitions for wireguard
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (12 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-network v5 13/29] sdn: add wireguard helper module Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 15/29] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
` (17 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Add the newly introduced properties for fabrics / nodes to the
existing schema definition. The existing fabric / node endpoints will
then work with the new WireGuard entities, without any additional
changes. To properly detect changes in the peers property, which is an
array, it needs to be added to the encode_value function as well,
which is used for comparing the pending configuration to the running
configuration.
Originally-by: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 2 +-
src/PVE/Network/SDN.pm | 2 +
src/PVE/Network/SDN/Fabrics.pm | 180 +++++++++++++++++++++++++++++++--
3 files changed, 177 insertions(+), 7 deletions(-)
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 03746e8b..6b73af25 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -125,7 +125,7 @@ my $create_reload_network_worker = sub {
}
},
);
- #my $upid = PVE::API2::Network->reload_network_config(node => $nodename});
+ #my $upid = PVE::API2::Network->reload_network_config({ node => $nodename });
my $res = PVE::Tools::upid_decode($upid);
return $res->{pid};
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 53a12c26..74c0edb2 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -511,6 +511,8 @@ sub encode_value {
|| $key eq 'entries'
|| $key eq 'match'
|| $key eq 'set'
+ || $key eq 'peers'
+ || $key eq 'allowed_ips'
) {
if (ref($value) eq 'HASH') {
return join(',', sort keys(%$value));
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index bc295f0f..d98e2e27 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -51,7 +51,44 @@ PVE::JSONSchema::register_standard_option(
{
description => "Type of configuration entry in an SDN Fabric section config",
type => 'string',
- enum => ['openfabric', 'ospf'],
+ enum => ['openfabric', 'ospf', 'wireguard'],
+ },
+);
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-fabric-wireguard-interface',
+ {
+ name => {
+ type => 'string',
+ format => 'pve-iface',
+ description => 'Name of the network interface',
+ },
+ public_key => {
+ type => 'string',
+ description => 'The public key of this interface',
+ optional => 1,
+ },
+ ip => {
+ type => 'string',
+ format => 'CIDRv4',
+ description => 'IPv4 address for this node',
+ optional => 1,
+ },
+ ip6 => {
+ type => 'string',
+ format => 'CIDRv6',
+ description => 'IPv6 address for this node',
+ optional => 1,
+ },
+ listen_port => {
+ type => 'number',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
+ description => 'Port to listen on for WireGuard traffic.',
+ optional => 1,
+ minimum => 1,
+ maximum => 65535,
+ },
},
);
@@ -202,18 +239,125 @@ sub node_properties {
description => 'OSPF network interface',
optional => 1,
},
+ {
+ type => 'array',
+ 'instance-types' => ['wireguard'],
+ items => {
+ description =>
+ "WireGuard network interface",
+ type => 'string',
+ format => 'pve-sdn-fabric-wireguard-interface',
+ },
+ description => 'List of WireGuard network interfaces for this node.',
+ optional => 1,
+ },
],
},
- };
-
- if ($update) {
- $properties->{delete} = {
+ public_key => {
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
+ description => 'The public key for the external node.',
+ type => 'string',
+ optional => 1,
+ },
+ role => {
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
+ description => 'The role of this node in the WireGuard fabric.',
+ type => 'string',
+ enum => ['internal', 'external'],
+ optional => 1,
+ },
+ endpoint => {
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
+ description => 'The endpoint used for connecting to this node.',
+ optional => 1,
+ type => 'string',
+ },
+ allowed_ips => {
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
type => 'array',
+ optional => 1,
+ description =>
+ 'A list of IPs that are routable via this node in the WireGuard fabric.',
items => {
type => 'string',
- enum => ['interfaces', 'ip', 'ip6'],
+ format => 'CIDR',
},
+ },
+ peers => {
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
optional => 1,
+ type => 'array',
+ items => {
+ type => 'string',
+ format => {
+ type => {
+ type => 'string',
+ enum => ['internal', 'external'],
+ },
+ node => {
+ description =>
+ 'The name of the peer (if external) or the name of the node and interface (if internal).',
+ type => 'string',
+ },
+ node_iface => {
+ description =>
+ 'The interface of the other node, if it is internal',
+ type => 'string',
+ optional => 1,
+ },
+ iface => {
+ description =>
+ 'The interface of this node that uses this peer definition.',
+ type => 'string',
+ },
+ endpoint => {
+ description =>
+ 'Override for the endpoint settings in the node section.',
+ optional => 1,
+ type => 'string',
+ },
+ skip_route_generation => {
+ description =>
+ 'Whether routes for the allowed IPs should be created in the kernel routing table.',
+ optional => 1,
+ default => 0,
+ type => 'boolean',
+ },
+ },
+ },
+ },
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ # coerce this value into an array before parsing (oneOf workaround)
+ type => 'array',
+ 'type-property' => 'protocol',
+ oneOf => [
+ {
+ type => 'array',
+ 'instance-types' => ['openfabric', 'ospf'],
+ items => {
+ type => 'string',
+ enum => ['interfaces', 'ip', 'ip6'],
+ },
+ optional => 1,
+ },
+ {
+ type => 'array',
+ 'instance-types' => ['wireguard'],
+ items => {
+ type => 'string',
+ enum => ['allowed_ips', 'endpoint', 'interfaces', 'ip', 'ip6', 'peers'],
+ },
+ optional => 1,
+ },
+ ],
};
}
@@ -275,6 +419,21 @@ sub fabric_properties {
'A prefix list that should be used for filtering routes that are to be installed into the kernel routing table',
optional => 1,
},
+ persistent_keepalive => {
+ type => 'number',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['wireguard'],
+ description => 'A seconds interval, between 1 and 65535 inclusive, of how often to'
+ . ' send an authenticated empty packet to the peer for the purpose of keeping a'
+ . ' stateful firewall or NAT mapping valid persistently. For example, if the'
+ . ' interface very rarely sends traffic, but it might at anytime receive traffic'
+ . ' from another node, and it is behind NAT, the interface might benefit from'
+ . ' having a persistent keepalive interval of 25 seconds. If unset or set to 0, it'
+ . ' is turned off',
+ optional => 1,
+ minimum => 0,
+ maximum => 65535,
+ },
};
if ($update) {
@@ -301,6 +460,15 @@ sub fabric_properties {
},
optional => 1,
},
+ {
+ type => 'array',
+ 'instance-types' => ['wireguard'],
+ items => {
+ type => 'string',
+ enum => ['persistent_keepalive'],
+ },
+ optional => 1,
+ },
],
};
}
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-network v5 15/29] fabrics: wireguard: implement wireguard key auto-generation
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (13 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-network v5 14/29] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 16/29] network: sdn: generate wireguard configuration on apply Stefan Hanreich
` (16 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Add additional logic to the existing fabrics API endpoints that
automatically create / delete keypairs for wireguard interfaces in
/etc/wireguard/proxmox. Keys are generated when new interfaces are
added - while they are deleted when applying the SDN configuration.
After generating the key, it is stored alongside the user-defined
configuration in the section config. This allows for easy access to
the public key of other nodes during config generation and when
returning them from the API.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 2 +
.../API2/Network/SDN/Fabrics/FabricNode.pm | 106 +++++++++++++++++-
2 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 6b73af25..e3c8d9dd 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -325,6 +325,8 @@ __PACKAGE__->register_method({
PVE::Network::SDN::commit_config();
$new_config_has_frr = PVE::Network::SDN::running_config_has_frr();
+ PVE::Network::SDN::WireGuard::cleanup_private_keys();
+
PVE::Network::SDN::delete_global_lock() if $lock_token && $release_lock;
},
"could not commit SDN config",
diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
index 000e4c39..2a60bff2 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
@@ -3,11 +3,13 @@ package PVE::API2::Network::SDN::Fabrics::FabricNode;
use strict;
use warnings;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::Tools qw(extract_param);
+use PVE::JSONSchema qw(get_standard_option parse_property_string);
+use PVE::Tools qw(extract_param run_command);
use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::WireGuard;
+use PVE::RS::SDN::Fabrics;
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
@@ -131,6 +133,11 @@ __PACKAGE__->register_method({
},
});
+my sub is_internal_wireguard_node {
+ my ($node) = @_;
+ return $node->{protocol} eq 'wireguard' && $node->{role} eq 'internal';
+}
+
__PACKAGE__->register_method({
name => 'add_node',
path => '',
@@ -162,8 +169,42 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
- $config->add_node($param);
- PVE::Network::SDN::Fabrics::write_config($config);
+ if (is_internal_wireguard_node($param) && $param->{interfaces}) {
+ my $private_keys = PVE::Network::SDN::WireGuard::private_keys();
+
+ my @parsed_interfaces = map {
+ PVE::RS::SDN::Fabrics::parse_wireguard_create_interface($_)
+ } $param->{interfaces}->@*;
+
+ my @interfaces;
+ for my $interface (@parsed_interfaces) {
+ $interface->{public_key} =
+ $private_keys->upsert($param->{node_id}, $interface->{name});
+ push @interfaces,
+ PVE::RS::SDN::Fabrics::print_wireguard_interface($interface);
+ }
+
+ $param->{interfaces} = \@interfaces;
+ $config->add_node($param);
+
+ eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys); };
+ die "could not save private key config: $@\n" if $@;
+
+ eval { PVE::Network::SDN::Fabrics::write_config($config); };
+ if (my $err = $@) {
+ for my $interface (@parsed_interfaces) {
+ $private_keys->delete($param->{node_id}, $interface->{name});
+ }
+
+ eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys) };
+ warn "could not roll back private key config: $@\n" if $@;
+
+ die $err;
+ }
+ } else {
+ $config->add_node($param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }
},
"adding node failed",
$lock_token,
@@ -205,8 +246,59 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
- $config->update_node($fabric_id, $node_id, $param);
- PVE::Network::SDN::Fabrics::write_config($config);
+ my $old_node = $config->get_node($fabric_id, $node_id);
+
+ # required so rust can parse the proper wireguard node
+ # variant
+ $param->{role} = $old_node->{role} if $old_node->{protocol} eq 'wireguard';
+
+ if (is_internal_wireguard_node($param)) {
+ my $private_keys = PVE::Network::SDN::WireGuard::private_keys();
+
+ my %new_interfaces = map {
+ my $interface =
+ PVE::RS::SDN::Fabrics::parse_wireguard_create_interface($_);
+ $interface->{name} => $interface
+ } $param->{interfaces}->@*;
+
+ my %old_interfaces = map {
+ my $interface = PVE::RS::SDN::Fabrics::parse_wireguard_interface($_);
+ $interface->{name} => $interface
+ } $old_node->{interfaces}->@*;
+
+ my @interfaces;
+ for my $interface_name (keys %new_interfaces) {
+ my $interface = $new_interfaces{$interface_name};
+ $interface->{public_key} =
+ $private_keys->upsert($node_id, $interface_name)
+ if !exists($old_interfaces{$interface_name});
+ push @interfaces,
+ PVE::RS::SDN::Fabrics::print_wireguard_interface($interface);
+ }
+ $param->{interfaces} = \@interfaces;
+
+ $config->update_node($fabric_id, $node_id, $param);
+
+ eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys); };
+ die "could not save private key config: $@\n" if $@;
+
+ eval { PVE::Network::SDN::Fabrics::write_config($config); };
+
+ if (my $err = $@) {
+ for my $interface (values %new_interfaces) {
+ $private_keys->delete($node_id, $interface->{name})
+ if !exists($old_interfaces{$interface->{name}});
+ }
+
+ eval { PVE::Network::SDN::WireGuard::write_private_keys($private_keys) };
+ warn "could not roll back private key config: $@\n" if $@;
+
+ die $err;
+ }
+ } else {
+ $config->update_node($fabric_id, $node_id, $param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }
},
"updating node failed",
$lock_token,
@@ -251,6 +343,8 @@ __PACKAGE__->register_method({
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+ my $old_node = $config->get_node($fabric_id, $node_id);
+
$config->delete_node($fabric_id, $node_id);
PVE::Network::SDN::Fabrics::write_config($config);
},
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 16/29] network: sdn: generate wireguard configuration on apply
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (14 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-network v5 15/29] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 17/29] ui: fix parsing of property-strings when values contain = Stefan Hanreich
` (15 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
PVE/API2/Network.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index fc053fec7..d5fb9aed1 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -922,6 +922,7 @@ __PACKAGE__->register_method({
if ($have_sdn) {
PVE::Network::SDN::generate_etc_network_config();
PVE::Network::SDN::generate_dhcp_config();
+ PVE::Network::SDN::WireGuard::generate_wireguard_config();
}
my $err = sub {
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 17/29] ui: fix parsing of property-strings when values contain =
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (15 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 16/29] network: sdn: generate wireguard configuration on apply Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 18/29] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
` (14 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
The old parsing logic utilized split with the limit parameter for
parsing key=value pairs in a property string. String.splt() doesn't
stop at splitting after `limit` occurences of the specified separator,
but rather splits the whole string and then only returns the first
'limit' parts from the result.
This leads to issues with values that contain an equals sign, since
the returned value only includes the string up until the first
occurence of an equals sign. This is particularly problematic when the
value is a base64 string, which are commonly padded by utilizing an
equals sign.
Use indexOf instead to find the first occurence of an equals sign, and
then split the property into key and value at only that index. This
allows for equals signs in values (but not keys).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Parser.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/www/manager6/Parser.js b/www/manager6/Parser.js
index 5e7e87df6..4676a7eca 100644
--- a/www/manager6/Parser.js
+++ b/www/manager6/Parser.js
@@ -54,7 +54,12 @@ Ext.define('PVE.Parser', {
try {
value.split(',').forEach((property) => {
- let [k, v] = property.split('=', 2);
+ let idx = property.indexOf('=');
+ let [k, v] =
+ idx === -1
+ ? [property, null]
+ : [property.substring(0, idx), property.substring(idx + 1)];
+
if (Ext.isDefined(v)) {
res[k] = v;
} else if (Ext.isDefined(defaultKey)) {
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 18/29] ui: fabrics: i18n: make node loading string translatable
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (16 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 17/29] ui: fix parsing of property-strings when values contain = Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 19/29] sdn: fabrics view: handle case where interfaces are deleted Stefan Hanreich
` (13 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/fabrics/NodeEdit.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index 161917ccd..dd8ad0274 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -123,7 +123,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
load: function () {
let me = this;
- me.setLoading('fetching node information');
+ me.setLoading(gettext('fetching node information'));
Promise.all([me.loadNode(me.fabricId, me.nodeId), me.loadNodeInterfaces(me.nodeId)])
.catch(Proxmox.Utils.alertResponseFailure)
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 19/29] sdn: fabrics view: handle case where interfaces are deleted
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (17 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 18/29] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 20/29] ui: fabrics: split node selector creation and config Stefan Hanreich
` (12 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Previously when a fabric had interfaces configured, but they have been
deleted in the pending configuration then the fabric view would fail
to render because the interfaces property is then the string
'deleted', causing the iteration below to fail. Fix this by returning
early and rendering nothing if no interfaces are configured in the
pending configuration but in the running configuration.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/FabricsView.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
index 093a70f35..21b88bab5 100644
--- a/www/manager6/sdn/FabricsView.js
+++ b/www/manager6/sdn/FabricsView.js
@@ -83,6 +83,10 @@ Ext.define('PVE.sdn.Fabric.View', {
renderer: function (value, metaData, rec) {
const interfaces = rec.data.pending?.interfaces || rec.data.interfaces || [];
+ if (interfaces === 'deleted') {
+ return;
+ }
+
let names = interfaces.map((iface) => {
const properties = Proxmox.Utils.parsePropertyString(iface);
return properties.name;
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 20/29] ui: fabrics: split node selector creation and config
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (18 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 19/29] sdn: fabrics view: handle case where interfaces are deleted Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 21/29] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
` (11 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
This allows for overriding the configuration of the node selector,
before it is created. This avoids running into potential race
conditions when adding listeners on load events, which could complete
after the component has been created, but before a child component has
attached its listener.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/fabrics/NodeEdit.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index dd8ad0274..b3d9751aa 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -136,10 +136,10 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
});
},
- getNodeSelector: function () {
+ getNodeSelectorConfig: function() {
let me = this;
- return Ext.create('PVE.form.NodeSelector', {
+ return {
xtype: 'pveNodeSelector',
reference: 'nodeselector',
fieldLabel: gettext('Node'),
@@ -193,7 +193,12 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
},
},
},
- });
+ };
+ },
+
+ getNodeSelector: function () {
+ let me = this;
+ return Ext.create('PVE.form.NodeSelector', me.getNodeSelectorConfig());
},
getInterfacePanel: function (protocol) {
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 21/29] ui: fabrics: edit: make ipv4/6 support generic over fabric panels
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (19 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 20/29] ui: fabrics: split node selector creation and config Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 22/29] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
` (10 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
This allows selectively enabling IPv4/6 in the child components,
without having to re-define the respective fields in every child
component. A later commit will introduce the Wireguard fabric, which
will have neither a IPv4 nor IPv6 prefix for now, so the option to
exclude those fields will be used there as well.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
www/manager6/sdn/fabrics/FabricEdit.js | 68 +++++++++++++++----
.../sdn/fabrics/openfabric/FabricEdit.js | 32 ---------
www/manager6/sdn/fabrics/ospf/FabricEdit.js | 2 +
3 files changed, 58 insertions(+), 44 deletions(-)
diff --git a/www/manager6/sdn/fabrics/FabricEdit.js b/www/manager6/sdn/fabrics/FabricEdit.js
index 07d169287..6111bbb51 100644
--- a/www/manager6/sdn/fabrics/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/FabricEdit.js
@@ -5,8 +5,18 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
width: 400,
fabricId: undefined,
+
+ hasIpv4Support: true,
+ hasIpv6Support: true,
+
baseUrl: '/cluster/sdn/fabrics/fabric',
+ viewModel: {
+ data: {
+ showIpv6ForwardingHint: false,
+ },
+ },
+
items: [
{
xtype: 'textfield',
@@ -24,18 +34,6 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
disabled: '{!isCreate}',
},
},
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv4 Prefix'),
- labelWidth: 120,
- name: 'ip_prefix',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- disabled: '{!isCreate}',
- deleteEmpty: '{!isCreate}',
- },
- },
],
additionalItems: [],
@@ -53,6 +51,52 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
me.url = me.baseUrl;
}
+ if (me.hasIpv4Support) {
+ me.items.push({
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv4 Prefix'),
+ labelWidth: 120,
+ name: 'ip_prefix',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ disabled: '{!isCreate}',
+ deleteEmpty: '{!isCreate}',
+ },
+ });
+ }
+
+ if (me.hasIpv6Support) {
+ me.items.push(
+ {
+ xtype: 'displayfield',
+ value: 'To make IPv6 fabrics work, enable global IPv6 forwarding on all nodes. Click on the Help button for more details.',
+ bind: {
+ hidden: '{!showIpv6ForwardingHint}',
+ },
+ userCls: 'pmx-hint',
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv6 Prefix'),
+ labelWidth: 120,
+ name: 'ip6_prefix',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ disabled: '{!isCreate}',
+ deleteEmpty: '{!isCreate}',
+ },
+ listeners: {
+ change: function (textbox, value) {
+ let vm = textbox.up('window').getViewModel();
+ vm.set('showIpv6ForwardingHint', !!value);
+ },
+ },
+ },
+ );
+ }
+
me.items.push(...me.additionalItems);
me.callParent();
diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
index 0a90700e9..81ef61f52 100644
--- a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
@@ -4,43 +4,11 @@ Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
subject: 'OpenFabric',
onlineHelp: 'pvesdn_openfabric_fabric',
- viewModel: {
- data: {
- showIpv6ForwardingHint: false,
- },
- },
-
extraRequestParams: {
protocol: 'openfabric',
},
additionalItems: [
- {
- xtype: 'displayfield',
- value: 'To make IPv6 fabrics work, enable global IPv6 forwarding on all nodes. Click on the Help button for more details.',
- bind: {
- hidden: '{!showIpv6ForwardingHint}',
- },
- userCls: 'pmx-hint',
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv6 Prefix'),
- labelWidth: 120,
- name: 'ip6_prefix',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- disabled: '{!isCreate}',
- deleteEmpty: '{!isCreate}',
- },
- listeners: {
- change: function (textbox, value) {
- let vm = textbox.up('window').getViewModel();
- vm.set('showIpv6ForwardingHint', !!value);
- },
- },
- },
{
xtype: 'proxmoxintegerfield',
// TRANSLATORS: See https://en.wikipedia.org/wiki/IS-IS#Packet_types
diff --git a/www/manager6/sdn/fabrics/ospf/FabricEdit.js b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
index 61e8822a0..b5d06c629 100644
--- a/www/manager6/sdn/fabrics/ospf/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
@@ -4,6 +4,8 @@ Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
subject: 'OSPF',
onlineHelp: 'pvesdn_ospf_fabric',
+ hasIpv6Support: false,
+
extraRequestParams: {
protocol: 'ospf',
},
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 22/29] ui: fabrics: node: make ipv4/6 support generic over edit panels
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (20 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 21/29] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 23/29] ui: fabrics: interface: " Stefan Hanreich
` (9 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
This allows selectively enabling IPv4/6 in the child components,
without having to re-define the respective fields in every child
component. A later commit will introduce the Wireguard fabric, which
will have neither a IPv4 nor IPv6 prefix for now, so the option to
exclude those fields will be used there as well.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/fabrics/NodeEdit.js | 44 ++++++++++++++-----
.../sdn/fabrics/openfabric/NodeEdit.js | 14 ------
www/manager6/sdn/fabrics/ospf/NodeEdit.js | 2 +
3 files changed, 34 insertions(+), 26 deletions(-)
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index b3d9751aa..7cbea9608 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -11,6 +11,9 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
nodeId: undefined,
protocol: undefined,
+ hasIpv4Support: true,
+ hasIpv6Support: true,
+
disallowedNodes: [],
baseUrl: '/cluster/sdn/fabrics/node',
@@ -22,17 +25,6 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
hidden: true,
allowBlank: true,
},
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv4'),
- labelWidth: 120,
- name: 'ip',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
],
additionalItems: [],
@@ -52,6 +44,34 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
me.url = `${me.baseUrl}/${me.fabricId}`;
}
+ if (me.hasIpv4Support) {
+ me.items.push({
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv4'),
+ labelWidth: 120,
+ name: 'ip',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ });
+ }
+
+ if (me.hasIpv6Support) {
+ me.items.push({
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv6'),
+ labelWidth: 120,
+ name: 'ip6',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ });
+ }
+
me.nodeSelector = me.getNodeSelector();
me.interfaceSelector = me.getInterfaceSelector();
@@ -136,7 +156,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
});
},
- getNodeSelectorConfig: function() {
+ getNodeSelectorConfig: function () {
let me = this;
return {
diff --git a/www/manager6/sdn/fabrics/openfabric/NodeEdit.js b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
index 3a0fafbbd..8296ec13f 100644
--- a/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
@@ -5,18 +5,4 @@ Ext.define('PVE.sdn.Fabric.OpenFabric.Node.Edit', {
extraRequestParams: {
protocol: 'openfabric',
},
-
- additionalItems: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv6'),
- labelWidth: 120,
- name: 'ip6',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
});
diff --git a/www/manager6/sdn/fabrics/ospf/NodeEdit.js b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
index 2bec10468..370aee191 100644
--- a/www/manager6/sdn/fabrics/ospf/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
@@ -2,6 +2,8 @@ Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
extend: 'PVE.sdn.Fabric.Node.Edit',
protocol: 'ospf',
+ hasIpv6Support: false,
+
extraRequestParams: {
protocol: 'ospf',
},
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 23/29] ui: fabrics: interface: make ipv4/6 support generic over edit panels
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (21 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 22/29] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
` (8 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
From: Christoph Heiss <c.heiss@proxmox.com>
This allows selectively enabling IPv4/6 in the child components,
without having to re-define the respective fields in every child
component. A later commit will introduce the Wireguard fabric, which
will have neither a IPv4 nor IPv6 prefix for now, so the option to
exclude those fields will be used there as well.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com?
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
www/manager6/sdn/fabrics/InterfacePanel.js | 18 ++++++++++++++++++
.../sdn/fabrics/openfabric/InterfacePanel.js | 13 -------------
.../sdn/fabrics/ospf/InterfacePanel.js | 2 ++
3 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/www/manager6/sdn/fabrics/InterfacePanel.js b/www/manager6/sdn/fabrics/InterfacePanel.js
index f75f1acd2..ab2162b1e 100644
--- a/www/manager6/sdn/fabrics/InterfacePanel.js
+++ b/www/manager6/sdn/fabrics/InterfacePanel.js
@@ -6,6 +6,8 @@ Ext.define('PVE.sdn.Fabric.InterfacePanel', {
nodeInterfaces: {},
+ hasIpv6Support: true,
+
selModel: {
mode: 'SIMPLE',
type: 'checkboxmodel',
@@ -106,6 +108,22 @@ Ext.define('PVE.sdn.Fabric.InterfacePanel', {
initComponent: function () {
let me = this;
+ if (me.hasIpv6Support) {
+ me.commonColumns.push({
+ text: gettext('IPv6'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'ip6',
+ flex: 1,
+ widget: {
+ xtype: 'proxmoxtextfield',
+ isFormField: false,
+ bind: {
+ disabled: '{record.isDisabled}',
+ },
+ },
+ });
+ }
+
Ext.apply(me, {
store: Ext.create('Ext.data.Store', {
model: 'Pve.sdn.Interface',
diff --git a/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js b/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
index f23b889b4..19438bf5f 100644
--- a/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
+++ b/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
@@ -2,19 +2,6 @@ Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
extend: 'PVE.sdn.Fabric.InterfacePanel',
additionalColumns: [
- {
- text: gettext('IPv6'),
- xtype: 'widgetcolumn',
- dataIndex: 'ip6',
- flex: 1,
- widget: {
- xtype: 'proxmoxtextfield',
- isFormField: false,
- bind: {
- disabled: '{record.isDisabled}',
- },
- },
- },
{
text: gettext('Hello Multiplier'),
xtype: 'widgetcolumn',
diff --git a/www/manager6/sdn/fabrics/ospf/InterfacePanel.js b/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
index 29f0502fa..b521b1a22 100644
--- a/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
+++ b/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
@@ -1,3 +1,5 @@
Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+ hasIpv6Support: false,
});
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (22 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 23/29] ui: fabrics: interface: " Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:41 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 25/29] ui: fabrics: wireguard: add node " Stefan Hanreich
` (7 subsequent siblings)
31 siblings, 1 reply; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
The WireGuard interface panel allows for creating and editing
WireGuard interfaces on an internal node, as well as the peers
associated with that interface. The information for available peers is
taken directly via the list_nodes endpoint in the API. Existing peer
definitions for interfaces are matched manually to the respective
returned definitions from the nodes API, by matching on the IDs in the
peer definitions.
A few features that are available in the backend, e.g. overriding
endpoints on a per-interface basis, are not yet exposed in the UI
directly.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/wireguard/InterfacePanel.js | 518 ++++++++++++++++++
2 files changed, 519 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 597769bb9..76266ad28 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -343,6 +343,7 @@ JSSRC= \
sdn/fabrics/ospf/InterfacePanel.js \
sdn/fabrics/ospf/NodeEdit.js \
sdn/fabrics/ospf/FabricEdit.js \
+ sdn/fabrics/wireguard/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/wireguard/InterfacePanel.js b/www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
new file mode 100644
index 000000000..7a7cdd005
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
@@ -0,0 +1,518 @@
+Ext.define('Pve.sdn.Fabric.WireGuard.Interface', {
+ extend: 'Ext.data.Model',
+ idProperty: 'name',
+ fields: ['name', 'ip', 'ip6', 'listen_port', 'peers'],
+});
+
+Ext.define('Pve.sdn.Fabric.WireGuard.InterfacePeer', {
+ extend: 'Ext.data.Model',
+ fields: ['node', 'node_iface', 'type', 'endpoint', 'skip_route_generation'],
+});
+
+Ext.define('PVE.sdn.Fabric.WireGuard.PeerSelectionPanel', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveSDNWireguardPeerSelector',
+
+ emptyText: gettext('No peers available'),
+
+ selModel: {
+ type: 'checkboxmodel',
+ mode: 'SIMPLE',
+ },
+
+ store: {
+ model: 'Pve.sdn.Fabric.WireGuard.InterfacePeer',
+ },
+
+ config: {
+ currentNode: null,
+ availablePeers: [],
+ selectedPeers: [],
+ },
+
+ publishes: ['selectedPeers'],
+
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'node',
+ flex: 1,
+ },
+ {
+ header: gettext('Interface'),
+ dataIndex: 'node_iface',
+ flex: 1,
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ flex: 1,
+ },
+ {
+ header: gettext('Endpoint'),
+ dataIndex: 'endpoint',
+ flex: 1,
+ renderer: function(value, _metaData, record) {
+ let me = this;
+
+ if (value) {
+ return value;
+ }
+
+ let availablePeer = me.getAvailablePeers().find((availablePeer) => {
+ return availablePeer.node === record.data.node
+ && availablePeer.node_iface === record.data.node_iface;
+ });
+
+ return availablePeer?.endpoint;
+ },
+ },
+ {
+ header: gettext('Skip Route generation'),
+ flex: 1,
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'proxmoxcheckbox',
+ bind: {
+ value: '{record.skip_route_generation}',
+ },
+ },
+ },
+ ],
+
+ setSelectedPeers: function (selectedPeers) {
+ let me = this;
+
+ if (!me.isConfiguring) {
+ let store = me.getStore();
+
+ let selectionModel = me.getSelectionModel();
+ selectionModel.suspendEvents();
+
+ selectionModel.select([]);
+ store.removeAll();
+
+ for (const availablePeer of me.getAvailablePeers()) {
+ if (availablePeer.node === me.getCurrentNode().node_id) {
+ continue;
+ }
+
+ let selectedPeer = selectedPeers?.find((selectedPeer) => {
+ return selectedPeer.data.node === availablePeer.node
+ && selectedPeer.data.node_iface === availablePeer.node_iface;
+ });
+
+ let model = store.add(
+ selectedPeer ?? structuredClone(availablePeer)
+ );
+
+ if (selectedPeer) {
+ selectionModel.select(model, true);
+ }
+ }
+
+ selectionModel.resumeEvents();
+ me.publishState('selectedPeers', selectionModel.getSelection());
+ }
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ me.callParent();
+
+ me.getStore().on('datachanged', function() {
+ me.fireEvent('datachanged');
+ });
+
+ me.on('selectionchange', function (_selectionModel, selected) {
+ me.publishState('selectedPeers', selected);
+ });
+ },
+});
+
+Ext.define('PVE.sdn.Fabric.WireGuard.InterfacePanel', {
+ extend: 'Ext.panel.Panel',
+ mixins: ['Ext.form.field.Field'],
+
+ xtype: 'pveSDNFabricWireGuardInterfacePanel',
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ config: {
+ deleteEmpty: true,
+ },
+
+ viewModel: {
+ data: {
+ availablePeers: [],
+ currentNode: null,
+ },
+ stores: {
+ interfaces: {
+ model: 'Pve.sdn.Fabric.WireGuard.Interface',
+ },
+ },
+ formulas: {
+ selectedInterface: {
+ bind: '{interfaceGrid.selection}',
+ get: function (selection) {
+ if (Array.isArray(selection)) {
+ return selection[0];
+ }
+
+ return selection;
+ },
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ border: false,
+ width: 200,
+ margin: '0 10 0 0',
+ items: [
+ {
+ xtype: 'grid',
+ reference: 'interfaceGrid',
+ flex: 1,
+ margin: '0 0 10 0',
+ hideHeaders: true,
+ columns: [
+ {
+ text: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ },
+ {
+ xtype: 'actioncolumn',
+ width: 20,
+ items: [
+ {
+ iconCls: 'fa critical fa-trash-o',
+ tooltip: gettext('Remove'),
+ handler: function (
+ table,
+ _rowIndex,
+ _colIndex,
+ _item,
+ _e,
+ rec,
+ ) {
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.String.format(
+ gettext(
+ 'Are you sure you want to remove Interface {0}',
+ ),
+ `${rec.data.name}`,
+ ),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'no',
+ callback: function (btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ let grid = table.up(
+ 'grid[reference=interfaceGrid]',
+ );
+
+ let updateSelection = grid
+ .getSelection()
+ .includes(rec);
+
+ grid.getStore().remove(rec);
+
+ if (updateSelection) {
+ grid.setSelection(grid.getStore().first());
+ }
+ },
+ });
+ },
+ },
+ ],
+ },
+ ],
+ bind: {
+ store: '{interfaces}',
+ },
+ },
+ {
+ xtype: 'button',
+ text: gettext('Add Interface'),
+ handler: 'addInterface',
+ },
+ ],
+ },
+ {
+ xtype: 'form',
+ border: false,
+ flex: 1,
+ width: 300,
+ padding: 4,
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Name'),
+ isFormField: false,
+ bind: {
+ value: '{selectedInterface.name}',
+ disabled: '{!selectedInterface.isCreate}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Public Key'),
+ isFormField: false,
+ bind: {
+ value: '{selectedInterface.public_key}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Listen Port'),
+ bind: '{selectedInterface.listen_port}',
+ minValue: 1,
+ maxValue: 65535,
+ isFormField: false,
+ },
+ {
+ fieldLabel: gettext('IPv4 address'),
+ bind: '{selectedInterface.ip}',
+ xtype: 'proxmoxtextfield',
+ isFormField: false,
+ },
+ {
+ fieldLabel: gettext('IPv6 address'),
+ bind: '{selectedInterface.ip6}',
+ xtype: 'proxmoxtextfield',
+ isFormField: false,
+ },
+ {
+ xtype: 'pveSDNWireguardPeerSelector',
+ reference: 'peerSelector',
+ bind: {
+ currentNode: '{currentNode}',
+ availablePeers: '{availablePeers}',
+ selectedPeers: '{selectedInterface.peers}',
+ },
+ },
+ ],
+ bind: {
+ hidden: '{!selectedInterface}',
+ },
+ },
+ ],
+
+ previousDirty: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addInterface: function () {
+ let me = this;
+
+ let interfacesStore = me.getView().getViewModel().getStore('interfaces');
+
+ let idx = 0;
+ let name = `wg${idx}`;
+
+ while (interfacesStore.getById(name)) {
+ idx++;
+ name = `wg${idx}`;
+ }
+
+ let newInterface = interfacesStore.add({
+ name,
+ peers: [],
+ listen_port: 51820,
+ isCreate: true,
+ });
+
+ let interfaceGrid = me.lookupReference('interfaceGrid');
+ interfaceGrid.setSelection(newInterface);
+ },
+ },
+
+ selectFirstInterface: function () {
+ let me = this;
+
+ let firstInterface = me.getViewModel().getStore('interfaces').first();
+ if (firstInterface) {
+ me.lookupReference('interfaceGrid').setSelection([firstInterface]);
+ }
+ },
+
+ setAvailablePeers: function(availablePeers) {
+ let me = this;
+ me.getViewModel().set('availablePeers', availablePeers);
+ },
+
+ setNode: async function (node) {
+ let me = this;
+
+ let ifaces = {};
+
+ for (const iface of node.interfaces) {
+ let treeIface = {
+ id: iface.name,
+ peers: [],
+ isCreate: false,
+ ...PVE.Parser.parsePropertyString(iface),
+ };
+
+ ifaces[treeIface.name] = treeIface;
+ }
+
+ for (let peer of node.peers) {
+ peer = PVE.Parser.parsePropertyString(peer);
+ ifaces[peer.iface].peers.push(Ext.create('Pve.sdn.Fabric.WireGuard.InterfacePeer', peer));
+ }
+
+ me.getViewModel().set('currentNode', node);
+ me.getViewModel().getStore('interfaces').setData(Object.values(ifaces));
+
+ me.selectFirstInterface();
+ },
+
+ isDirty: function () {
+ let me = this;
+
+ let interfaceStore = me.getViewModel().getStore('interfaces');
+ let interfaces = interfaceStore.getData().items;
+
+ if (interfaces === undefined) {
+ return false;
+ }
+
+ return (
+ interfaceStore.getNewRecords().length > 0 ||
+ interfaceStore.getRemovedRecords().length > 0 ||
+ interfaces.some((iface) => iface.isDirty() || iface.data.peers.some((peer) => peer.isDirty()))
+ );
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ me.callParent();
+
+ let store = me.getViewModel().getStore('interfaces');
+
+ me.lookupReference('peerSelector').on('datachanged', function() {
+ let dirtyStatus = me.isDirty();
+
+ if (dirtyStatus !== me.previousDirty) {
+ me.previousDirty = dirtyStatus;
+ me.fireEvent('dirtychange');
+ }
+ });
+
+ store.on('update', function () {
+ let dirtyStatus = me.isDirty();
+
+ if (dirtyStatus !== me.previousDirty) {
+ me.previousDirty = dirtyStatus;
+ me.fireEvent('dirtychange');
+ }
+ });
+
+ store.on('add', function () {
+ me.previousDirty = true;
+ me.fireEvent('dirtychange');
+ });
+
+ store.on('remove', function () {
+ me.previousDirty = true;
+ me.fireEvent('dirtychange');
+ });
+ },
+
+ getSubmitData: function () {
+ let me = this;
+
+ if (me.isDisabled()) {
+ return null;
+ }
+
+ let peers = [];
+ let interfaces = [];
+
+ for (let record of me.getViewModel().getStore('interfaces').getData().items) {
+ let data = {};
+
+ for (const [key, value] of Object.entries(record.data)) {
+ if (value === '' || value === undefined || value === null) {
+ continue;
+ }
+
+ if (['peers', 'isCreate'].includes(key)) {
+ // peers are handled later separately, since they're two
+ // fields when talking to the API, but in the UI, they're a
+ // field in the interface model itself
+ //
+ // Other fields are ExtJS specific, so don't send them to
+ // the backend.
+ continue;
+ }
+
+ data[key] = value;
+ }
+
+ for (const peer of record.data.peers) {
+ let peerData = {
+ iface: record.data.name,
+ };
+
+ for (let [key, value] of Object.entries(peer.data)) {
+ if (value === '' || value === undefined || value === null) {
+ continue;
+ }
+
+ if (['id', 'allowed_ips', 'endpoint'].includes(key)) {
+ // filter ExtJS specific data, that has purely
+ // informational purposes when selecting peers
+ continue;
+ }
+
+ peerData[key] = value;
+ }
+
+ peers.push(PVE.Parser.printPropertyString(peerData));
+ }
+
+ interfaces.push(PVE.Parser.printPropertyString(data));
+ }
+
+ if (interfaces.length > 0) {
+ let retVal = {
+ interfaces,
+ };
+
+ if (peers.length > 0) {
+ retVal.peers = peers;
+ } else if (me.getDeleteEmpty()) {
+ retVal.delete = ['peers'];
+ }
+
+ return retVal;
+ } else if (me.getDeleteEmpty()) {
+ return {
+ delete: ['interfaces', 'peers'],
+ };
+ }
+
+ return null;
+ },
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 25/29] ui: fabrics: wireguard: add node edit panel
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (23 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 26/29] ui: fabrics: wireguard: add fabric " Stefan Hanreich
` (6 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Add a component for creating / updating nodes of a Wireguard fabric.
There are some key differences compared to OSPF / OpenFabric nodes,
mostly that WireGuard nodes can have different roles and that
WireGuard interfaces are not selected from the existing interfaces of
the node, but rather created. This requires some changes to the
existing NodeEdit panel provided by the fabrics:
* do not load available interfaces, but rather available peers
* let users create interfaces rather than select them
* add an additional roleSelector to the existing nodeSelector
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/wireguard/NodeEdit.js | 202 ++++++++++++++++++
2 files changed, 203 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/wireguard/NodeEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 76266ad28..f7c40ee04 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -344,6 +344,7 @@ JSSRC= \
sdn/fabrics/ospf/NodeEdit.js \
sdn/fabrics/ospf/FabricEdit.js \
sdn/fabrics/wireguard/InterfacePanel.js \
+ sdn/fabrics/wireguard/NodeEdit.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/wireguard/NodeEdit.js b/www/manager6/sdn/fabrics/wireguard/NodeEdit.js
new file mode 100644
index 000000000..c72eb5e04
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/NodeEdit.js
@@ -0,0 +1,202 @@
+Ext.define('PVE.sdn.Fabric.WireGuard.Node.Edit', {
+ extend: 'PVE.sdn.Fabric.Node.Edit',
+ protocol: 'wireguard',
+
+ extraRequestParams: {
+ protocol: 'wireguard',
+ },
+
+ referenceHolder: true,
+
+ // handled in the interface configuration (for now)
+ hasIpv4Support: false,
+ hasIpv6Support: false,
+
+ viewModel: {
+ data: {
+ current: {
+ isPveNode: true,
+ },
+ },
+ formulas: {
+ nameEditable: function(get) {
+ let me = this;
+ return me.getView().isCreate && get('current.isPveNode');
+ },
+ },
+ },
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Name'),
+ labelWidth: 120,
+ name: 'node_id',
+ bind: {
+ disabled: '{nameEditable}',
+ hidden: '{current.isPveNode}',
+ },
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Public Key'),
+ labelWidth: 120,
+ name: 'public_key',
+ bind: {
+ hidden: '{current.isPveNode}',
+ disabled: '{current.isPveNode}',
+ },
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Endpoint'),
+ emptyText: gettext('Listen Address for WireGuard'),
+ labelWidth: 120,
+ name: 'endpoint',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Allowed IPs'),
+ emptyText: gettext('Allowed Destination IPs for traffic coming from this node.'),
+ labelWidth: 120,
+ name: 'allowed_ips',
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ // TODO: implement proper list selection field that handles
+ // converting from / to array
+ setValue: function (value) {
+ if (Ext.isArray(value)) {
+ value = value.join(',');
+ }
+
+ this.setRawValue(value);
+ },
+ getSubmitValue: function () {
+ let value = this.getValue();
+ return value ? this.getValue().split(',') : null;
+ },
+ },
+ ],
+
+ loadAvailablePeers: async function () {
+ let me = this;
+
+ let response = await Proxmox.Async.api2({
+ url: `/cluster/sdn/fabrics/node/${me.fabricId}`,
+ method: 'GET',
+ });
+
+ return response.result.data.flatMap((node) => {
+ let availablePeers = [];
+
+ let peer = {
+ type: node.role,
+ endpoint: node.endpoint,
+ node: node.node_id,
+ };
+
+ if (node.role === 'internal') {
+ for (let iface of node.interfaces ?? []) {
+ let parsed_iface = PVE.Parser.parsePropertyString(iface);
+
+ let iface_peer = structuredClone(peer);
+ iface_peer.node_iface = parsed_iface.name;
+
+ availablePeers.push(iface_peer);
+ }
+ } else if (node.role === 'external') {
+ availablePeers.push(peer);
+ } else {
+ throw `unknown node type: ${node.role}`;
+ }
+
+ return availablePeers;
+ });
+ },
+
+ load: function () {
+ let me = this;
+
+ me.setLoading(gettext('fetching node information'));
+
+ Promise.all([me.loadNode(me.fabricId, me.nodeId), me.loadAvailablePeers()])
+ .catch(Proxmox.Utils.alertResponseFailure)
+ .then(([node, availablePeers]) => {
+ me.interfaceSelector.setAvailablePeers(availablePeers);
+
+ node.interfaces = node.interfaces ?? [];
+ node.peers = node.peers ?? [];
+ me.interfaceSelector.setNode(node);
+
+ me.setValues(node);
+ })
+ .finally(() => {
+ me.setLoading(false);
+ });
+ },
+
+ getNodeSelectorConfig: function () {
+ let me = this;
+ let config = me.callParent();
+
+ Ext.Object.merge(config, {
+ store: {
+ listeners: {
+ load: function (store) {
+ if (store.count() === 0) {
+ me.lookupReference('roleSelector').select('external');
+ me.lookupReference('nodeSelector').setDisabled(true);
+ }
+ },
+ },
+ },
+ });
+
+ return config;
+ },
+
+ getNodeSelector: function () {
+ let me = this;
+
+ let nodeSelector = me.callParent();
+ nodeSelector.setDisabled(!me.isCreate);
+
+ let roleSelector = Ext.create({
+ xtype: 'combobox',
+ name: 'role',
+ labelWidth: 120,
+ fieldLabel: gettext('Type'),
+ emptyText: gettext('Node'),
+ editable: false,
+ disabled: !me.isCreate,
+ reference: 'roleSelector',
+ value: 'internal',
+ store: [
+ ['internal', gettext('Node')],
+ ['external', gettext('External')],
+ ],
+ listeners: {
+ change: function (_this, newValue) {
+ let isPveNode = newValue === 'internal';
+
+ me.getViewModel().set('current.isPveNode', isPveNode);
+
+ me.interfaceSelector.setHidden(!isPveNode);
+ me.interfaceSelector.setDisabled(!isPveNode);
+
+ me.lookupReference('nodeSelector').setDisabled(!me.isCreate || !isPveNode);
+ me.lookupReference('nodeSelector').setHidden(!isPveNode);
+ },
+ },
+ });
+
+ return Ext.create({
+ xtype: 'inputpanel',
+ items: [roleSelector, nodeSelector],
+ });
+ },
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 26/29] ui: fabrics: wireguard: add fabric edit panel
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (24 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 25/29] ui: fabrics: wireguard: add node " Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 27/29] ui: fabrics: hook up wireguard components Stefan Hanreich
` (5 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
An edit window for updating / creating WireGuard fabrics and expose
the WireGuard specific configuration options (currently only
PersistentKeepAlive interval). Disable IPv4/6 prefix fields for now,
as they are currently not required and IPs are currently configured
per-interface.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/wireguard/FabricEdit.js | 29 +++++++++++++++++++
2 files changed, 30 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/wireguard/FabricEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index f7c40ee04..002ac9733 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -345,6 +345,7 @@ JSSRC= \
sdn/fabrics/ospf/FabricEdit.js \
sdn/fabrics/wireguard/InterfacePanel.js \
sdn/fabrics/wireguard/NodeEdit.js \
+ sdn/fabrics/wireguard/FabricEdit.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/wireguard/FabricEdit.js b/www/manager6/sdn/fabrics/wireguard/FabricEdit.js
new file mode 100644
index 000000000..0a46b2b88
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/FabricEdit.js
@@ -0,0 +1,29 @@
+Ext.define('PVE.sdn.Fabric.WireGuard.Fabric.Edit', {
+ extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+ subject: 'WireGuard',
+ onlineHelpX: 'pvesdn_wireguard_fabric',
+
+ extraRequestParams: {
+ protocol: 'wireguard',
+ },
+
+ // handled in the interface configuration (for now)
+ hasIpv4Support: false,
+ hasIpv6Support: false,
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Persistent Keepalive'),
+ name: 'persistent-keepalive',
+ minValue: 1,
+ maxValue: 65535,
+ labelWidth: 120,
+ allowBlank: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 27/29] ui: fabrics: hook up wireguard components
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (25 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 26/29] ui: fabrics: wireguard: add fabric " Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 28/29] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
` (4 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Add the newly created Wireguard-specific components to the
FabricsView, so they can be utilized to create / edit WireGuard
fabrics / nodes.
The reference to nodeselector has been renamed, but no further changes
are necessary since it was unused in the existing components. The
WireGuard components utilize the new name for setting bindings
specific to the node selector component.
Since WireGuard nodes can be external as well, do not show an error
message if there are sections for every Proxmox VE node in the
configuration, since it is still possible to create external nodes.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/FabricsView.js | 12 ++++++++++++
www/manager6/sdn/fabrics/NodeEdit.js | 8 ++++++--
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
index 21b88bab5..7da1990db 100644
--- a/www/manager6/sdn/FabricsView.js
+++ b/www/manager6/sdn/FabricsView.js
@@ -33,6 +33,7 @@ Ext.define('PVE.sdn.Fabric.View', {
const PROTOCOL_DISPLAY_NAMES = {
openfabric: 'OpenFabric',
ospf: 'OSPF',
+ wireguard: 'WireGuard',
};
const displayValue = PROTOCOL_DISPLAY_NAMES[value];
if (rec.data.state === undefined || rec.data.state === null) {
@@ -198,6 +199,10 @@ Ext.define('PVE.sdn.Fabric.View', {
text: 'OSPF',
handler: 'addOspf',
},
+ {
+ text: 'WireGuard',
+ handler: 'addWireGuard',
+ },
],
},
addNodeButton,
@@ -276,6 +281,7 @@ Ext.define('PVE.sdn.Fabric.View', {
const FABRIC_PANELS = {
openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
+ wireguard: 'PVE.sdn.Fabric.WireGuard.Fabric.Edit',
};
return FABRIC_PANELS[protocol];
@@ -285,11 +291,17 @@ Ext.define('PVE.sdn.Fabric.View', {
const NODE_PANELS = {
openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit',
+ wireguard: 'PVE.sdn.Fabric.WireGuard.Node.Edit',
};
return NODE_PANELS[protocol];
},
+ addWireGuard: function () {
+ let me = this;
+ me.openFabricAddWindow('wireguard');
+ },
+
addOpenfabric: function () {
let me = this;
me.openFabricAddWindow('openfabric');
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index 7cbea9608..31d328647 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -161,7 +161,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
return {
xtype: 'pveNodeSelector',
- reference: 'nodeselector',
+ reference: 'nodeSelector',
fieldLabel: gettext('Node'),
labelWidth: 120,
name: 'node_id',
@@ -225,6 +225,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
const INTERFACE_PANELS = {
openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
ospf: 'PVE.sdn.Fabric.Ospf.InterfacePanel',
+ wireguard: 'PVE.sdn.Fabric.WireGuard.InterfacePanel',
};
return INTERFACE_PANELS[protocol];
@@ -233,8 +234,11 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
getInterfaceSelector: function () {
let me = this;
- return Ext.create(me.getInterfacePanel(me.protocol), {
+ let componentName = me.getInterfacePanel(me.protocol);
+
+ return Ext.create(componentName, {
name: 'interfaces',
+ reference: 'interfaceSelector',
});
},
});
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-manager v5 28/29] fabrics: node edit: add option to include wireguard interfaces
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (26 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 27/29] ui: fabrics: hook up wireguard components Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard Stefan Hanreich
` (3 subsequent siblings)
31 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Because Wireguard interfaces are generated in a file separate from
/e/n/i, they do not show up automatically in the PVE interface
selector. Since they can be used without issue in some fabrics
(everything layer 3 and above), add the option to the node edit
component to include wireguard interfaces in the interface selection
of the node edit window as well.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/fabrics/NodeEdit.js | 44 ++++++++++++++++++++---
www/manager6/sdn/fabrics/ospf/NodeEdit.js | 1 +
2 files changed, 40 insertions(+), 5 deletions(-)
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index 31d328647..f2a32c7bc 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -30,6 +30,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
additionalItems: [],
addAnotherCallback: undefined,
+ includeWireguardInterfaces: false,
initComponent: function () {
let me = this;
@@ -127,17 +128,50 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
loadNodeInterfaces: async function () {
let me = this;
- let req = await Proxmox.Async.api2({
- url: `/api2/extjs/nodes/${me.nodeId}/network`,
- method: 'GET',
- });
+ let requests = [
+ Proxmox.Async.api2({
+ url: `/api2/extjs/nodes/${me.nodeId}/network`,
+ method: 'GET',
+ }),
+ ];
+
+ if (me.includeWireguardInterfaces) {
+ requests.push(
+ Proxmox.Async.api2({
+ url: `/api2/extjs/cluster/sdn/fabrics/node/`,
+ method: 'GET',
+ }),
+ );
+ }
- return req.result.data.map((iface) => ({
+ let result = await Promise.all(requests);
+
+ let interfaces = result[0].result.data.map((iface) => ({
name: iface.iface,
type: iface.type,
ip: iface.cidr,
ipv6: iface.cidr6,
}));
+
+ if (me.includeWireguardInterfaces) {
+ let wireguardNodes = result[1].result.data
+ .filter((node) => {
+ return node.node_id === me.nodeId && node.protocol === 'wireguard' && node.interfaces;
+ });
+
+ for (const node of wireguardNodes) {
+ for (const ifacePropertyString of node.interfaces) {
+ let iface = PVE.Parser.parsePropertyString(ifacePropertyString);
+
+ interfaces.push({
+ name: iface.name,
+ type: 'wireguard',
+ });
+ }
+ }
+ }
+
+ return interfaces;
},
load: function () {
diff --git a/www/manager6/sdn/fabrics/ospf/NodeEdit.js b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
index 370aee191..bf96ee95f 100644
--- a/www/manager6/sdn/fabrics/ospf/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
@@ -3,6 +3,7 @@ Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
protocol: 'ospf',
hasIpv6Support: false,
+ includeWireguardInterfaces: true,
extraRequestParams: {
protocol: 'ospf',
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (27 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-manager v5 28/29] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
@ 2026-05-12 17:31 ` Stefan Hanreich
2026-05-12 17:38 ` Stefan Hanreich
2026-05-13 2:51 ` partially-applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
` (2 subsequent siblings)
31 siblings, 1 reply; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:31 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pvesdn.adoc | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)
diff --git a/pvesdn.adoc b/pvesdn.adoc
index d20a0eb..1e83495 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -769,6 +769,106 @@ NOTE: The dummy interface will automatically be configured as `passive`. Every
interface which doesn't have an ip-address configured will be treated as a
`point-to-point` link.
+[[pvesdn_wireguard]]
+WireGuard
+~~~~~~~~~
+
+WireGuard can be used for establishing a VPN between Proxmox VE nodes and / or
+external nodes. It does not provide dynamic routing by itself, but can be used
+in conjunction with dynamic routing protocols operating on layer 3 and above
+(OSPF, BGP) to provide a dynamically routed, encrypted transport for e.g. EVPN
+or VXLAN.
+
+NOTE: In order to use WireGuard, the package `wireguard-tools` needs to be
+installed.
+
+Configuration options:
+
+[[pvesdn_wireguard_fabric]]
+On the Fabric
+^^^^^^^^^^^^^
+
+Name:: This is the name of the WireGuard fabric and can be at most 8 characters
+long.
+
+Persistent Keepalive:: If this is set, then WireGuard will send an empty
+authenticated packet every N seconds to each configured peer. This can help
+keeping connections alive when using stateful firewalls or NAT.
+
+[[pvesdn_wireguard_node]]
+On the Node
+^^^^^^^^^^^
+
+There are two types of nodes: internal and external. Internal nodes are Proxmox
+VE nodes, external nodes everything else. They are essentially reusable peer
+definitions that can be used across the whole cluster.
+
+.Internal
+
+Endpoint:: This is the IP or hostname that other Proxmox VE nodes should use for
+connecting to this Proxmox VE node. This is used as the endpoint when
+configuring this Proxmox VE node as a peer.
+
+Allowed IPs:: A comma-separated list of IP addresses. When selecting this node
+as a peer on other nodes, then this is used as the `AllowedIPs` setting in the
+WireGuard peer configuration. They specify the addresses that are allowed for
+incoming and outgoing traffic from/to this node.
+
+.External
+
+Name:: The name of the external node.
+
+Public Key:: The public key used by the external node.
+
+Endpoint:: The endpoint which is used for connecting to this external peer (e.g.
+192.0.2.1:51820).
+
+Allowed IPs:: A comma-separated list of IP addresses. When selecting this node
+as a peer on other nodes, then this is used as the `AllowedIPs` setting in the
+WireGuard peer configuration. They specify the addresses that are allowed for
+incoming and outgoing traffic from/to this node.
+
+[[pvesdn_wireguard_interface]]
+On The Interface
+^^^^^^^^^^^^^^^^
+
+Name:: The name of the network interface on the Linux host. At most 8
+alphanumerical characters + hyphens.
+
+IP::: The IPv4 address that should be configured on this interface.
+
+IPv6::: The IPv6 address that should be configured on this interface.
+
+Listen Port:: The listening port for this interface.
+
+Peers:: A list of peers that should be configured for that interface. All nodes
+that are part of the fabric can be selected as peers - the peer definition will
+be auto-generated from the configuration in the node.
+
+When defining an interface, then Proxmox VE automatically generates a public key
+for that interface in `/etc/pve/priv/wg-keys.conf` upon saving the interface.
+The public key can then be inspected in the Web UI when editing the node.
+Deleting an interface and re-applying the SDN configuration will delete the
+private key again.
+
+The fabric will also automatically generate routes for every allowed IP of every
+peer. E.g. if an interface wg0 has two peers with 198.51.100.0/24 and
+203.0.113.0/24 as allowed IPs, then routes for both subnets will be
+automatically created. If the peer is the interface of a Proxmox VE node, then
+the configured IP address will also be automatically added to the Allowed IPs in
+the peer configuration (e.g. if the other node has 192.0.2.10/24 as IP config,
+then 192.0.2.10/32 will be added to the allowed IPs).
+
+
+[[pvesdn_wireguard_interface]]
+On The Peer
+^^^^^^^^^^^
+
+Skip Route Generation:: The fabric will autogenerate routes in the kernel
+routing table for all allowed IPs of a peer. By setting this option, no routes
+will be inserted into the kernel routing table.
+
+
[[pvesdn_config_ipam]]
IPAM
----
--
2.47.3
^ permalink raw reply related [flat|nested] 35+ messages in thread
* Re: [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard
2026-05-12 17:31 ` [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard Stefan Hanreich
@ 2026-05-12 17:38 ` Stefan Hanreich
0 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:38 UTC (permalink / raw)
To: pve-devel
On 5/12/26 7:30 PM, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> pvesdn.adoc | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 100 insertions(+)
>
> diff --git a/pvesdn.adoc b/pvesdn.adoc
> index d20a0eb..1e83495 100644
> --- a/pvesdn.adoc
> +++ b/pvesdn.adoc
> @@ -769,6 +769,106 @@ NOTE: The dummy interface will automatically be configured as `passive`. Every
> interface which doesn't have an ip-address configured will be treated as a
> `point-to-point` link.
>
> +[[pvesdn_wireguard]]
> +WireGuard
> +~~~~~~~~~
> +
> +WireGuard can be used for establishing a VPN between Proxmox VE nodes and / or
> +external nodes. It does not provide dynamic routing by itself, but can be used
> +in conjunction with dynamic routing protocols operating on layer 3 and above
> +(OSPF, BGP) to provide a dynamically routed, encrypted transport for e.g. EVPN
> +or VXLAN.
> +
> +NOTE: In order to use WireGuard, the package `wireguard-tools` needs to be
> +installed.
> +
> +Configuration options:
> +
> +[[pvesdn_wireguard_fabric]]
> +On the Fabric
> +^^^^^^^^^^^^^
> +
> +Name:: This is the name of the WireGuard fabric and can be at most 8 characters
> +long.
> +
> +Persistent Keepalive:: If this is set, then WireGuard will send an empty
> +authenticated packet every N seconds to each configured peer. This can help
> +keeping connections alive when using stateful firewalls or NAT.
> +
> +[[pvesdn_wireguard_node]]
> +On the Node
> +^^^^^^^^^^^
> +
> +There are two types of nodes: internal and external. Internal nodes are Proxmox
> +VE nodes, external nodes everything else. They are essentially reusable peer
> +definitions that can be used across the whole cluster.
> +
> +.Internal
> +
> +Endpoint:: This is the IP or hostname that other Proxmox VE nodes should use for
> +connecting to this Proxmox VE node. This is used as the endpoint when
> +configuring this Proxmox VE node as a peer.
> +
> +Allowed IPs:: A comma-separated list of IP addresses. When selecting this node
> +as a peer on other nodes, then this is used as the `AllowedIPs` setting in the
> +WireGuard peer configuration. They specify the addresses that are allowed for
> +incoming and outgoing traffic from/to this node.
> +
> +.External
> +
> +Name:: The name of the external node.
> +
> +Public Key:: The public key used by the external node.
> +
> +Endpoint:: The endpoint which is used for connecting to this external peer (e.g.
> +192.0.2.1:51820).
> +
> +Allowed IPs:: A comma-separated list of IP addresses. When selecting this node
> +as a peer on other nodes, then this is used as the `AllowedIPs` setting in the
> +WireGuard peer configuration. They specify the addresses that are allowed for
> +incoming and outgoing traffic from/to this node.
> +
> +[[pvesdn_wireguard_interface]]
> +On The Interface
> +^^^^^^^^^^^^^^^^
> +
> +Name:: The name of the network interface on the Linux host. At most 8
> +alphanumerical characters + hyphens.
> +
> +IP::: The IPv4 address that should be configured on this interface.
> +
> +IPv6::: The IPv6 address that should be configured on this interface.
> +
> +Listen Port:: The listening port for this interface.
> +
> +Peers:: A list of peers that should be configured for that interface. All nodes
> +that are part of the fabric can be selected as peers - the peer definition will
> +be auto-generated from the configuration in the node.
> +
> +When defining an interface, then Proxmox VE automatically generates a public key
> +for that interface in `/etc/pve/priv/wg-keys.conf` upon saving the interface.
> +The public key can then be inspected in the Web UI when editing the node.
> +Deleting an interface and re-applying the SDN configuration will delete the
> +private key again.
> +
> +The fabric will also automatically generate routes for every allowed IP of every
> +peer. E.g. if an interface wg0 has two peers with 198.51.100.0/24 and
> +203.0.113.0/24 as allowed IPs, then routes for both subnets will be
> +automatically created. If the peer is the interface of a Proxmox VE node, then
> +the configured IP address will also be automatically added to the Allowed IPs in
> +the peer configuration (e.g. if the other node has 192.0.2.10/24 as IP config,
> +then 192.0.2.10/32 will be added to the allowed IPs).
> +
> +
> +[[pvesdn_wireguard_interface]]
> +On The Peer
> +^^^^^^^^^^^
forgot to amend the fix for this heading into the docs commit - this
should be `pvesdn_wireguard_peer` instead.
^ permalink raw reply [flat|nested] 35+ messages in thread
* Re: [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel
2026-05-12 17:31 ` [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
@ 2026-05-12 17:41 ` Stefan Hanreich
0 siblings, 0 replies; 35+ messages in thread
From: Stefan Hanreich @ 2026-05-12 17:41 UTC (permalink / raw)
To: pve-devel, Dominik Csapak
On 5/12/26 7:30 PM, Stefan Hanreich wrote:
> The WireGuard interface panel allows for creating and editing
> WireGuard interfaces on an internal node, as well as the peers
> associated with that interface. The information for available peers is
> taken directly via the list_nodes endpoint in the API. Existing peer
> definitions for interfaces are matched manually to the respective
> returned definitions from the nodes API, by matching on the IDs in the
> peer definitions.
>
> A few features that are available in the backend, e.g. overriding
> endpoints on a per-interface basis, are not yet exposed in the UI
> directly.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
change detection is a bit wonky here, but no blocker imo
could you maybe take a look @Dominik?
^ permalink raw reply [flat|nested] 35+ messages in thread
* partially-applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (28 preceding siblings ...)
2026-05-12 17:31 ` [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard Stefan Hanreich
@ 2026-05-13 2:51 ` Thomas Lamprecht
2026-05-15 5:02 ` applied: " Thomas Lamprecht
2026-05-15 5:04 ` Thomas Lamprecht
31 siblings, 0 replies; 35+ messages in thread
From: Thomas Lamprecht @ 2026-05-13 2:51 UTC (permalink / raw)
To: pve-devel, Stefan Hanreich
On Tue, 12 May 2026 19:31:15 +0200, Stefan Hanreich wrote:
> ## Introduction
>
> This patch series introduces WireGuard as fabric protocol. Potential use-cases
> include:
>
> * Connecting to remote PBS / PDM instances
> * Simple encryption layer for intra-DC VXLAN tunnels
> * Secure migration network
> * Connecting with remote PVE clusters
>
> [...]
Applied proxmox-ve-rs and proxmox-perl-rs with some fix-ups and follow-ups,
thanks! Only bumped and uploaded the sdn-types and ve-config crates for now
though, need a bit more time with the rest and might be good if Dominik gets
around to check out your UI side too before we bump pve-rs (plus there I want
to add some pending HA patches too)
[proxmox-ve-rs]:
[1/8] sdn-types: add wireguard-specific PersistentKeepalive api type
commit: 0b85c44720126e0001ea90b27c3b8b0c6d4159fa
[2/8] ve-config: fabrics: split interface name regex into two parts
commit: 6d5b67ea9e6c2523597bb74690ef9f25e4e853bc
[3/8] ve-config: fabric: refactor fabric config entry impl using macro
commit: dba13f2dcbb10ec740ccaa3a35b42d744a52f4ef
[4/8] ve-config: fabrics: add protocol-specific properties for wireguard
commit: 216df238290fcbb415f147572f1e97fd7cd98a2c
[5/8] ve-config: wireguard: add private keys section config
commit: 68a3d249dd68f9b5e9b1e714540b7696c4784ff7
[6/8] ve-config: sdn: fabrics: add wireguard to the fabric config
commit: 9c3361826e6f3cdb0de4a816d3864106b5beb3ae
[7/8] ve-config: fabrics: wireguard add validation for wireguard config
commit: bc58956cc557ad9ab577a2bbabaf2caa8fa1b15f
[8/8] ve-config: fabrics: implement wireguard config generation
commit: 913dfbc1bf5a6bfcce982ab63a66c76f30f15bd9
[proxmox-perl-rs]:
[1/3] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
commit: f37170e5e70f6a3f74129623a54e6e28b1d9b8e5
[2/3] pve-rs: fabrics: add helpers for parsing interface property strings
commit: a579dc69cfdcf6b9df6dea74c97e22a4ee43224e
[3/3] pve-rs: sdn: wireguard: add private keys module
commit: 843b148e451b69ebf3dca8a2bea4b4be3d534c56
^ permalink raw reply [flat|nested] 35+ messages in thread
* applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (29 preceding siblings ...)
2026-05-13 2:51 ` partially-applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
@ 2026-05-15 5:02 ` Thomas Lamprecht
2026-05-15 5:04 ` Thomas Lamprecht
31 siblings, 0 replies; 35+ messages in thread
From: Thomas Lamprecht @ 2026-05-15 5:02 UTC (permalink / raw)
To: pve-devel, Stefan Hanreich
On Tue, 12 May 2026 19:31:15 +0200, Stefan Hanreich wrote:
> ## Introduction
>
> This patch series introduces WireGuard as fabric protocol. Potential use-cases
> include:
>
> * Connecting to remote PBS / PDM instances
> * Simple encryption layer for intra-DC VXLAN tunnels
> * Secure migration network
> * Connecting with remote PVE clusters
>
> [...]
Applied, thanks!
[1/3] sdn: add wireguard helper module
commit: 7e79e81386cb6a31927635b1698b69691180f118
[2/3] fabrics: wireguard: add schema definitions for wireguard
commit: db09e50375a75e175408b13d8a6d841d46c067b9
[3/3] fabrics: wireguard: implement wireguard key auto-generation
commit: 8d6ac7d1f97c196ed181cccd3d4a7b685f1138aa
^ permalink raw reply [flat|nested] 35+ messages in thread
* applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (30 preceding siblings ...)
2026-05-15 5:02 ` applied: " Thomas Lamprecht
@ 2026-05-15 5:04 ` Thomas Lamprecht
31 siblings, 0 replies; 35+ messages in thread
From: Thomas Lamprecht @ 2026-05-15 5:04 UTC (permalink / raw)
To: pve-devel, Stefan Hanreich
On Tue, 12 May 2026 19:31:15 +0200, Stefan Hanreich wrote:
> ## Introduction
>
> This patch series introduces WireGuard as fabric protocol. Potential use-cases
> include:
>
> * Connecting to remote PBS / PDM instances
> * Simple encryption layer for intra-DC VXLAN tunnels
> * Secure migration network
> * Connecting with remote PVE clusters
>
> [...]
Applied, thanks!
[01/13] network: sdn: generate wireguard configuration on apply
commit: cdc488adf70495627b9fc1788abcc58ffcc73e60
[02/13] ui: fix parsing of property-strings when values contain =
commit: 0ccf362ebbf80f28ad922d73e241f8cedfe7c9e4
[03/13] ui: fabrics: i18n: make node loading string translatable
commit: bd84f2b85325852a5f29cc56da53b246791409b3
[04/13] sdn: fabrics view: handle case where interfaces are deleted
commit: 22eae6d22f6e8de2d4b07faf90f9c49546fed5c3
[05/13] ui: fabrics: split node selector creation and config
commit: ad6550787586bc309e869e378d6cd74ed682ef05
[06/13] ui: fabrics: edit: make ipv4/6 support generic over fabric panels
commit: e5e3374273af56dad849ab970d057cc7f4338dfd
[07/13] ui: fabrics: node: make ipv4/6 support generic over edit panels
commit: 2e09809b6762be236667cba8da6177a2b2757d56
[08/13] ui: fabrics: interface: make ipv4/6 support generic over edit panels
commit: f577357ee26abc7116009fa3c1b1d24c3ea20dea
[09/13] ui: fabrics: wireguard: add interface edit panel
commit: 800f2215c1508a384a95684a042de7cb9524647f
[10/13] ui: fabrics: wireguard: add node edit panel
commit: d115dfb45f6b92d04c5cce14ac232ca7492140ba
[11/13] ui: fabrics: wireguard: add fabric edit panel
commit: cfc0b8ded748d13c54c6394ed4fdd1a2ce7d1266
[12/13] ui: fabrics: hook up wireguard components
commit: 9fa808781515591b28c70cd6a78fcd0b8464e7e9
[13/13] fabrics: node edit: add option to include wireguard interfaces
commit: c2277dfa16f3dce9b56df779a90097c2bce64a93
^ permalink raw reply [flat|nested] 35+ messages in thread
end of thread, other threads:[~2026-05-15 5:05 UTC | newest]
Thread overview: 35+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-12 17:31 [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-cluster v5 01/29] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 02/29] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 03/29] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 04/29] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 05/29] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 06/29] ve-config: wireguard: add private keys section config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 07/29] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 08/29] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-ve-rs v5 09/29] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 10/29] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 11/29] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
2026-05-12 17:31 ` [PATCH proxmox-perl-rs v5 12/29] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 13/29] sdn: add wireguard helper module Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 14/29] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-network v5 15/29] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 16/29] network: sdn: generate wireguard configuration on apply Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 17/29] ui: fix parsing of property-strings when values contain = Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 18/29] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 19/29] sdn: fabrics view: handle case where interfaces are deleted Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 20/29] ui: fabrics: split node selector creation and config Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 21/29] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 22/29] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 23/29] ui: fabrics: interface: " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 24/29] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
2026-05-12 17:41 ` Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 25/29] ui: fabrics: wireguard: add node " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 26/29] ui: fabrics: wireguard: add fabric " Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 27/29] ui: fabrics: hook up wireguard components Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-manager v5 28/29] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
2026-05-12 17:31 ` [PATCH pve-docs v5 29/29] sdn: fabrics: add section about wireguard Stefan Hanreich
2026-05-12 17:38 ` Stefan Hanreich
2026-05-13 2:51 ` partially-applied: [PATCH cluster/docs/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/29] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
2026-05-15 5:02 ` applied: " Thomas Lamprecht
2026-05-15 5:04 ` Thomas Lamprecht
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox