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
next prev 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