* [PATCH proxmox 1/2] wireguard: skip serializing preshared_key if unset
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 2/2] wireguard: implement ApiType for endpoints and hostnames Stefan Hanreich
` (25 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 593713d8..41a2922d 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -155,6 +155,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 [flat|nested] 28+ messages in thread* [PATCH proxmox 2/2] wireguard: implement ApiType for endpoints and hostnames
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 1/2] wireguard: skip serializing preshared_key if unset Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 1/9] debian: update control file Stefan Hanreich
` (24 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 24e33c7f..a6a209e2 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, Updater, 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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 1/9] debian: update control file
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 1/2] wireguard: skip serializing preshared_key if unset Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox 2/2] wireguard: implement ApiType for endpoints and hostnames Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 2/9] clippy: fix 'hiding a lifetime that's elided elsewhere is confusing' Stefan Hanreich
` (23 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-sdn-types/debian/control | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/proxmox-sdn-types/debian/control b/proxmox-sdn-types/debian/control
index a0b7af9..7c2aa3a 100644
--- a/proxmox-sdn-types/debian/control
+++ b/proxmox-sdn-types/debian/control
@@ -16,12 +16,11 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git
Homepage: https://proxmox.com
X-Cargo-Crate: proxmox-sdn-types
-Rules-Requires-Root: no
Package: librust-proxmox-sdn-types-dev
Architecture: any
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 2/9] clippy: fix 'hiding a lifetime that's elided elsewhere is confusing'
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (2 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 1/9] debian: update control file Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 3/9] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
` (22 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/firewall/types/ipset.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index 3f93bb2..f09b8d8 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -313,7 +313,7 @@ impl Ipset {
Ok(())
}
- pub fn ipfilter(&self) -> Option<Ipfilter> {
+ pub fn ipfilter(&self) -> Option<Ipfilter<'_>> {
if self.name.scope() != IpsetScope::Guest {
return None;
}
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 3/9] sdn-types: add wireguard-specific PersistentKeepalive api type
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (3 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 2/9] clippy: fix 'hiding a lifetime that's elided elsewhere is confusing' Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 4/9] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
` (21 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 1656f1d..3328b45 100644
--- a/proxmox-sdn-types/src/lib.rs
+++ b/proxmox-sdn-types/src/lib.rs
@@ -1,3 +1,4 @@
pub mod area;
pub mod net;
pub mod openfabric;
+pub mod wireguard;
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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 4/9] ve-config: fabrics: split interface name regex into two parts
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (4 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 3/9] sdn-types: add wireguard-specific PersistentKeepalive api type Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 5/9] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
` (20 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 5/9] ve-config: fabric: refactor fabric config entry impl using macro
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (5 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 4/9] ve-config: fabrics: split interface name regex into two parts Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 6/9] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
` (19 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 677a309..d0add92 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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 6/9] ve-config: fabrics: add protocol-specific properties for wireguard
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (6 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 5/9] ve-config: fabric: refactor fabric config entry impl using macro Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 7/9] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
` (18 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 478 ++++++++++++++++++
4 files changed, 484 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 3a4dd61..fdcb331 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 b211827..28fae4c 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..3765b89
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -0,0 +1,478 @@
+//! 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>,
+}
+
+#[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>,
+}
+
+#[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,
+ }
+ }
+}
+
+#[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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 7/9] ve-config: sdn: fabrics: add wireguard to the fabric config
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (7 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 6/9] ve-config: fabrics: add protocol-specific properties for wireguard Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 8/9] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
` (17 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 44 ++++-
5 files changed, 292 insertions(+), 4 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 10025b3..fc41410 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -232,6 +232,7 @@ pub fn build_fabric(
frr_config.protocol_routemaps.insert(protocol_routemap);
}
+ FabricEntry::WireGuard(_) => {} // not a frr fabric
}
}
Ok(())
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index d0add92..53ce87f 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(());
+ }
}
}
@@ -695,6 +819,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..77ce15f 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},
@@ -36,6 +37,18 @@ api_string_type! {
pub struct NodeId(String);
}
+impl std::str::FromStr for NodeId {
+ type Err = anyhow::Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::API_SCHEMA
+ .unwrap_string_schema()
+ .check_constraints(value)?;
+
+ Ok(unsafe { Self::from_string_unchecked(value.to_string()) })
+ }
+}
+
/// ID of a node in the section config.
///
/// This corresponds to the ID of the fabric, that contains this node, as well as the hostname of
@@ -147,8 +160,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 +169,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 +199,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 +209,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 +218,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 +227,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 +239,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 +256,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 +288,7 @@ pub mod api {
OpenfabricNodePropertiesUpdater,
},
ospf::{OspfNodeDeletableProperties, OspfNodeProperties, OspfNodePropertiesUpdater},
+ wireguard::{WireGuardNodeDeletableProperties, WireGuardNodeUpdater},
};
use super::*;
@@ -320,6 +346,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 +355,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 +365,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 +379,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 +418,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 [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 8/9] ve-config: fabrics: wireguard add validation for wireguard config
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (8 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 7/9] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
` (16 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/src/sdn/fabric/mod.rs | 161 ++++++++++++++++--
.../src/sdn/fabric/section_config/node.rs | 2 +-
.../section_config/protocol/wireguard.rs | 63 ++++++-
4 files changed, 210 insertions(+), 17 deletions(-)
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index fdcb331..bb1a057 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 53ce87f..7d1a2b9 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,10 @@ 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 interface")]
+ InvalidInterfaceReference,
+ #[error("WireGuard interface listen port duplicated in node configuration: {0}")]
+ DuplicatePort(String),
}
/// An entry in a [`FabricConfig`].
@@ -500,7 +504,49 @@ 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
+ }));
+ }
+ }
+
+ if !(all_interfaces.is_superset(&internal_peers)) {
+ return Err(FabricConfigError::InvalidInterfaceReference);
+ }
+ }
+
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 +600,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()
@@ -634,6 +668,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 +697,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| {
+ node_interfaces.insert((node_id, interface.name.as_str()))
+ }) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+ }
}
}
}
@@ -952,3 +993,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 77ce15f..c7b26f4 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -239,7 +239,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 3765b89..3acd856 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -28,6 +28,7 @@
//! definition can be overridden in the peer definition, if e.g. a different endpoint is required
//! for connecting to a node.
+use std::collections::HashSet;
use std::ops::{Deref, DerefMut};
use anyhow::Result;
@@ -44,7 +45,10 @@ use proxmox_sdn_types::wireguard::PersistentKeepalive;
use proxmox_wireguard::PublicKey;
use serde::{Deserialize, Serialize};
-use crate::sdn::fabric::section_config::node::NodeId;
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::fabric::FabricSection;
+use crate::sdn::fabric::section_config::node::{NodeId, NodeSection};
+use crate::sdn::fabric::FabricConfigError;
pub const WIREGUARD_INTERFACE_NAME_REGEX_STR: &str = "[a-zA-Z0-9][a-zA-Z0-9-]{0,6}[a-zA-Z0-9]?";
@@ -79,6 +83,14 @@ pub struct WireGuardProperties {
pub(crate) persistent_keepalive: Option<PersistentKeepalive>,
}
+impl Validatable for FabricSection<WireGuardProperties> {
+ type Error = FabricConfigError;
+
+ fn validate(&self) -> Result<(), Self::Error> {
+ Ok(())
+ }
+}
+
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WireGuardDeletableProperties {
@@ -159,6 +171,18 @@ impl ApiType for WireGuardNode {
.schema();
}
+impl Validatable for NodeSection<WireGuardNode> {
+ type Error = FabricConfigError;
+
+ fn validate(&self) -> Result<(), Self::Error> {
+ if let WireGuardNode::Internal(node) = self.properties() {
+ return node.validate();
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
#[serde(rename_all = "snake_case", tag = "role")]
pub enum WireGuardNodeUpdater {
@@ -291,6 +315,43 @@ impl InternalWireGuardNode {
}
}
+impl Validatable for InternalWireGuardNode {
+ type Error = FabricConfigError;
+
+ /// Validates the [FabricSection<WireGuardNodeProperties>].
+ ///
+ /// 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::InvalidInterfaceReference);
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
#[api(
properties: {
allowed_ips: {
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: implement wireguard config generation
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (9 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 8/9] ve-config: fabrics: wireguard add validation for wireguard config Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-perl-rs 1/2] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
` (15 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 +
.../section_config/protocol/wireguard.rs | 7 +
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/wireguard.rs | 311 ++++++++++++++++++
5 files changed, 322 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 bb1a057..22a76a7 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 28fae4c..5cfc068 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-0.1+api-types-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.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-0.1+api-types-dev (>= 0.1.1-~~),
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~),
librust-proxmox-schema-5+api-types-dev,
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 3acd856..ec3315e 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
@@ -440,6 +440,13 @@ impl WireGuardNodePeer {
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,
+ }
+ }
}
#[derive(Clone, Debug, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 4586c56..9811f7f 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -3,6 +3,7 @@ pub mod fabric;
#[cfg(feature = "frr")]
pub mod frr;
pub mod ipam;
+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..876eb20
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/wireguard.rs
@@ -0,0 +1,311 @@
+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::{PrivateKey, 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::{WireGuardNode, WireGuardNodePeer};
+use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
+
+type WireGuardPrivateKeyLoader = Box<dyn Fn(&str) -> Result<PrivateKey, anyhow::Error>>;
+
+pub struct WireGuardConfigBuilder {
+ fabrics: Valid<FabricConfig>,
+ loader_callback: WireGuardPrivateKeyLoader,
+}
+
+const WIREGUARD_CONFIG_FOLDER: &str = "/etc/wireguard/proxmox";
+
+fn default_load_private_key(interface_name: &str) -> Result<PrivateKey, anyhow::Error> {
+ Ok(PrivateKey::from_raw(
+ proxmox_base64::decode(
+ std::fs::read_to_string(format!(
+ "{}/{}.key",
+ WIREGUARD_CONFIG_FOLDER, interface_name
+ ))
+ .map_err(|_| anyhow::format_err!("could not read private key file"))?,
+ )
+ .map_err(|_| anyhow::format_err!("could not decode base64 string"))?
+ .as_slice()
+ .try_into()
+ .map_err(|_| anyhow::format_err!("private key has invalid format"))?,
+ ))
+}
+
+impl Default for WireGuardConfigBuilder {
+ fn default() -> Self {
+ Self {
+ loader_callback: Box::new(default_load_private_key),
+ fabrics: Default::default(),
+ }
+ }
+}
+
+impl WireGuardConfigBuilder {
+ pub fn with_loader_callback(
+ loader_callback: impl Fn(&str) -> Result<PrivateKey, anyhow::Error> + 'static,
+ ) -> Self {
+ Self {
+ loader_callback: Box::new(loader_callback),
+ fabrics: Default::default(),
+ }
+ }
+
+ pub fn add_fabrics(mut self, fabric: Valid<FabricConfig>) -> Self {
+ self.fabrics = fabric;
+ self
+ }
+
+ pub fn build(
+ self,
+ current_node: NodeId,
+ ) -> Result<HashMap<String, WireGuardConfig>, anyhow::Error> {
+ let mut wireguard_config = HashMap::new();
+
+ for fabric_entry in self.fabrics.values() {
+ let Fabric::WireGuard(fabric_config) = fabric_entry.fabric() else {
+ continue;
+ };
+
+ let Ok(Node::WireGuard(node_config)) = fabric_entry.get_node(¤t_node) else {
+ continue;
+ };
+
+ let WireGuardNode::Internal(node_properties) = node_config.properties() else {
+ continue;
+ };
+
+ for interface in node_properties.interfaces() {
+ let private_key = (self.loader_callback)(&interface.name)?;
+
+ let wireguard_interface = WireGuardInterface {
+ 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),
+ ) => {
+ 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 proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use crate::sdn::fabric::{section_config::Section, FabricConfig};
+
+ use super::*;
+
+ fn mock_load_private_key(_interface_name: &str) -> Result<PrivateKey, anyhow::Error> {
+ Ok(PrivateKey::from_raw(
+ proxmox_base64::decode("qGXl+84iE1teMyQeL1DgkuLivKKYasx6fOYqBfr3QEI=")
+ .expect("valid base64")
+ .as_slice()
+ .try_into()
+ .expect("private key is a 32 byte slice"),
+ ))
+ }
+
+ #[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 mut builder = WireGuardConfigBuilder::with_loader_callback(mock_load_private_key)
+ .add_fabrics(fabric_config);
+
+ 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::with_loader_callback(mock_load_private_key)
+ .add_fabrics(fabric_config);
+
+ 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 [flat|nested] 28+ messages in thread* [PATCH proxmox-perl-rs 1/2] pve-rs: fabrics: wireguard: generate ifupdown2 configuration
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (10 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: implement wireguard config generation Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH proxmox-perl-rs 2/2] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
` (14 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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. This also happens via respective post-up
/ down commands in the ifupdown2 configuration file. Currently, it is
not ECMP aware, so it is not possible to route the same subnet via two
different WireGuard interfaces. This is intended to be implemented in
a future patch series. For now, this function generates two entries in
the routing table and only the first inserted one works, analogous to
how ifupdown2 behaves when configuring the same subnet twice.
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/Cargo.toml | 1 +
pve-rs/src/bindings/sdn/fabrics.rs | 177 ++++++++++++++++++++++++-----
pve-rs/src/sdn/status.rs | 16 +++
3 files changed, 165 insertions(+), 29 deletions(-)
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 527fcae..3757b80 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] }
proxmox-oci = "0.2.1"
proxmox-openid = "1.0.2"
proxmox-resource-scheduling = "1.0.1"
+proxmox-schema = "5"
proxmox-section-config = "3"
proxmox-shared-cache = "1"
proxmox-subscription = "1"
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 54606e7..daec8a7 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -35,6 +35,11 @@ pub mod pve_rs_sdn_fabrics {
};
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
+ use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder;
+
+ use proxmox_ve_config::sdn::fabric::section_config::protocol::wireguard::{
+ WireGuardInterfaceCreateProperties, WireGuardInterfaceProperties, WireGuardNode,
+ };
use crate::sdn::status::{self, RunningConfig};
@@ -364,6 +369,7 @@ pub mod pve_rs_sdn_fabrics {
}
}
}
+ ConfigNode::WireGuard(_) => {}
}
}
@@ -456,6 +462,7 @@ pub mod pve_rs_sdn_fabrics {
FabricEntry::Openfabric(_) => {
daemons.insert("fabricd");
}
+ FabricEntry::WireGuard(_) => {} // not a frr fabric
};
}
@@ -478,8 +485,72 @@ pub mod pve_rs_sdn_fabrics {
to_raw_config(&frr_config)
}
+ /// 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,
+ ) -> Result<HashMap<String, String>, Error> {
+ let config = this.fabric_config.lock().unwrap();
+
+ let configs = WireGuardConfigBuilder::default()
+ .add_fabrics(config.clone().into_valid()?)
+ .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>(
+ name: &str,
+ cidrs: (Option<&Ipv4Cidr>, Option<&Ipv6Cidr>),
+ allowed_ips: impl Iterator<Item = &'a Cidr>,
+ ) -> Result<String, Error> {
+ let mut interface = String::new();
+
+ let (ip, ip6) = cidrs;
+
+ writeln!(interface, "auto {name}")?;
+
+ if let Some(ip) = 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) = 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}")?;
@@ -488,14 +559,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(
@@ -513,37 +612,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}")?;
}
@@ -560,7 +642,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}")?;
}
@@ -568,10 +650,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(
@@ -579,9 +661,43 @@ 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| {
+ return if peer.iface() == interface.name() {
+ let ConfigNode::WireGuard(node) =
+ entry.get_node(peer.node()).ok()?
+ else {
+ return None;
+ };
+
+ Some(node.properties().allowed_ips())
+ } else {
+ None
+ };
+ })
+ .flatten();
+
+ let interface = render_wireguard_interface(
+ interface.name(),
+ (interface.ip(), interface.ip6()),
+ allowed_ips,
+ )?;
+
write!(interfaces, "{interface}")?;
}
}
@@ -678,6 +794,7 @@ pub mod pve_rs_sdn_fabrics {
status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename())
}
+ FabricEntry::WireGuard(_) => Ok(Vec::new()),
}
}
@@ -736,6 +853,7 @@ pub mod pve_rs_sdn_fabrics {
)
.map(|v| v.into())
}
+ FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
}
}
@@ -795,6 +913,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 [flat|nested] 28+ messages in thread* [PATCH proxmox-perl-rs 2/2] pve-rs: fabrics: add helpers for parsing interface property strings
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (11 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-perl-rs 1/2] pve-rs: fabrics: wireguard: generate ifupdown2 configuration Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 1/3] sdn: add wireguard helper module Stefan Hanreich
` (13 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 daec8a7..78f66f0 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -19,6 +19,7 @@ pub mod pve_rs_sdn_fabrics {
use perlmod::Value;
use proxmox_frr::ser::serializer::to_raw_config;
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};
@@ -337,6 +338,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 [flat|nested] 28+ messages in thread* [PATCH pve-network 1/3] sdn: add wireguard helper module
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (12 preceding siblings ...)
2026-02-19 14:56 ` [PATCH proxmox-perl-rs 2/2] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 2/3] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
` (12 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 15 ++-
src/PVE/Network/SDN/WireGuard.pm | 163 +++++++++++++++++++++++++++++++
2 files changed, 177 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 d1ffef9..90bfffa 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,17 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm Frr.pm
+SOURCES=\
+ Controllers.pm \
+ Dhcp.pm \
+ Dns.pm \
+ Fabrics.pm \
+ Frr.pm \
+ Ipams.pm \
+ RouteMap.pm \
+ SubnetPlugin.pm \
+ Subnets.pm \
+ VnetPlugin.pm \
+ Vnets.pm \
+ WireGuard.pm \
+ Zones.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..1db44db
--- /dev/null
+++ b/src/PVE/Network/SDN/WireGuard.pm
@@ -0,0 +1,163 @@
+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::File;
+use PVE::INotify;
+use PVE::RESTEnvironment qw(log_warn);
+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";
+
+=head3 create_wireguard_keypair($interface_name)
+
+Creates a new WireGuard keypair on the current node. The keys are stored in the
+$wireguard_config_folder path as '${interface_name}.key' and
+'${interface_name}.key.pub' respectively.
+
+If the private key file already exists, then a warning is printed and the
+existing corresponding public key returned.
+
+=cut
+
+sub create_wireguard_keypair {
+ my ($interface_name) = @_;
+
+ my $code = sub {
+ mkdir("/etc/wireguard") if !-d '/etc/wireguard';
+ mkdir($wireguard_config_folder) if !-d $wireguard_config_folder;
+
+ my $private_key_file = "$wireguard_config_folder/${interface_name}.key";
+ my $public_key_file = "$wireguard_config_folder/${interface_name}.key.pub";
+
+ if (-e $private_key_file) {
+ warn
+ "trying to create keypair for interface $interface_name, but file already exists!\n";
+ return PVE::File::file_get_contents($public_key_file);
+ }
+
+ my $private_key = undef;
+ PVE::Tools::run_command(
+ ['wg', 'genkey'],
+ outfunc => sub {
+ $private_key = shift;
+ },
+ );
+
+ my $public_key = undef;
+ PVE::Tools::run_command(
+ ['wg', 'pubkey'],
+ input => $private_key,
+ outfunc => sub {
+ $public_key = shift;
+ },
+ );
+
+ PVE::File::file_set_contents($private_key_file, $private_key, 400);
+ PVE::File::file_set_contents($public_key_file, $public_key, 400);
+
+ return $public_key;
+ };
+
+ my $public_key = PVE::Tools::lock_file($local_wireguard_lock, 10, $code);
+ die $@ if $@;
+
+ return $public_key;
+}
+
+=head3 delete_wireguard_keypair($interface_name)
+
+Deletes the public / private key files for a given WireGuard interface with name
+$interface_name, if they exist in the filesystem. Otherwise a warning is printed
+and nothing deleted.
+
+=cut
+
+sub delete_wireguard_keypair {
+ my ($interface_name) = @_;
+
+ my $code = sub {
+ unlink "$wireguard_config_folder/${interface_name}.key"
+ if -e "$wireguard_config_folder/${interface_name}.key";
+
+ unlink "$wireguard_config_folder/${interface_name}.key.pub"
+ if -e "$wireguard_config_folder/${interface_name}.key.pub";
+ };
+
+ PVE::Tools::lock_file($local_wireguard_lock, 10, $code);
+ warn $@ if $@;
+}
+
+=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 $fabric_config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $nodename = PVE::INotify::nodename();
+ my $raw_config = $fabric_config->get_wireguard_raw_config($nodename);
+
+ if (!-e "/usr/bin/wg") {
+ warn
+ "In order to apply the generated WireGuard configuration the package 'wg-tools' needs to be installed.\n";
+ }
+
+ 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) = @_;
+
+ for my $interface (keys $raw_config->%*) {
+ PVE::File::file_set_contents(
+ "$wireguard_config_folder/$interface.conf",
+ $raw_config->{$interface},
+ 400,
+ );
+
+ if ($apply) {
+ eval {
+ PVE::Tools::run_command(
+ ['wg', 'syncconf', $interface, "/etc/wireguard/proxmox/$interface.conf"]);
+ };
+ warn $@ if $@;
+ }
+ }
+}
+
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH pve-network 2/3] fabrics: wireguard: add schema definitions for wireguard
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (13 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-network 1/3] sdn: add wireguard helper module Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 3/3] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
` (11 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 9 +-
src/PVE/Network/SDN/Fabrics.pm | 257 ++++++++++++++++++++++++++++++++-
3 files changed, 260 insertions(+), 8 deletions(-)
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index b35a588..aa8d359 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -107,7 +107,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 c7c390e..78b15d5 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -472,7 +472,14 @@ sub generate_dhcp_config {
sub encode_value {
my ($type, $key, $value) = @_;
- if ($key eq 'nodes' || $key eq 'exitnodes' || $key eq 'dhcp-range' || $key eq 'interfaces') {
+ if (
+ $key eq 'nodes'
+ || $key eq 'exitnodes'
+ || $key eq 'dhcp-range'
+ || $key eq 'interfaces'
+ || $key eq 'peers'
+ || $key eq 'allowed_ips'
+ ) {
if (ref($value) eq 'HASH') {
return join(',', sort keys(%$value));
} elsif (ref($value) eq 'ARRAY') {
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index d90992a..a81b62e 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -45,7 +45,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,
+ },
},
);
@@ -203,18 +240,202 @@ 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 this node that uses this peer definition.',
+ optional => 1,
+ type => 'string',
+ },
+ iface => {
+ description =>
+ 'The interface of this node that uses this peer definition.',
+ optional => 1,
+ type => 'string',
+ },
+ endpoint => {
+ description =>
+ 'Override for the endpoint settings in the node section.',
+ optional => 1,
+ type => 'string',
+ },
+ allowed_ips => {
+ type => 'array',
+ optional => 1,
+ description => 'Additional allowed IPs for this peer.',
+ items => {
+ type => 'string',
+ format => 'CIDR',
+ },
+ },
+ },
+ },
+ },
+ 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',
+ 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 this node that uses this peer definition.',
+ optional => 1,
+ type => 'string',
+ },
+ iface => {
+ description =>
+ 'The interface of this node that uses this peer definition.',
+ optional => 1,
+ type => 'string',
+ },
+ endpoint => {
+ description =>
+ 'Override for the endpoint settings in the node section.',
+ optional => 1,
+ type => 'string',
+ },
+ allowed_ips => {
+ type => 'array',
+ optional => 1,
+ description => 'Additional allowed IPs for this peer.',
+ items => {
+ type => 'string',
+ format => 'CIDR',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ 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,
+ },
+ ],
};
}
@@ -267,6 +488,21 @@ sub fabric_properties {
'OSPF area. Either a IPv4 address or a 32-bit number. Gets validated in rust.',
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) {
@@ -293,6 +529,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 [flat|nested] 28+ messages in thread* [PATCH pve-network 3/3] fabrics: wireguard: implement wireguard key auto-generation
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (14 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-network 2/3] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 01/11] network: sdn: generate wireguard configuration on apply Stefan Hanreich
` (10 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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. This is accomplished by proxying create /
update / delete API calls for internal wireguard nodes to the
respective node and handling the wireguard key generation there. 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 while being able to store the generated
wireguard keypairs locally on each node without involving pmxcfs.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../API2/Network/SDN/Fabrics/FabricNode.pm | 129 +++++++++++++++++-
1 file changed, 123 insertions(+), 6 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
index 000e4c3..f6483d5 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,9 +133,20 @@ __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 => '',
+ proxyto_callback => sub {
+ my ($rpcenv, $proxyto, $param) = @_;
+
+ return $param->{node_id} if is_internal_wireguard_node($param);
+ return 'localhost';
+ },
method => 'POST',
description => 'Add a node',
protected => 1,
@@ -162,8 +175,36 @@ __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 @parsed_interfaces = map {
+ PVE::RS::SDN::Fabrics::parse_wireguard_create_interface($_)
+ } $param->{interfaces}->@*;
+
+ my @interfaces;
+ for my $interface (@parsed_interfaces) {
+ $interface->{public_key} =
+ PVE::Network::SDN::WireGuard::create_wireguard_keypair(
+ $interface->{name});
+ push @interfaces,
+ PVE::RS::SDN::Fabrics::print_wireguard_interface($interface);
+ }
+
+ $param->{interfaces} = \@interfaces;
+ $config->add_node($param);
+
+ eval { PVE::Network::SDN::Fabrics::write_config($config) };
+ if (my $err = $@) {
+ for my $interface (@parsed_interfaces) {
+ PVE::Network::SDN::WireGuard::delete_wireguard_keypair(
+ $interface->{name});
+ }
+
+ die $err;
+ }
+ } else {
+ $config->add_node($param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }
},
"adding node failed",
$lock_token,
@@ -174,6 +215,15 @@ __PACKAGE__->register_method({
__PACKAGE__->register_method({
name => 'update_node',
path => '{node_id}',
+ proxyto_callback => sub {
+ my ($rpcenv, $proxyto, $param) = @_;
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+ my $old_node = $config->get_node($param->{fabric_id}, $param->{node_id});
+
+ return $old_node->{node_id} if is_internal_wireguard_node($old_node);
+ return 'localhost';
+ },
method => 'PUT',
description => 'Update a node',
protected => 1,
@@ -205,8 +255,55 @@ __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 %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} =
+ PVE::Network::SDN::WireGuard::create_wireguard_keypair($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::Fabrics::write_config($config); };
+
+ if (my $err = $@) {
+ for my $interface (values %new_interfaces) {
+ PVE::Network::SDN::WireGuard::delete_wireguard_keypair(
+ $interface->{name});
+ }
+
+ die $err;
+ }
+
+ for my $interface_name (keys %old_interfaces) {
+ PVE::Network::SDN::WireGuard::delete_wireguard_keypair($interface_name)
+ if !exists($new_interfaces{$interface_name});
+ }
+ } else {
+ $config->update_node($fabric_id, $node_id, $param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }
},
"updating node failed",
$lock_token,
@@ -220,6 +317,15 @@ __PACKAGE__->register_method({
method => 'DELETE',
description => 'Add a node',
protected => 1,
+ proxyto_callback => sub {
+ my ($rpcenv, $proxyto, $param) = @_;
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+ my $old_node = $config->get_node($param->{fabric_id}, $param->{node_id});
+
+ return $old_node->{node_id} if is_internal_wireguard_node($old_node);
+ return 'localhost';
+ },
permissions => {
check => [
'and',
@@ -251,8 +357,19 @@ __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);
+
+ if (is_internal_wireguard_node($old_node)) {
+ for my $interface_string ($old_node->{interfaces}->@*) {
+ my $interface =
+ PVE::RS::SDN::Fabrics::parse_wireguard_interface($interface_string);
+ PVE::Network::SDN::WireGuard::delete_wireguard_keypair(
+ $interface->{name});
+ }
+ }
},
"deleting node failed",
$lock_token,
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH pve-manager 01/11] network: sdn: generate wireguard configuration on apply
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (15 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-network 3/3] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 02/11] ui: fix parsing of property-strings when values contain = Stefan Hanreich
` (9 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 02/11] ui: fix parsing of property-strings when values contain =
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (16 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 01/11] network: sdn: generate wireguard configuration on apply Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 03/11] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
` (8 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 03/11] ui: fabrics: i18n: make node loading string translatable
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (17 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 02/11] ui: fix parsing of property-strings when values contain = Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 04/11] ui: fabrics: split node selector creation and config Stefan Hanreich
` (7 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 04/11] ui: fabrics: split node selector creation and config
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (18 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 03/11] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 05/11] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
` (6 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 05/11] ui: fabrics: edit: make ipv4/6 support generic over fabric panels
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (19 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 04/11] ui: fabrics: split node selector creation and config Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 06/11] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
` (5 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 14b71fae2..b89764293 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 4c4e17c99..cbbe8c9c8 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 06/11] ui: fabrics: node: make ipv4/6 support generic over edit panels
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (20 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 05/11] ui: fabrics: edit: make ipv4/6 support generic over fabric panels Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 07/11] ui: fabrics: interface: " Stefan Hanreich
` (4 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 07/11] ui: fabrics: interface: make ipv4/6 support generic over edit panels
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (21 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 06/11] ui: fabrics: node: make ipv4/6 support generic over edit panels Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 08/11] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
` (3 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 08/11] ui: fabrics: wireguard: add interface edit panel
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (22 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 07/11] ui: fabrics: interface: " Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 09/11] ui: fabrics: wireguard: add node " Stefan Hanreich
` (2 subsequent siblings)
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 427 ++++++++++++++++++
2 files changed, 428 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4558d53e5..372431e8d 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -333,6 +333,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..aa30fe2d0
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/InterfacePanel.js
@@ -0,0 +1,427 @@
+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: '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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 09/11] ui: fabrics: wireguard: add node edit panel
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (23 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 08/11] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 10/11] ui: fabrics: wireguard: add fabric " Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 11/11] ui: fabrics: hook up wireguard components Stefan Hanreich
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 | 230 ++++++++++++++++++
2 files changed, 231 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/wireguard/NodeEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 372431e8d..a0f06024a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -334,6 +334,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..7b1b7ac73
--- /dev/null
+++ b/www/manager6/sdn/fabrics/wireguard/NodeEdit.js
@@ -0,0 +1,230 @@
+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;
+ },
+ },
+ nodeSelectorDisabled: function (get) {
+ return get('isCreate') || !get('current.isPveNode');
+ },
+ },
+ },
+
+ 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'),
+ labelWidth: 120,
+ name: 'endpoint',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Allowed IPs'),
+ 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');
+ },
+ },
+ });
+
+ 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}',
+ disabled: '{nodeSelectorDisabled}',
+ });
+ },
+});
--
2.47.3
^ permalink raw reply [flat|nested] 28+ messages in thread* [PATCH pve-manager 10/11] ui: fabrics: wireguard: add fabric edit panel
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (24 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 09/11] ui: fabrics: wireguard: add node " Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 11/11] ui: fabrics: hook up wireguard components Stefan Hanreich
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 a0f06024a..e3a704d9d 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -335,6 +335,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 [flat|nested] 28+ messages in thread* [PATCH pve-manager 11/11] ui: fabrics: hook up wireguard components
2026-02-19 14:56 [RFC manager/network/proxmox{,-ve-rs,-perl-rs} 00/27] Add WireGuard as protocol to SDN fabrics Stefan Hanreich
` (25 preceding siblings ...)
2026-02-19 14:56 ` [PATCH pve-manager 10/11] ui: fabrics: wireguard: add fabric " Stefan Hanreich
@ 2026-02-19 14:56 ` Stefan Hanreich
26 siblings, 0 replies; 28+ messages in thread
From: Stefan Hanreich @ 2026-02-19 14:56 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 [flat|nested] 28+ messages in thread