all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: implement wireguard config generation
Date: Thu, 19 Feb 2026 15:56:28 +0100	[thread overview]
Message-ID: <20260219145649.441418-12-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260219145649.441418-1-s.hanreich@proxmox.com>

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(&current_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




  parent reply	other threads:[~2026-02-19 14:58 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` [PATCH proxmox-ve-rs 1/9] debian: update control file 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
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 ` [PATCH proxmox-ve-rs 4/9] ve-config: fabrics: split interface name regex into two parts 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
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 ` [PATCH proxmox-ve-rs 7/9] ve-config: sdn: fabrics: add wireguard to the fabric config Stefan Hanreich
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 [this message]
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 ` [PATCH proxmox-perl-rs 2/2] pve-rs: fabrics: add helpers for parsing interface property strings Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 1/3] sdn: add wireguard helper module Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 2/3] fabrics: wireguard: add schema definitions for wireguard Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-network 3/3] fabrics: wireguard: implement wireguard key auto-generation Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 01/11] network: sdn: generate wireguard configuration on apply Stefan Hanreich
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 ` [PATCH pve-manager 03/11] ui: fabrics: i18n: make node loading string translatable Stefan Hanreich
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 ` [PATCH pve-manager 05/11] ui: fabrics: edit: make ipv4/6 support generic over fabric panels 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
2026-02-19 14:56 ` [PATCH pve-manager 07/11] ui: fabrics: interface: " Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 08/11] ui: fabrics: wireguard: add interface edit panel Stefan Hanreich
2026-02-19 14:56 ` [PATCH pve-manager 09/11] ui: fabrics: wireguard: add node " 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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260219145649.441418-12-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal