public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics
@ 2026-05-07 12:39 Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-cluster v4 01/31] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
                   ` (31 more replies)
  0 siblings, 32 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 UTC (permalink / raw)
  To: pve-devel

This patch series is based on top of the route-maps series [1]. While it does
not rely on any of the features / code included there, there are some merge
conflicts when applying either series first, so I'm sending it based on top of
the route-maps series so they can be applied conveniently after another.

## 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 [2] 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 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

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://lore.proxmox.com/pve-devel/20260504160350.395470-2-s.hanreich@proxmox.com/T/#t
[2] 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:

Stefan Hanreich (4):
  wireguard: utilize x25519 for public key generation
  wireguard: skip serializing preshared_key if unset
  wireguard: implement ApiType for private key
  network-types: implement ApiType for endpoints and hostnames

 proxmox-network-types/src/endpoint.rs | 30 ++++++++++-
 proxmox-wireguard/Cargo.toml          |  1 +
 proxmox-wireguard/src/lib.rs          | 72 +++++++++++++--------------
 3 files changed, 64 insertions(+), 39 deletions(-)


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       | 405 +++++++--
 .../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      | 788 ++++++++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |   1 +
 proxmox-ve-config/src/sdn/wireguard.rs        | 309 +++++++
 14 files changed, 1607 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             |  16 ++
 6 files changed, 308 insertions(+), 31 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    | 105 ++++++++++-
 src/PVE/Network/SDN.pm                        |   2 +
 src/PVE/Network/SDN/Fabrics.pm                | 173 +++++++++++++++++-
 src/PVE/Network/SDN/Makefile                  |   3 +-
 src/PVE/Network/SDN/WireGuard.pm              | 162 ++++++++++++++++
 6 files changed, 435 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 (10):
  network: sdn: generate wireguard configuration on apply
  ui: fix parsing of property-strings when values contain =
  ui: fabrics: i18n: make node loading string translatable
  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               |  12 +
 www/manager6/sdn/fabrics/FabricEdit.js        |  68 ++-
 www/manager6/sdn/fabrics/InterfacePanel.js    |  18 +
 www/manager6/sdn/fabrics/NodeEdit.js          | 108 ++++-
 .../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   | 435 ++++++++++++++++++
 .../sdn/fabrics/wireguard/NodeEdit.js         | 229 +++++++++
 16 files changed, 882 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


Summary over all repositories:
  47 files changed, 3298 insertions(+), 249 deletions(-)

-- 
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 34+ messages in thread

* [PATCH pve-cluster v4 01/31] cfs: add 'priv/wg-keys.cfg' to observed files
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation Stefan Hanreich
                   ` (30 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-cluster v4 01/31] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:40   ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox v4 03/31] wireguard: skip serializing preshared_key if unset Stefan Hanreich
                   ` (29 subsequent siblings)
  31 siblings, 1 reply; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 UTC (permalink / raw)
  To: pve-devel

Previously, proxmox-wireguard used ed25519 for generating the public
keys, which is the wrong algorithm for deriving suitable public keys
for WireGuard - since ed25519 is a digital signature algorithm. x25519
is for conducting DH key exchanges, which is what is utilized in the
WireGuard protocol.

The generated public keys from the tests have been checked against the
output from wg pubkey - to make sure that generated keys are exactly
the same as the ones generated by the userspace wg(8) tool.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-wireguard/Cargo.toml |  1 +
 proxmox-wireguard/src/lib.rs | 56 +++++++++++++-----------------------
 2 files changed, 21 insertions(+), 36 deletions(-)

diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
index b1abae3d..ae3236a8 100644
--- a/proxmox-wireguard/Cargo.toml
+++ b/proxmox-wireguard/Cargo.toml
@@ -11,6 +11,7 @@ rust-version.workspace = true
 
 [dependencies]
 ed25519-dalek = "2.1"
+x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
 serde = { workspace = true, features = [ "derive" ] }
 thiserror.workspace = true
 proxmox-schema = { workspace = true, optional = true, features = ["api-types"] }
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 08579775..bf6ea8ad 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -12,9 +12,11 @@
 
 #![forbid(unsafe_code, missing_docs)]
 
+use std::fmt;
+
 use ed25519_dalek::SigningKey;
 use serde::{Deserialize, Serialize};
-use std::fmt;
+use x25519_dalek::StaticSecret;
 
 use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr};
 #[cfg(feature = "api-types")]
@@ -42,9 +44,7 @@ impl From<proxmox_ini::Error> for Error {
 /// Public key of a WireGuard peer.
 #[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)]
 #[serde(transparent)]
-pub struct PublicKey(
-    #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
-);
+pub struct PublicKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
 
 #[cfg(feature = "api-types")]
 impl ApiType for PublicKey {
@@ -62,9 +62,7 @@ impl UpdaterType for PublicKey {
 /// Private key of a WireGuard peer.
 #[derive(Serialize)]
 #[serde(transparent)]
-pub struct PrivateKey(
-    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
-);
+pub struct PrivateKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
 
 impl fmt::Debug for PrivateKey {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -73,42 +71,27 @@ impl fmt::Debug for PrivateKey {
 }
 
 impl PrivateKey {
-    /// Length of the raw private key data in bytes.
-    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
-
     /// Generates a new private key suitable for use with WireGuard.
     #[cfg(feature = "key-generation")]
     pub fn generate() -> Result<Self, Error> {
-        generate_key().map(Self)
+        Ok(Self(StaticSecret::random().to_bytes()))
     }
 
     /// Calculates the public key from the private key.
     pub fn public_key(&self) -> PublicKey {
-        PublicKey(
-            ed25519_dalek::SigningKey::from_bytes(&self.0)
-                .verifying_key()
-                .to_bytes(),
-        )
-    }
-
-    /// Builds a new [`PrivateKey`] from raw key material.
-    #[must_use]
-    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
-        // [`SigningKey`] takes care of correct key clamping.
-        Self(SigningKey::from(&data).to_bytes())
+        PublicKey(x25519_dalek::PublicKey::from(&StaticSecret::from(self.0)).to_bytes())
     }
 }
 
-impl From<ed25519_dalek::SecretKey> for PrivateKey {
-    fn from(value: ed25519_dalek::SecretKey) -> Self {
+impl From<[u8; 32]> for PrivateKey {
+    fn from(value: [u8; 32]) -> Self {
         Self(value)
     }
 }
 
-impl AsRef<ed25519_dalek::SecretKey> for PrivateKey {
-    /// Returns the raw private key material.
-    fn as_ref(&self) -> &ed25519_dalek::SecretKey {
-        &self.0
+impl From<x25519_dalek::StaticSecret> for PrivateKey {
+    fn from(value: x25519_dalek::StaticSecret) -> Self {
+        Self(value.to_bytes())
     }
 }
 
@@ -239,7 +222,8 @@ mod tests {
 
     fn mock_private_key(v: u8) -> PrivateKey {
         let base = v * 32;
-        PrivateKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
+        let key: [u8; 32] = (base..base + 32).collect::<Vec<u8>>().try_into().unwrap();
+        PrivateKey(key.into())
     }
 
     fn mock_preshared_key(v: u8) -> PresharedKey {
@@ -272,7 +256,7 @@ ListenPort = 51820
 FwMark = 127
 
 [Peer]
-PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
 PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
 AllowedIPs = 192.168.0.0/24
 Endpoint = foo.example.com:51820
@@ -328,24 +312,24 @@ PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
 ListenPort = 51820
 
 [Peer]
-PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
 PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
 AllowedIPs = 192.168.0.0/24
 Endpoint = foo.example.com:51820
 
 [Peer]
-PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0=
+PublicKey = eaYx7t4b+cmPEgMs3q3Q56B5OY/HhriMyEbsia+FpRo=
 PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=
 AllowedIPs = 192.168.1.0/24
 PersistentKeepalive = 25
 
 [Peer]
-PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U=
+PublicKey = Z13VdO13iTELPS52gfN5C0ZsdzsVIf7PNld5WDcepS8=
 PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8=
 AllowedIPs = 192.168.2.0/24
 
 [Peer]
-PublicKey = zRSzf5VulTGU/3+3Oz2B3MVh1hp1OAlLfD4aZD7l86o=
+PublicKey = ST6C/HRGSlkmiBdiPSBTxeuOLMSpiLT+4XnsawENUx0=
 PresharedKey = gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8=
 Endpoint = 10.0.0.1:51820
 PersistentKeepalive = 25
@@ -376,7 +360,7 @@ PersistentKeepalive = 25
 PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
 
 [Peer]
-PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
 PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
 AllowedIPs = 192.168.0.0/24
 Endpoint = 10.0.0.1:51820
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH proxmox v4 03/31] wireguard: skip serializing preshared_key if unset
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-cluster v4 01/31] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox v4 04/31] wireguard: implement ApiType for private key Stefan Hanreich
                   ` (28 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 UTC (permalink / raw)
  To: pve-devel

Otherwise the generated WireGuard configuration is invalid, because it
contains a line 'PresharedKey=', which trips up wg(8). Avoid this by
skipping serialization for unset preshared keys.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-wireguard/src/lib.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index bf6ea8ad..3a6248b2 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -142,6 +142,7 @@ pub struct WireGuardPeer {
     /// Additional key preshared between two peers. Adds an additional layer of symmetric-key
     /// cryptography to be mixed into the already existing public-key cryptography, for
     /// post-quantum resistance.
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub preshared_key: Option<PresharedKey>,
     /// List of IPv4/v6 CIDRs from which incoming traffic for this peer is allowed and to which
     /// outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may be specified for
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH proxmox v4 04/31] wireguard: implement ApiType for private key
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (2 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox v4 03/31] wireguard: skip serializing preshared_key if unset Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox v4 05/31] network-types: implement ApiType for endpoints and hostnames Stefan Hanreich
                   ` (27 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 UTC (permalink / raw)
  To: pve-devel

This is required for storing private keys in a section config file
under /etc/pve/priv/wg-keys.cfg.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-wireguard/src/lib.rs | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 3a6248b2..9ca50e1d 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -60,10 +60,23 @@ impl UpdaterType for PublicKey {
 }
 
 /// Private key of a WireGuard peer.
-#[derive(Serialize)]
+#[derive(Clone, Copy, Deserialize, Serialize, Hash)]
 #[serde(transparent)]
 pub struct PrivateKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
 
+#[cfg(feature = "api-types")]
+impl ApiType for PrivateKey {
+    const API_SCHEMA: proxmox_schema::Schema =
+        StringSchema::new("ED25519 private key (base64 encoded)")
+            .format(&ApiStringFormat::Pattern(&ED25519_BASE64_KEY_REGEX))
+            .schema();
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for PrivateKey {
+    type Updater = Option<PrivateKey>;
+}
+
 impl fmt::Debug for PrivateKey {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "<private-key>")
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH proxmox v4 05/31] network-types: implement ApiType for endpoints and hostnames
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (3 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox v4 04/31] wireguard: implement ApiType for private key Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 06/31] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
                   ` (26 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 UTC (permalink / raw)
  To: pve-devel

This allows those types to be used in the section config types for the
wireguard fabric, as well as in the API definitions for them.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-network-types/src/endpoint.rs | 30 +++++++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)

diff --git a/proxmox-network-types/src/endpoint.rs b/proxmox-network-types/src/endpoint.rs
index 10f6a813..609de58a 100644
--- a/proxmox-network-types/src/endpoint.rs
+++ b/proxmox-network-types/src/endpoint.rs
@@ -7,13 +7,19 @@ use std::{
     str::FromStr,
 };
 
+#[cfg(feature = "api-types")]
+use proxmox_schema::StringSchema;
+#[cfg(feature = "api-types")]
+use proxmox_schema::{ApiType, UpdaterType};
+use serde::{Deserialize, Serialize};
 use serde_with::{DeserializeFromStr, SerializeDisplay};
 
 /// Represents either a resolvable hostname or an IPv4/IPv6 address.
 /// IPv6 address are correctly bracketed on [`Display`], and parsing
 /// automatically tries parsing it as an IP address first, falling back to a
 /// plain hostname in the other case.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Hash, Deserialize, Serialize)]
+#[serde(untagged)]
 pub enum HostnameOrIpAddr {
     Hostname(String),
     IpAddr(IpAddr),
@@ -42,9 +48,19 @@ impl<S: Into<String>> From<S> for HostnameOrIpAddr {
     }
 }
 
+#[cfg(feature = "api-types")]
+impl ApiType for HostnameOrIpAddr {
+    const API_SCHEMA: proxmox_schema::Schema = StringSchema::new("hostname or ip address").schema();
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for HostnameOrIpAddr {
+    type Updater = Option<Self>;
+}
+
 /// Represents a (host, port) tuple, where the host can either be a resolvable
 /// hostname or an IPv4/IPv6 address.
-#[derive(Clone, Debug, PartialEq, SerializeDisplay, DeserializeFromStr)]
+#[derive(Clone, Debug, PartialEq, Hash, SerializeDisplay, DeserializeFromStr)]
 pub struct ServiceEndpoint {
     host: HostnameOrIpAddr,
     port: u16,
@@ -99,6 +115,16 @@ impl FromStr for ServiceEndpoint {
     }
 }
 
+#[cfg(feature = "api-types")]
+impl UpdaterType for ServiceEndpoint {
+    type Updater = Option<Self>;
+}
+
+#[cfg(feature = "api-types")]
+impl ApiType for ServiceEndpoint {
+    const API_SCHEMA: proxmox_schema::Schema = StringSchema::new("service endpoint").schema();
+}
+
 #[cfg(test)]
 mod tests {
     use crate::endpoint::HostnameOrIpAddr;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 06/31] sdn-types: add wireguard-specific PersistentKeepalive api type
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (4 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox v4 05/31] network-types: implement ApiType for endpoints and hostnames Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 07/31] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
                   ` (25 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 07/31] ve-config: fabrics: split interface name regex into two parts
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (5 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 06/31] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 08/31] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
                   ` (24 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 08/31] ve-config: fabric: refactor fabric config entry impl using macro
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (6 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 07/31] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 09/31] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
                   ` (23 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 09/31] ve-config: fabrics: add protocol-specific properties for wireguard
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (7 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 08/31] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 10/31] ve-config: wireguard: add private keys section config Stefan Hanreich
                   ` (22 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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      | 501 ++++++++++++++++++
 4 files changed, 507 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 ba97a9e..2f8cded 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 0a1c5e1..d86c79e 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..8ad24c4
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -0,0 +1,501 @@
+//! 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")]
+    #[updater(serde(skip_serializing_if = "Option::is_none"))]
+    pub(crate) auto_generate_routes: 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")]
+    #[updater(serde(skip_serializing_if = "Option::is_none"))]
+    pub(crate) auto_generate_routes: 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 auto_generate_routes(&self) -> bool {
+        match self {
+            WireGuardNodePeer::Internal(internal_peer) => &internal_peer.auto_generate_routes,
+            WireGuardNodePeer::External(external_peer) => &external_peer.auto_generate_routes,
+        }
+        .unwrap_or(true)
+    }
+}
+
+#[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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 10/31] ve-config: wireguard: add private keys section config
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (8 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 09/31] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 11/31] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
                   ` (21 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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      | 224 ++++++++++++++++++
 1 file changed, 224 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 8ad24c4..e885ef8 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
@@ -499,3 +499,227 @@ 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> {
+            self.0.get_mut(node)?.remove(interface)
+        }
+
+        /// 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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 11/31] ve-config: sdn: fabrics: add wireguard to the fabric config
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (9 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 10/31] ve-config: wireguard: add private keys section config Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 12/31] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
                   ` (20 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 17d2f0b..437428b 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},
@@ -147,8 +148,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()
     }
@@ -156,8 +157,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()
     }
@@ -186,6 +187,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 {
@@ -194,6 +197,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(),
         }
     }
 
@@ -202,6 +206,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(),
         }
     }
 
@@ -210,6 +215,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(),
         }
     }
 }
@@ -221,6 +227,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(()),
         }
     }
 }
@@ -237,6 +244,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,
@@ -263,6 +276,7 @@ pub mod api {
             OpenfabricNodePropertiesUpdater,
         },
         ospf::{OspfNodeDeletableProperties, OspfNodeProperties, OspfNodePropertiesUpdater},
+        wireguard::{WireGuardNodeDeletableProperties, WireGuardNodeUpdater},
     };
 
     use super::*;
@@ -320,6 +334,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 {
@@ -327,6 +343,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()),
             }
         }
     }
@@ -336,6 +353,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()),
             }
         }
     }
@@ -349,6 +367,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")]
@@ -384,6 +406,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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 12/31] ve-config: fabrics: wireguard add validation for wireguard config
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (10 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 11/31] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 13/31] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
                   ` (19 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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       | 167 ++++++++++++++++--
 .../src/sdn/fabric/section_config/node.rs     |   2 +-
 .../section_config/protocol/wireguard.rs      |  65 ++++++-
 4 files changed, 218 insertions(+), 17 deletions(-)

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 2f8cded..8485a0f 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..2c2ad52 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,12 @@ 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("WireGuard interface listen port duplicated in node configuration: {0}")]
+    DuplicatePort(String),
 }
 
 /// An entry in a [`FabricConfig`].
@@ -500,7 +506,52 @@ 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 internal_peers = HashSet::new();
+
+            for node_id in entry.nodes.keys() {
+                let node_section = entry.node_section(node_id)?;
+
+                if let WireGuardNode::Internal(node) = node_section.properties() {
+                    all_interfaces.extend(
+                        node.interfaces()
+                            .map(|interface| (&node_section.id.node_id, &interface.name)),
+                    );
+
+                    internal_peers.extend(node.peers().filter_map(|peer| {
+                        if let WireGuardNodePeer::Internal(peer) = peer {
+                            return Some((&peer.node, &peer.node_iface));
+                        }
+
+                        None
+                    }));
+                }
+            }
+
+            for (node_id, interface) in internal_peers.difference(&all_interfaces) {
+                return Err(FabricConfigError::InvalidRemoteInterfaceReference(
+                    interface.to_string(),
+                    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 +605,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 +639,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 +674,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 +703,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 +1016,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 437428b..69c222d 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -227,7 +227,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 e885ef8..78bbcb4 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,45 @@ impl InternalWireGuardNode {
     }
 }
 
+impl Validatable for InternalWireGuardNode {
+    type Error = FabricConfigError;
+
+    /// Validates the [FabricSection<WireGuardNodeProperties>].
+    ///
+    /// Checks if we have either an IPv4 or an IPv6 address. If neither is set, return an error.
+    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] 34+ messages in thread

* [PATCH proxmox-ve-rs v4 13/31] ve-config: fabrics: implement wireguard config generation
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (11 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 12/31] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 14/31] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
                   ` (18 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 8485a0f..8e5e333 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 d86c79e..eae7fcd 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(&current_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(&current_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] 34+ messages in thread

* [PATCH proxmox-perl-rs v4 14/31] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (12 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 13/31] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 15/31] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
                   ` (17 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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           |  16 +++
 2 files changed, 164 insertions(+), 31 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 18848c4..45653c1 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.auto_generate_routes()
+                                        || 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..132a0f4 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
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH proxmox-perl-rs v4 15/31] pve-rs: fabrics: add helpers for parsing interface property strings
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (13 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 14/31] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 16/31] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
                   ` (16 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 45653c1..1ebfde1 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] 34+ messages in thread

* [PATCH proxmox-perl-rs v4 16/31] pve-rs: sdn: wireguard: add private keys module
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (14 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 15/31] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-network v4 17/31] sdn: add wireguard helper module Stefan Hanreich
                   ` (15 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 c92d822..3065f9b 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.6", 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] 34+ messages in thread

* [PATCH pve-network v4 17/31] sdn: add wireguard helper module
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (15 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 16/31] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-network v4 18/31] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
                   ` (14 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 | 162 +++++++++++++++++++++++++++++++
 2 files changed, 164 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 2a476ce..d0b4bce 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 0000000..bb69475
--- /dev/null
+++ b/src/PVE/Network/SDN/WireGuard.pm
@@ -0,0 +1,162 @@
+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 {
+        for my $interface (keys $raw_config->%*) {
+            PVE::File::file_set_contents(
+                "$wireguard_config_folder/$interface.conf",
+                $raw_config->{$interface},
+                400,
+            );
+
+            if ($apply) {
+                if (!-e "/usr/bin/wg") {
+                    warn
+                        "In order to apply the generated WireGuard configuration the package 'wireguard-tools' needs to be installed.\n";
+                    return;
+                }
+
+                eval {
+                    PVE::Tools::run_command(
+                        [
+                            'wg',
+                            'syncconf',
+                            $interface,
+                            "/etc/wireguard/proxmox/$interface.conf",
+                        ],
+                    );
+                };
+                warn $@ if $@;
+            }
+        }
+    };
+
+    PVE::Tools::lock_file($local_wireguard_lock, 10, $code);
+    die $@ if $@;
+
+    return;
+}
+
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH pve-network v4 18/31] fabrics: wireguard: add schema definitions for wireguard
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (16 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-network v4 17/31] sdn: add wireguard helper module Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-network v4 19/31] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
                   ` (13 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 | 173 +++++++++++++++++++++++++++++++--
 3 files changed, 170 insertions(+), 7 deletions(-)

diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 03746e8..6b73af2 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 53a12c2..74c0edb 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 bc295f0..98e9c90 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,118 @@ sub node_properties {
                     description => 'OSPF network interface',
                     optional => 1,
                 },
+                {
+                    type => 'array',
+                    'instance-types' => ['wireguard'],
+                    items => {
+                        description =>
+                            "Type of configuration entry in an SDN Fabric section config",
+                        type => 'string',
+                        format => 'pve-sdn-fabric-wireguard-interface',
+                    },
+                    description => 'WireGuard network interface',
+                    optional => 1,
+                },
             ],
         },
-    };
-
-    if ($update) {
-        $properties->{delete} = {
+        public_key => {
+            'type-property' => 'protocol',
+            'instance-types' => ['wireguard'],
+            description => 'The role of this node in the WireGuard fabric.',
+            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',
+                    },
+                },
+            },
+        },
+    };
+
+    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 +412,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 +453,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] 34+ messages in thread

* [PATCH pve-network v4 19/31] fabrics: wireguard: implement wireguard key auto-generation
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (17 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-network v4 18/31] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-manager v4 20/31] network: sdn: generate wireguard configuration on apply Stefan Hanreich
                   ` (12 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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    | 105 +++++++++++++++++-
 2 files changed, 101 insertions(+), 6 deletions(-)

diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 6b73af2..e3c8d9d 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 000e4c3..2df52c1 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,58 @@ __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});
+                        }
+
+                        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 +342,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] 34+ messages in thread

* [PATCH pve-manager v4 20/31] network: sdn: generate wireguard configuration on apply
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (18 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-network v4 19/31] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-manager v4 21/31] ui: fix parsing of property-strings when values contain = Stefan Hanreich
                   ` (11 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH pve-manager v4 21/31] ui: fix parsing of property-strings when values contain =
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (19 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-manager v4 20/31] network: sdn: generate wireguard configuration on apply Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-manager v4 22/31] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
                   ` (10 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH pve-manager v4 22/31] ui: fabrics: i18n: make node loading string translatable
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (20 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-manager v4 21/31] ui: fix parsing of property-strings when values contain = Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-manager v4 23/31] ui: fabrics: split node selector creation and config Stefan Hanreich
                   ` (9 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH pve-manager v4 23/31] ui: fabrics: split node selector creation and config
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (21 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-manager v4 22/31] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:39 ` [PATCH pve-manager v4 24/31] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
                   ` (8 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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] 34+ messages in thread

* [PATCH pve-manager v4 24/31] ui: fabrics: edit: make ipv4/6 support generic over fabric panels
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (22 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-manager v4 23/31] ui: fabrics: split node selector creation and config Stefan Hanreich
@ 2026-05-07 12:39 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 25/31] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
                   ` (7 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:39 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 77851a7df..7e71bc527 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 d0de87cff..3ab9bad82 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] 34+ messages in thread

* [PATCH pve-manager v4 25/31] ui: fabrics: node: make ipv4/6 support generic over edit panels
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (23 preceding siblings ...)
  2026-05-07 12:39 ` [PATCH pve-manager v4 24/31] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 26/31] ui: fabrics: interface: " Stefan Hanreich
                   ` (6 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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] 34+ messages in thread

* [PATCH pve-manager v4 26/31] ui: fabrics: interface: make ipv4/6 support generic over edit panels
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (24 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 25/31] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 27/31] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
                   ` (5 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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] 34+ messages in thread

* [PATCH pve-manager v4 27/31] ui: fabrics: wireguard: add interface edit panel
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (25 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 26/31] ui: fabrics: interface: " Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 28/31] ui: fabrics: wireguard: add node " Stefan Hanreich
                   ` (4 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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   | 435 ++++++++++++++++++
 2 files changed, 436 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..f60f17216
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
@@ -0,0 +1,435 @@
+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.Peer', {
+    extend: 'Ext.data.Model',
+    fields: ['node', 'node_iface', 'type', 'endpoint'],
+});
+
+Ext.define('PVE.sdn.Fabric.WireGuard.PeerSelectionPanel', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pveSDNWireguardPeerSelector',
+
+    emptyText: gettext('No peers available'),
+
+    selModel: {
+        type: 'checkboxmodel',
+        mode: 'SIMPLE',
+    },
+
+    config: {
+        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,
+        },
+    ],
+
+    setSelectedPeers: function (selectedPeers) {
+        let me = this;
+
+        if (!me.isConfiguring) {
+            if (!selectedPeers || selectedPeers.length === 0) {
+                me.setSelection();
+            } else {
+                me.setSelection(selectedPeers);
+            }
+
+            me.publishState('selectedPeers', selectedPeers);
+        }
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        me.callParent();
+
+        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,
+    },
+
+    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: {
+                        store: '{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: 50000,
+                isCreate: true,
+            });
+
+            let interfaceGrid = me.lookupReference('interfaceGrid');
+            interfaceGrid.setSelection(newInterface);
+        },
+    },
+
+    setAvailablePeers: function (availablePeers) {
+        let me = this;
+        me.getViewModel().getStore('availablePeers').setData(availablePeers);
+    },
+
+    selectFirstInterface: function () {
+        let me = this;
+
+        let firstInterface = me.getViewModel().getStore('interfaces').first();
+        if (firstInterface) {
+            me.lookupReference('interfaceGrid').setSelection([firstInterface]);
+        }
+    },
+
+    setNode: async function (node) {
+        let me = this;
+
+        node = structuredClone(node);
+
+        let ifaces = {};
+
+        for (const iface of node.interfaces) {
+            let treeIface = {
+                id: iface.name,
+                peers: [],
+                isCreate: false,
+                ...PVE.Parser.parsePropertyString(iface),
+            };
+
+            ifaces[treeIface.name] = treeIface;
+        }
+
+        let availablePeers = me.getViewModel().getStore('availablePeers');
+
+        for (let peer of node.peers) {
+            peer = PVE.Parser.parsePropertyString(peer);
+
+            let peerId = peer.type === 'external' ? peer.node : `${peer.node}_${peer.node_iface}`;
+            let peerModel = availablePeers.getById(peerId);
+
+            ifaces[peer.iface].peers.push(peerModel);
+        }
+
+        availablePeers.setFilters([(peer) => peer.data.node !== node.node_id]);
+
+        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())
+        );
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        me.callParent();
+
+        let store = me.getViewModel().getStore('interfaces');
+
+        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 (const [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] 34+ messages in thread

* [PATCH pve-manager v4 28/31] ui: fabrics: wireguard: add node edit panel
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (26 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 27/31] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 29/31] ui: fabrics: wireguard: add fabric " Stefan Hanreich
                   ` (3 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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         | 229 ++++++++++++++++++
 2 files changed, 230 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..27674d0b3
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/NodeEdit.js
@@ -0,0 +1,229 @@
+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,
+            },
+        },
+        stores: {
+            availablePeers: {
+                model: 'Pve.sdn.Fabric.WireGuard.Peer',
+            },
+            interfaces: {
+                model: 'Pve.sdn.Fabric.WireGuard.Interface',
+            },
+        },
+        formulas: {
+            selectedInterface: {
+                bind: '{interfaceGrid.selection}',
+                get: function (selection) {
+                    if (Array.isArray(selection)) {
+                        return selection[0];
+                    }
+
+                    return selection;
+                },
+            },
+        },
+    },
+
+    additionalItems: [
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('Name'),
+            labelWidth: 120,
+            name: 'node_id',
+            bind: {
+                hidden: '{current.isPveNode}',
+                disabled: '{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,
+                allowed_ips: node.allowed_ips,
+                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;
+                    iface_peer.id = `${iface_peer.node}_${iface_peer.node_iface}`;
+
+                    availablePeers.push(iface_peer);
+                }
+            } else if (node.role === 'external') {
+                peer.id = peer.node;
+                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) {
+                    me.getViewModel().set('current.isPveNode', newValue === 'internal');
+                    me.lookupReference('nodeSelector').setDisabled(!me.isCreate || newValue !== 'internal');
+                },
+            },
+        });
+
+        return Ext.create({
+            xtype: 'inputpanel',
+            items: [roleSelector, nodeSelector],
+        });
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        me.callParent();
+
+        me.interfaceSelector.setBind({
+            hidden: '{!current.isPveNode}',
+            disabled: '{!current.isPveNode}',
+        });
+
+        // needs to lookup the specific reference, because me.nodeSelector is
+        // the whole input panel that includes roleSelector as well.
+        me.lookupReference('nodeSelector').setBind({
+            hidden: '{!current.isPveNode}',
+        });
+    },
+});
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH pve-manager v4 29/31] ui: fabrics: wireguard: add fabric edit panel
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (27 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 28/31] ui: fabrics: wireguard: add node " Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 30/31] ui: fabrics: hook up wireguard components Stefan Hanreich
                   ` (2 subsequent siblings)
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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] 34+ messages in thread

* [PATCH pve-manager v4 30/31] ui: fabrics: hook up wireguard components
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (28 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 29/31] ui: fabrics: wireguard: add fabric " Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 12:40 ` [PATCH pve-manager v4 31/31] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
  2026-05-07 14:08 ` partially-applied: [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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 |  9 +++++++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
index 093a70f35..0362df019 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) {
@@ -194,6 +195,10 @@ Ext.define('PVE.sdn.Fabric.View', {
                             text: 'OSPF',
                             handler: 'addOspf',
                         },
+                        {
+                            text: 'WireGuard',
+                            handler: 'addWireGuard',
+                        },
                     ],
                 },
                 addNodeButton,
@@ -272,6 +277,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];
@@ -281,11 +287,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..55dce3043 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,12 @@ 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',
+            viewModel: me.getViewModel(),
         });
     },
 });
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH pve-manager v4 31/31] fabrics: node edit: add option to include wireguard interfaces
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (29 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 30/31] ui: fabrics: hook up wireguard components Stefan Hanreich
@ 2026-05-07 12:40 ` Stefan Hanreich
  2026-05-07 14:08 ` partially-applied: [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht
  31 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 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 55dce3043..4b3967a17 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' ;
+                });
+
+            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] 34+ messages in thread

* Re: [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation
  2026-05-07 12:39 ` [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation Stefan Hanreich
@ 2026-05-07 12:40   ` Stefan Hanreich
  0 siblings, 0 replies; 34+ messages in thread
From: Stefan Hanreich @ 2026-05-07 12:40 UTC (permalink / raw)
  To: pve-devel, Christian Ebner

@Christoph could you please double-check this in particular?


On 5/7/26 2:38 PM, Stefan Hanreich wrote:
> Previously, proxmox-wireguard used ed25519 for generating the public
> keys, which is the wrong algorithm for deriving suitable public keys
> for WireGuard - since ed25519 is a digital signature algorithm. x25519
> is for conducting DH key exchanges, which is what is utilized in the
> WireGuard protocol.
> 
> The generated public keys from the tests have been checked against the
> output from wg pubkey - to make sure that generated keys are exactly
> the same as the ones generated by the userspace wg(8) tool.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  proxmox-wireguard/Cargo.toml |  1 +
>  proxmox-wireguard/src/lib.rs | 56 +++++++++++++-----------------------
>  2 files changed, 21 insertions(+), 36 deletions(-)
> 
> diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
> index b1abae3d..ae3236a8 100644
> --- a/proxmox-wireguard/Cargo.toml
> +++ b/proxmox-wireguard/Cargo.toml
> @@ -11,6 +11,7 @@ rust-version.workspace = true
>  
>  [dependencies]
>  ed25519-dalek = "2.1"
> +x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
>  serde = { workspace = true, features = [ "derive" ] }
>  thiserror.workspace = true
>  proxmox-schema = { workspace = true, optional = true, features = ["api-types"] }
> diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
> index 08579775..bf6ea8ad 100644
> --- a/proxmox-wireguard/src/lib.rs
> +++ b/proxmox-wireguard/src/lib.rs
> @@ -12,9 +12,11 @@
>  
>  #![forbid(unsafe_code, missing_docs)]
>  
> +use std::fmt;
> +
>  use ed25519_dalek::SigningKey;
>  use serde::{Deserialize, Serialize};
> -use std::fmt;
> +use x25519_dalek::StaticSecret;
>  
>  use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr};
>  #[cfg(feature = "api-types")]
> @@ -42,9 +44,7 @@ impl From<proxmox_ini::Error> for Error {
>  /// Public key of a WireGuard peer.
>  #[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)]
>  #[serde(transparent)]
> -pub struct PublicKey(
> -    #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
> -);
> +pub struct PublicKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
>  
>  #[cfg(feature = "api-types")]
>  impl ApiType for PublicKey {
> @@ -62,9 +62,7 @@ impl UpdaterType for PublicKey {
>  /// Private key of a WireGuard peer.
>  #[derive(Serialize)]
>  #[serde(transparent)]
> -pub struct PrivateKey(
> -    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
> -);
> +pub struct PrivateKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
>  
>  impl fmt::Debug for PrivateKey {
>      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
> @@ -73,42 +71,27 @@ impl fmt::Debug for PrivateKey {
>  }
>  
>  impl PrivateKey {
> -    /// Length of the raw private key data in bytes.
> -    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
> -
>      /// Generates a new private key suitable for use with WireGuard.
>      #[cfg(feature = "key-generation")]
>      pub fn generate() -> Result<Self, Error> {
> -        generate_key().map(Self)
> +        Ok(Self(StaticSecret::random().to_bytes()))
>      }
>  
>      /// Calculates the public key from the private key.
>      pub fn public_key(&self) -> PublicKey {
> -        PublicKey(
> -            ed25519_dalek::SigningKey::from_bytes(&self.0)
> -                .verifying_key()
> -                .to_bytes(),
> -        )
> -    }
> -
> -    /// Builds a new [`PrivateKey`] from raw key material.
> -    #[must_use]
> -    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
> -        // [`SigningKey`] takes care of correct key clamping.
> -        Self(SigningKey::from(&data).to_bytes())
> +        PublicKey(x25519_dalek::PublicKey::from(&StaticSecret::from(self.0)).to_bytes())
>      }
>  }
>  
> -impl From<ed25519_dalek::SecretKey> for PrivateKey {
> -    fn from(value: ed25519_dalek::SecretKey) -> Self {
> +impl From<[u8; 32]> for PrivateKey {
> +    fn from(value: [u8; 32]) -> Self {
>          Self(value)
>      }
>  }
>  
> -impl AsRef<ed25519_dalek::SecretKey> for PrivateKey {
> -    /// Returns the raw private key material.
> -    fn as_ref(&self) -> &ed25519_dalek::SecretKey {
> -        &self.0
> +impl From<x25519_dalek::StaticSecret> for PrivateKey {
> +    fn from(value: x25519_dalek::StaticSecret) -> Self {
> +        Self(value.to_bytes())
>      }
>  }
>  
> @@ -239,7 +222,8 @@ mod tests {
>  
>      fn mock_private_key(v: u8) -> PrivateKey {
>          let base = v * 32;
> -        PrivateKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
> +        let key: [u8; 32] = (base..base + 32).collect::<Vec<u8>>().try_into().unwrap();
> +        PrivateKey(key.into())
>      }
>  
>      fn mock_preshared_key(v: u8) -> PresharedKey {
> @@ -272,7 +256,7 @@ ListenPort = 51820
>  FwMark = 127
>  
>  [Peer]
> -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
> +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
>  PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
>  AllowedIPs = 192.168.0.0/24
>  Endpoint = foo.example.com:51820
> @@ -328,24 +312,24 @@ PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
>  ListenPort = 51820
>  
>  [Peer]
> -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
> +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
>  PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
>  AllowedIPs = 192.168.0.0/24
>  Endpoint = foo.example.com:51820
>  
>  [Peer]
> -PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0=
> +PublicKey = eaYx7t4b+cmPEgMs3q3Q56B5OY/HhriMyEbsia+FpRo=
>  PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=
>  AllowedIPs = 192.168.1.0/24
>  PersistentKeepalive = 25
>  
>  [Peer]
> -PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U=
> +PublicKey = Z13VdO13iTELPS52gfN5C0ZsdzsVIf7PNld5WDcepS8=
>  PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8=
>  AllowedIPs = 192.168.2.0/24
>  
>  [Peer]
> -PublicKey = zRSzf5VulTGU/3+3Oz2B3MVh1hp1OAlLfD4aZD7l86o=
> +PublicKey = ST6C/HRGSlkmiBdiPSBTxeuOLMSpiLT+4XnsawENUx0=
>  PresharedKey = gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8=
>  Endpoint = 10.0.0.1:51820
>  PersistentKeepalive = 25
> @@ -376,7 +360,7 @@ PersistentKeepalive = 25
>  PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
>  
>  [Peer]
> -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
> +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=
>  PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
>  AllowedIPs = 192.168.0.0/24
>  Endpoint = 10.0.0.1:51820





^ permalink raw reply	[flat|nested] 34+ messages in thread

* partially-applied: [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics
  2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
                   ` (30 preceding siblings ...)
  2026-05-07 12:40 ` [PATCH pve-manager v4 31/31] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
@ 2026-05-07 14:08 ` Thomas Lamprecht
  31 siblings, 0 replies; 34+ messages in thread
From: Thomas Lamprecht @ 2026-05-07 14:08 UTC (permalink / raw)
  To: pve-devel, Stefan Hanreich

On Thu, 07 May 2026 14:39:35 +0200, Stefan Hanreich wrote:
> This patch series is based on top of the route-maps series [1]. While it does
> not rely on any of the features / code included there, there are some merge
> conflicts when applying either series first, so I'm sending it based on top of
> the route-maps series so they can be applied conveniently after another.
> 
> ## Introduction
> 
> [...]

Applied and uploaded the proxmox part, thanks!

[1/4] wireguard: utilize x25519 for public key generation
      commit: e7764edd14d7a848ad9df61a990718f83784e259
[2/4] wireguard: skip serializing preshared_key if unset
      commit: ae2fad7b89c9b42ded1d861a63b8d748be999a78
[3/4] wireguard: implement ApiType for private key
      commit: d69c5d6bdb3cebc0404fe10beb69a247a5939e37
[4/4] network-types: implement ApiType for endpoints and hostnames
      commit: 9349687e713b23229404f070b2f95733ae8bc0b8




^ permalink raw reply	[flat|nested] 34+ messages in thread

end of thread, other threads:[~2026-05-07 14:09 UTC | newest]

Thread overview: 34+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-07 12:39 [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-cluster v4 01/31] cfs: add 'priv/wg-keys.cfg' to observed files Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation Stefan Hanreich
2026-05-07 12:40   ` Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox v4 03/31] wireguard: skip serializing preshared_key if unset Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox v4 04/31] wireguard: implement ApiType for private key Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox v4 05/31] network-types: implement ApiType for endpoints and hostnames Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 06/31] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 07/31] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 08/31] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 09/31] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 10/31] ve-config: wireguard: add private keys section config Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 11/31] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 12/31] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-ve-rs v4 13/31] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 14/31] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 15/31] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
2026-05-07 12:39 ` [PATCH proxmox-perl-rs v4 16/31] pve-rs: sdn: wireguard: add private keys module Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-network v4 17/31] sdn: add wireguard helper module Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-network v4 18/31] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-network v4 19/31] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-manager v4 20/31] network: sdn: generate wireguard configuration on apply Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-manager v4 21/31] ui: fix parsing of property-strings when values contain = Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-manager v4 22/31] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-manager v4 23/31] ui: fabrics: split node selector creation and config Stefan Hanreich
2026-05-07 12:39 ` [PATCH pve-manager v4 24/31] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 25/31] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 26/31] ui: fabrics: interface: " Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 27/31] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 28/31] ui: fabrics: wireguard: add node " Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 29/31] ui: fabrics: wireguard: add fabric " Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 30/31] ui: fabrics: hook up wireguard components Stefan Hanreich
2026-05-07 12:40 ` [PATCH pve-manager v4 31/31] fabrics: node edit: add option to include wireguard interfaces Stefan Hanreich
2026-05-07 14:08 ` partially-applied: [PATCH cluster/manager/network/proxmox{,-ve-rs,-perl-rs} v4 00/31] Add WireGuard as protocol to SDN fabrics Thomas Lamprecht

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal