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 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.