From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 3311C1FF141 for ; Fri, 16 Jan 2026 16:34:36 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C46511C6BB; Fri, 16 Jan 2026 16:34:25 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Date: Fri, 16 Jan 2026 16:33:11 +0100 Message-ID: <20260116153317.1146323-7-c.heiss@proxmox.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20260116153317.1146323-1-c.heiss@proxmox.com> References: <20260116153317.1146323-1-c.heiss@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1768577580165 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.001 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SHORT 0.001 Use of a URL Shortener for very short URL PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record WEIRD_PORT 0.001 Uses non-standard port number for HTTP Subject: [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" 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 --- 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 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 , + rustc:native (>= 1.82) , + libstd-rust-dev , + 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 +Maintainer: Proxmox Support Team +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 +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 . 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 " + +[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 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, "") + } +} + +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 { + 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 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, "") + } +} + +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 { + 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, + /// Remote peer endpoint address to connect to. Optional; only needed on the connecting side. + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + /// 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, +} + +/// 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) -> 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, + /// Fwmark for outgoing packets. + #[serde(skip_serializing_if = "Option::is_none")] + pub fw_mark: Option, +} + +/// 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, +} + +impl WireGuardConfig { + /// Generate a raw, INI-style configuration file as accepted by wg(8). + pub fn to_raw_config(self) -> Result { + Ok(proxmox_serde::ini::to_string(&self)?) + } +} + +/// Generates a new ED25519 private key. +fn generate_key() -> Result { + 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::>().try_into().unwrap()) + } + + fn mock_preshared_key(v: u8) -> PresharedKey { + let base = v * 32; + PresharedKey((base..base + 32).collect::>().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