public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate
Date: Fri, 16 Jan 2026 16:33:11 +0100	[thread overview]
Message-ID: <20260116153317.1146323-7-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260116153317.1146323-1-c.heiss@proxmox.com>

This introduces a new crate, `proxmox-wireguard`.

It provides:
- a slight abstraction over raw ED25519 keys and keyspairs, as used by
  WireGuard
- keypair generation
- wg(8) configuration support, by providing the necessary structs and
  INI serialization thereof

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Cargo.toml                             |   1 +
 proxmox-wireguard/Cargo.toml           |  21 ++
 proxmox-wireguard/debian/changelog     |   5 +
 proxmox-wireguard/debian/control       |  48 ++++
 proxmox-wireguard/debian/copyright     |  18 ++
 proxmox-wireguard/debian/debcargo.toml |   7 +
 proxmox-wireguard/src/lib.rs           | 320 +++++++++++++++++++++++++
 7 files changed, 420 insertions(+)
 create mode 100644 proxmox-wireguard/Cargo.toml
 create mode 100644 proxmox-wireguard/debian/changelog
 create mode 100644 proxmox-wireguard/debian/control
 create mode 100644 proxmox-wireguard/debian/copyright
 create mode 100644 proxmox-wireguard/debian/debcargo.toml
 create mode 100644 proxmox-wireguard/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index 3cdad8d8..59714664 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,7 @@ members = [
     "proxmox-time-api",
     "proxmox-upgrade-checks",
     "proxmox-uuid",
+    "proxmox-wireguard",
     "proxmox-worker-task",
     "pbs-api-types",
     "pve-api-types",
diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
new file mode 100644
index 00000000..5976aa90
--- /dev/null
+++ b/proxmox-wireguard/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "proxmox-wireguard"
+description = "WireGuard configuration support"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+ed25519-dalek = "2.1"
+serde = { workspace = true, features = [ "derive" ] }
+thiserror.workspace = true
+proxmox-serde = { workspace = true, features = [ "ini-ser" ] }
+proxmox-network-types.workspace = true
+proxmox-sys.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
diff --git a/proxmox-wireguard/debian/changelog b/proxmox-wireguard/debian/changelog
new file mode 100644
index 00000000..a7120b87
--- /dev/null
+++ b/proxmox-wireguard/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-wireguard (0.1.0-1) unstable; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 15 May 2025 14:54:37 +0200
diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control
new file mode 100644
index 00000000..4adc1ac2
--- /dev/null
+++ b/proxmox-wireguard/debian/control
@@ -0,0 +1,48 @@
+Source: rust-proxmox-wireguard
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-ed25519-dalek-2+default-dev (>= 2.1-~~) <!nocheck>,
+ librust-proxmox-network-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+ini-ser-dev <!nocheck>,
+ librust-proxmox-serde-1+serde-json-dev <!nocheck>,
+ librust-proxmox-sys-1+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-thiserror-2+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+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-wireguard
+
+Package: librust-proxmox-wireguard-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-ed25519-dalek-2+default-dev (>= 2.1-~~),
+ librust-proxmox-network-types-0.1+default-dev,
+ librust-proxmox-serde-1+default-dev,
+ librust-proxmox-serde-1+ini-ser-dev,
+ librust-proxmox-serde-1+serde-json-dev,
+ librust-proxmox-sys-1+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-thiserror-2+default-dev
+Provides:
+ librust-proxmox-wireguard+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0+default-dev (= ${binary:Version})
+Description: WireGuard configuration support - Rust source code
+ Source code for Debianized Rust crate "proxmox-wireguard"
diff --git a/proxmox-wireguard/debian/copyright b/proxmox-wireguard/debian/copyright
new file mode 100644
index 00000000..1ea8a56b
--- /dev/null
+++ b/proxmox-wireguard/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-wireguard/debian/debcargo.toml b/proxmox-wireguard/debian/debcargo.toml
new file mode 100644
index 00000000..87a787e6
--- /dev/null
+++ b/proxmox-wireguard/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git"
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
new file mode 100644
index 00000000..840767d8
--- /dev/null
+++ b/proxmox-wireguard/src/lib.rs
@@ -0,0 +1,320 @@
+//! Implements a interface for handling WireGuard configurations and serializing them into the
+//! INI-style format as described in wg(8) "CONFIGURATION FILE FORMAT".
+//!
+//! WireGuard keys are 32-bytes securely-generated values, encoded as base64
+//! for any usage where users might come in contact with them.
+//!
+//! [`PrivateKey`], [`PublicKey`] and [`PresharedKey`] implement all the needed
+//! key primitives.
+//!
+//! By design there is no key pair, as keys should be treated as opaque from a
+//! configuration perspective and not worked with.
+
+#![forbid(unsafe_code, missing_docs)]
+
+use ed25519_dalek::SigningKey;
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr};
+
+/// Possible error when handling WireGuard configurations.
+#[derive(thiserror::Error, Debug, PartialEq, Clone)]
+pub enum Error {
+    /// (Private) key generation failed
+    #[error("failed to generate private key: {0}")]
+    KeyGenFailed(String),
+    /// Serialization  to the WireGuard INI format failed
+    #[error("failed to serialize config: {0}")]
+    SerializationFailed(String),
+}
+
+impl From<proxmox_serde::ini::Error> for Error {
+    fn from(err: proxmox_serde::ini::Error) -> Self {
+        Self::SerializationFailed(err.to_string())
+    }
+}
+
+/// Public key of a WireGuard peer.
+#[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)]
+#[serde(transparent)]
+pub struct PublicKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+);
+
+/// Private key of a WireGuard peer.
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct PrivateKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
+);
+
+impl fmt::Debug for PrivateKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "<private-key>")
+    }
+}
+
+impl PrivateKey {
+    /// Length of the raw private key data in bytes.
+    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
+
+    /// Generates a new private key suitable for use with WireGuard.
+    pub fn generate() -> Result<Self, Error> {
+        generate_key().map(Self)
+    }
+
+    /// Calculates the public key from the private key.
+    pub fn public_key(&self) -> PublicKey {
+        PublicKey(
+            ed25519_dalek::SigningKey::from_bytes(&self.0)
+                .verifying_key()
+                .to_bytes(),
+        )
+    }
+
+    /// Returns the raw private key material.
+    #[must_use]
+    pub fn raw(&self) -> &ed25519_dalek::SecretKey {
+        &self.0
+    }
+
+    /// Builds a new [`PrivateKey`] from raw key material.
+    #[must_use]
+    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
+        // [`SigningKey`] takes care of correct key clamping.
+        Self(SigningKey::from(&data).to_bytes())
+    }
+}
+
+impl From<ed25519_dalek::SecretKey> for PrivateKey {
+    fn from(value: ed25519_dalek::SecretKey) -> Self {
+        Self(value)
+    }
+}
+
+/// Preshared key between two WireGuard peers.
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct PresharedKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
+);
+
+impl fmt::Debug for PresharedKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "<preshared-key>")
+    }
+}
+
+impl PresharedKey {
+    /// Length of the raw private key data in bytes.
+    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
+
+    /// Generates a new preshared key suitable for use with WireGuard.
+    pub fn generate() -> Result<Self, Error> {
+        generate_key().map(Self)
+    }
+
+    /// Returns the raw private key material.
+    #[must_use]
+    pub fn raw(&self) -> &ed25519_dalek::SecretKey {
+        &self.0
+    }
+
+    /// Builds a new [`PrivateKey`] from raw key material.
+    #[must_use]
+    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
+        // [`SigningKey`] takes care of correct key clamping.
+        Self(SigningKey::from(&data).to_bytes())
+    }
+}
+
+/// A single WireGuard peer.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardPeer {
+    /// Public key, matching the private key of of the remote peer.
+    pub public_key: PublicKey,
+    /// 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.
+    pub preshared_key: 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
+    /// matching all IPv4 addresses, and ::/0 may be specified for matching all IPv6 addresses.
+    #[serde(rename = "AllowedIPs")]
+    pub allowed_ips: Vec<Cidr>,
+    /// Remote peer endpoint address to connect to. Optional; only needed on the connecting side.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub endpoint: Option<ServiceEndpoint>,
+    /// 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 a peer, 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.
+    #[serde(skip_serializing_if = "persistent_keepalive_is_off")]
+    pub persistent_keepalive: Option<u16>,
+}
+
+/// 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<u16>) -> bool {
+    value.map(|v| v == 0).unwrap_or(true)
+}
+
+/// Properties of a WireGuard interface.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardInterface {
+    /// Private key for this interface.
+    pub private_key: PrivateKey,
+    /// Port to listen on. Optional; if not specified, chosen randomly. Only needed on the "server"
+    /// side.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub listen_port: Option<u16>,
+    /// Fwmark for outgoing packets.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub fw_mark: Option<u32>,
+}
+
+/// Top-level WireGuard configuration for WireGuard network interface. Holds all
+/// parameters for the interface itself, as well as its remote peers.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardConfig {
+    /// The WireGuard-specific network interface configuration.
+    pub interface: WireGuardInterface,
+    /// Peers for this WireGuard interface.
+    #[serde(rename = "Peer")]
+    pub peers: Vec<WireGuardPeer>,
+}
+
+impl WireGuardConfig {
+    /// Generate a raw, INI-style configuration file as accepted by wg(8).
+    pub fn to_raw_config(self) -> Result<String, Error> {
+        Ok(proxmox_serde::ini::to_string(&self)?)
+    }
+}
+
+/// Generates a new ED25519 private key.
+fn generate_key() -> Result<ed25519_dalek::SecretKey, Error> {
+    let mut secret = ed25519_dalek::SecretKey::default();
+    proxmox_sys::linux::fill_with_random_data(&mut secret)
+        .map_err(|err| Error::KeyGenFailed(err.to_string()))?;
+
+    // [`SigningKey`] takes care of correct key clamping.
+    Ok(SigningKey::from(&secret).to_bytes())
+}
+
+#[cfg(test)]
+mod tests {
+    use std::net::Ipv4Addr;
+
+    use proxmox_network_types::ip_address::Cidr;
+
+    use crate::{PresharedKey, PrivateKey, WireGuardConfig, WireGuardInterface, WireGuardPeer};
+
+    fn mock_private_key(v: u8) -> PrivateKey {
+        let base = v * 32;
+        PrivateKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
+    }
+
+    fn mock_preshared_key(v: u8) -> PresharedKey {
+        let base = v * 32;
+        PresharedKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
+    }
+
+    #[test]
+    fn single_peer() {
+        let config = WireGuardConfig {
+            interface: WireGuardInterface {
+                private_key: mock_private_key(0),
+                listen_port: Some(51820),
+                fw_mark: Some(127),
+            },
+            peers: vec![WireGuardPeer {
+                public_key: mock_private_key(1).public_key(),
+                preshared_key: mock_preshared_key(1),
+                allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()],
+                endpoint: Some("foo.example.com:51820".parse().unwrap()),
+                persistent_keepalive: Some(25),
+            }],
+        };
+
+        pretty_assertions::assert_eq!(
+            config.to_raw_config().unwrap(),
+            "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+ListenPort = 51820
+FwMark = 127
+
+[Peer]
+PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+AllowedIPs = 192.168.0.0/24
+Endpoint = foo.example.com:51820
+PersistentKeepalive = 25
+"
+        );
+    }
+
+    #[test]
+    fn multiple_peers() {
+        let config = WireGuardConfig {
+            interface: WireGuardInterface {
+                private_key: mock_private_key(0),
+                listen_port: Some(51820),
+                fw_mark: None,
+            },
+            peers: vec![
+                WireGuardPeer {
+                    public_key: mock_private_key(1).public_key(),
+                    preshared_key: mock_preshared_key(1),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()],
+                    endpoint: Some("foo.example.com:51820".parse().unwrap()),
+                    persistent_keepalive: None,
+                },
+                WireGuardPeer {
+                    public_key: mock_private_key(2).public_key(),
+                    preshared_key: mock_preshared_key(2),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap()],
+                    endpoint: None,
+                    persistent_keepalive: Some(25),
+                },
+                WireGuardPeer {
+                    public_key: mock_private_key(3).public_key(),
+                    preshared_key: mock_preshared_key(3),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 2, 0), 24).unwrap()],
+                    endpoint: None,
+                    persistent_keepalive: None,
+                },
+            ],
+        };
+
+        pretty_assertions::assert_eq!(
+            config.to_raw_config().unwrap(),
+            "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+ListenPort = 51820
+
+[Peer]
+PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+AllowedIPs = 192.168.0.0/24
+Endpoint = foo.example.com:51820
+
+[Peer]
+PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0=
+PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=
+AllowedIPs = 192.168.1.0/24
+PersistentKeepalive = 25
+
+[Peer]
+PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U=
+PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8=
+AllowedIPs = 192.168.2.0/24
+"
+        );
+    }
+}
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  parent reply	other threads:[~2026-01-16 15:34 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
2026-01-16 15:33 ` Christoph Heiss [this message]
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types Christoph Heiss

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=20260116153317.1146323-7-c.heiss@proxmox.com \
    --to=c.heiss@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal