* [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support
@ 2026-03-30 18:28 Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 1/8] ini: add crate for INI serialization Christoph Heiss
` (8 more replies)
0 siblings, 9 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
This series lays the groundwork with initial primitives and configuration
support for adding WireGuard as a new SDN fabric to our stack in the
future.
Nothing of this code is actively used anywhere in the stack yet, but
Stefan already sent out a series adding WireGuard as a new SDN fabric,
that depends on this series [0].
[0] https://lore.proxmox.com/pve-devel/20260219145649.441418-1-s.hanreich@proxmox.com/
History
=======
v2: https://lore.proxmox.com/all/20260213143601.1424613-1-c.heiss@proxmox.com/
v1: https://lore.proxmox.com/pve-devel/20260116153317.1146323-1-c.heiss@proxmox.com/
Notable changes v2 -> v3:
* split out ini serializer into dedicated `proxmox-ini` crate
* rework ini serialization to avoid nested serializer instances,
instead serializing all values directly in place
Notable changes v1 -> v2:
* key generation (and thus the dependency on proxmox-sys) is now
feature-gated and optional
* dropped proxmox-ve-rs patches, Stefan will be incorporating them
his SDN series
Diffstat
========
Christoph Heiss (6):
ini: add crate for INI serialization
serde: add base64 module for byte arrays
network-types: add ServiceEndpoint type as host/port tuple abstraction
schema: provide integer schema for node ports
schema: api-types: add ed25519 base64 encoded key schema
wireguard: init configuration support crate
Stefan Hanreich (2):
wireguard: implement api for PublicKey
wireguard: make per-peer preshared key optional
Cargo.toml | 4 +
proxmox-ini/Cargo.toml | 18 +
proxmox-ini/debian/changelog | 5 +
proxmox-ini/debian/control | 32 +
proxmox-ini/debian/copyright | 18 +
proxmox-ini/debian/debcargo.toml | 7 +
proxmox-ini/src/lib.rs | 991 +++++++++++++++++++++++++
proxmox-network-types/src/endpoint.rs | 154 ++++
proxmox-network-types/src/lib.rs | 1 +
proxmox-schema/src/api_types.rs | 19 +-
proxmox-serde/src/lib.rs | 91 +++
proxmox-wireguard/Cargo.toml | 29 +
proxmox-wireguard/debian/changelog | 5 +
proxmox-wireguard/debian/control | 86 +++
proxmox-wireguard/debian/copyright | 18 +
proxmox-wireguard/debian/debcargo.toml | 7 +
proxmox-wireguard/src/lib.rs | 408 ++++++++++
17 files changed, 1892 insertions(+), 1 deletion(-)
create mode 100644 proxmox-ini/Cargo.toml
create mode 100644 proxmox-ini/debian/changelog
create mode 100644 proxmox-ini/debian/control
create mode 100644 proxmox-ini/debian/copyright
create mode 100644 proxmox-ini/debian/debcargo.toml
create mode 100644 proxmox-ini/src/lib.rs
create mode 100644 proxmox-network-types/src/endpoint.rs
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
--
2.52.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 1/8] ini: add crate for INI serialization
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 2/8] serde: add base64 module for byte arrays Christoph Heiss
` (7 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
This is needed for serializing WireGuard configurations, to allow
consumption by the official wg(8) tooling. It uses a pretty standard
INI-like format.
One of the "quirks" of the INI format used is that multiple sections the
same name are supported, which is also supported here.
E.g. `wg syncconf` will be used by in the future by the WireGuard fabric
for applying changes to a particular WireGuard interface.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* move to dedicated `proxmox-ini` crate
* rework map serialization to avoid nested serializer instances,
instead serializing all values directly in place (thanks Wolfgang!)
* make better use of custom error type to indicate whether we last
serialized a primitive type or a section
* expand tests, esp. around nested lists and structs
Changes v1 -> v2:
* use correct version of the `pretty-assertions` crate
Cargo.toml | 2 +
proxmox-ini/Cargo.toml | 18 +
proxmox-ini/debian/changelog | 5 +
proxmox-ini/debian/control | 32 +
proxmox-ini/debian/copyright | 18 +
proxmox-ini/debian/debcargo.toml | 7 +
proxmox-ini/src/lib.rs | 991 +++++++++++++++++++++++++++++++
7 files changed, 1073 insertions(+)
create mode 100644 proxmox-ini/Cargo.toml
create mode 100644 proxmox-ini/debian/changelog
create mode 100644 proxmox-ini/debian/control
create mode 100644 proxmox-ini/debian/copyright
create mode 100644 proxmox-ini/debian/debcargo.toml
create mode 100644 proxmox-ini/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 02ff7f81..4617bbab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ members = [
"proxmox-http",
"proxmox-http-error",
"proxmox-human-byte",
+ "proxmox-ini",
"proxmox-io",
"proxmox-lang",
"proxmox-ldap",
@@ -117,6 +118,7 @@ openssl = "0.10"
pam-sys = "0.5"
percent-encoding = "2.1"
pin-utils = "0.1.0"
+pretty_assertions = "1.4"
proc-macro2 = "1.0"
quick-xml = "0.36.1"
quote = "1.0"
diff --git a/proxmox-ini/Cargo.toml b/proxmox-ini/Cargo.toml
new file mode 100644
index 00000000..07af66c4
--- /dev/null
+++ b/proxmox-ini/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "proxmox-ini"
+description = "INI format support for serde"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
+serde = { workspace = true, features = ["derive"] }
diff --git a/proxmox-ini/debian/changelog b/proxmox-ini/debian/changelog
new file mode 100644
index 00000000..064f3e2f
--- /dev/null
+++ b/proxmox-ini/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-ini (0.1.0-1) unstable; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Fri, 27 Mar 2026 12:03:43 +0100
diff --git a/proxmox-ini/debian/control b/proxmox-ini/debian/control
new file mode 100644
index 00000000..4a724570
--- /dev/null
+++ b/proxmox-ini/debian/control
@@ -0,0 +1,32 @@
+Source: rust-proxmox-ini
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-ini
+
+Package: librust-proxmox-ini-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-serde-1+default-dev
+Provides:
+ librust-proxmox-ini+default-dev (= ${binary:Version}),
+ librust-proxmox-ini-0-dev (= ${binary:Version}),
+ librust-proxmox-ini-0+default-dev (= ${binary:Version}),
+ librust-proxmox-ini-0.1-dev (= ${binary:Version}),
+ librust-proxmox-ini-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-ini-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-ini-0.1.0+default-dev (= ${binary:Version})
+Description: INI format support for serde - Rust source code
+ Source code for Debianized Rust crate "proxmox-ini"
diff --git a/proxmox-ini/debian/copyright b/proxmox-ini/debian/copyright
new file mode 100644
index 00000000..01138fa0
--- /dev/null
+++ b/proxmox-ini/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2026 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-ini/debian/debcargo.toml b/proxmox-ini/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-ini/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.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-ini/src/lib.rs b/proxmox-ini/src/lib.rs
new file mode 100644
index 00000000..921a92e8
--- /dev/null
+++ b/proxmox-ini/src/lib.rs
@@ -0,0 +1,991 @@
+//! Implements a serde serializer for the INI file format.
+//!
+//! Nested structs/maps are supported and use the widely used variant of using dots as hierarchy
+//! separators.
+//!
+//! Newtype variants, tuple variants, struct variants and raw bytes are not supported.
+
+#![forbid(unsafe_code, missing_docs)]
+
+use std::{
+ collections::BTreeMap,
+ fmt::{self, Display, Write},
+ io,
+};
+
+use serde::ser::{self, Impossible, Serialize};
+
+#[derive(Debug, PartialEq)]
+/// Errors that can occur during INI serialization.
+pub enum Error {
+ /// Some error that occurred elsewhere.
+ Generic(String),
+ /// Error during I/O.
+ Io(String),
+ /// Encountered an unsupported data type during serialization.
+ UnsupportedType(&'static str),
+ /// A key was expected at this point during serialization, but a value was received.
+ ExpectedKey,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Generic(s) => write!(f, "{s}"),
+ Error::Io(s) => write!(f, "{s}"),
+ Error::UnsupportedType(s) => write!(f, "unsupported data type: {s}"),
+ Error::ExpectedKey => write!(f, "expected key"),
+ }
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl ser::Error for Error {
+ fn custom<T: fmt::Display>(err: T) -> Self {
+ Error::Generic(err.to_string())
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(err: io::Error) -> Self {
+ Self::Io(err.to_string())
+ }
+}
+
+impl From<fmt::Error> for Error {
+ fn from(err: fmt::Error) -> Self {
+ Self::Io(err.to_string())
+ }
+}
+
+impl From<std::string::FromUtf8Error> for Error {
+ fn from(err: std::string::FromUtf8Error) -> Self {
+ Self::Generic(err.to_string())
+ }
+}
+
+/// Return type used throughout the serializer.
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+/// Type of serialized value.
+enum SerializedType {
+ /// Last serialized value was a key-value pair, ie. `key = value`.
+ Simple,
+ /// Last serialized was a section.
+ Section,
+}
+
+/// Implements a serde serializer for transforming Rust values into the INI
+/// format.
+#[derive(Debug)]
+struct IniSerializer {
+ /// Last key observed during serialization
+ last_key: Option<String>,
+ /// Already serialized key-value pairs on this level of the serialization tree
+ buf: String,
+ /// Nested sections under this part of the tree. Multiple sections with the
+ /// same name are allowed.
+ sections: BTreeMap<String, Vec<String>>,
+}
+
+impl IniSerializer {
+ /// Creates a new INI serializer.
+ fn new() -> Self {
+ IniSerializer {
+ last_key: None,
+ buf: String::new(),
+ sections: BTreeMap::new(),
+ }
+ }
+
+ /// Write out the serialized INI to a target implementing [`io::Write`].
+ fn write<W: io::Write>(self, mut w: W) -> Result<()> {
+ w.write_all(self.buf.as_bytes())?;
+ if !self.buf.is_empty() && !self.sections.is_empty() {
+ w.write_all(b"\n")?;
+ }
+
+ for (index, (name, values)) in self.sections.iter().enumerate() {
+ for (nested_idx, section) in values.iter().enumerate() {
+ write!(w, "[{name}]\n{section}")?;
+
+ if nested_idx < values.len() - 1 {
+ w.write_all(b"\n")?;
+ }
+ }
+
+ if index < self.sections.len() - 1 {
+ w.write_all(b"\n")?;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Serializes a value using its [`Display`] implementation.
+ /// If a key is known for this value, it's prepended to the output and forgotten.
+ fn serialize_as_display<T: Display>(&mut self, v: T) -> Result<SerializedType> {
+ if let Some(key) = self.last_key.take() {
+ writeln!(self.buf, "{key} = {v}")?;
+ } else {
+ self.buf += &v.to_string();
+ }
+ Ok(SerializedType::Simple)
+ }
+}
+
+/// Serialize the given data structure as INI into an I/O stream.
+pub fn to_writer<W, T>(writer: W, value: &T) -> Result<()>
+where
+ W: io::Write,
+ T: ?Sized + Serialize,
+{
+ let mut ser = IniSerializer::new();
+ value.serialize(&mut ser)?;
+ ser.write(writer)
+}
+
+/// Serialize the given data structure as INI into a string.
+pub fn to_string<T>(value: &T) -> Result<String>
+where
+ T: ?Sized + Serialize,
+{
+ let mut buf = Vec::new();
+ to_writer(&mut buf, value)?;
+
+ Ok(String::from_utf8(buf)?)
+}
+
+macro_rules! forward_to_display {
+ ($name:ident($ty:ty), $($rest:tt)* ) => {
+ fn $name(self, v: $ty) -> Result<Self::Ok> {
+ self.serialize_as_display(&v)
+ }
+
+ forward_to_display! { $($rest)* }
+ };
+ () => {};
+}
+
+impl<'a> ser::Serializer for &'a mut IniSerializer {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ type SerializeSeq = IniSeqSerializer<'a>;
+ type SerializeTuple = IniSeqSerializer<'a>;
+ type SerializeTupleStruct = IniSeqSerializer<'a>;
+ type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
+ type SerializeMap = IniMapSerializer<'a>;
+ type SerializeStruct = IniMapSerializer<'a>;
+ type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
+
+ forward_to_display! {
+ serialize_bool(bool),
+ serialize_i8(i8),
+ serialize_i16(i16),
+ serialize_i32(i32),
+ serialize_i64(i64),
+ serialize_u8(u8),
+ serialize_u16(u16),
+ serialize_u32(u32),
+ serialize_u64(u64),
+ serialize_f32(f32),
+ serialize_f64(f64),
+ serialize_char(char),
+ serialize_str(&str),
+ }
+
+ fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok> {
+ Err(Error::UnsupportedType("raw bytes"))
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok> {
+ self.last_key = None;
+ Ok(Self::Ok::Simple)
+ }
+
+ fn serialize_some<T>(self, v: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ v.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok> {
+ self.serialize_none()
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
+ self.serialize_none()
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Self::Ok> {
+ self.serialize_str(variant)
+ }
+
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ Err(Error::UnsupportedType("enum newtype variant"))
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Ok(IniSeqSerializer::new(self))
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ Err(Error::UnsupportedType("enum tuple variant"))
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Ok(IniMapSerializer {
+ ser: self,
+ last_key: None,
+ })
+ }
+
+ fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
+ self.serialize_map(Some(len))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ Err(Error::UnsupportedType("enum struct variant"))
+ }
+}
+
+struct IniMapSerializer<'a> {
+ /// Root serializer.
+ ser: &'a mut IniSerializer,
+ /// Last serialized key observed at this level.
+ last_key: Option<String>,
+}
+
+impl<'a> ser::SerializeMap for IniMapSerializer<'a> {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ fn serialize_key<T>(&mut self, key: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ let mut s = String::new();
+ key.serialize(IniKeySerializer::new(&mut s))?;
+
+ self.last_key = Some(s);
+ self.ser.last_key = self.last_key.clone();
+
+ Ok(())
+ }
+
+ fn serialize_value<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ let mut serializer = IniSerializer::new();
+ serializer.last_key = self.ser.last_key.clone();
+
+ let key = self.last_key.clone().ok_or(Error::ExpectedKey)?;
+
+ match value.serialize(&mut serializer)? {
+ SerializedType::Simple => {
+ // Value serialized as a primitive type, we can just write that out
+
+ self.ser.buf += &serializer.buf;
+ }
+ SerializedType::Section => {
+ dbg!(&serializer.buf, &serializer.sections);
+
+ if !serializer.buf.is_empty() {
+ // First, add all top-level entries from the map into a new section,
+ // in case we serialized a map
+ self.ser
+ .sections
+ .entry(key.clone())
+ .or_default()
+ .push(serializer.buf);
+ } else if let Some(mut values) = serializer.sections.remove(&key) {
+ // Otherwise we serialized a sequence of maps, append all of them under the current
+ // name
+ self.ser
+ .sections
+ .entry(key.clone())
+ .or_default()
+ .append(&mut values);
+ }
+
+ // .. and finally, append all other nested sections
+ for (name, mut values) in serializer.sections {
+ self.ser
+ .sections
+ .entry(format!("{key}.{name}"))
+ .or_default()
+ .append(&mut values);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ Ok(Self::Ok::Section)
+ }
+}
+
+impl<'a> ser::SerializeStruct for IniMapSerializer<'a> {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ ser::SerializeMap::serialize_key(self, key)?;
+ ser::SerializeMap::serialize_value(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ ser::SerializeMap::end(self)
+ }
+}
+
+struct IniSeqSerializer<'a> {
+ /// Root serializer.
+ ser: &'a mut IniSerializer,
+ /// Whether at least one element has been serialized yet.
+ first: bool,
+ /// Whether we saw at least one section in the past.
+ has_sections: bool,
+}
+
+impl<'a> IniSeqSerializer<'a> {
+ pub fn new(ser: &'a mut IniSerializer) -> Self {
+ Self {
+ ser,
+ first: true,
+ has_sections: false,
+ }
+ }
+}
+
+impl ser::SerializeSeq for IniSeqSerializer<'_> {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ // As we (at least, for now) don't support enum newtype variants, types which serialize to
+ // either a primitive type or a section type cannot be represested.
+
+ let mut serializer = IniSerializer::new();
+ let key = self.ser.last_key.clone().ok_or(Error::ExpectedKey)?;
+
+ match value.serialize(&mut serializer)? {
+ SerializedType::Simple => {
+ // Value serialized as a primitive type, so write it out
+ if !self.first {
+ write!(self.ser.buf, ", {}", serializer.buf)?;
+ } else {
+ write!(self.ser.buf, "{key} = {}", serializer.buf)?;
+ self.first = false;
+ }
+ Ok(())
+ }
+ SerializedType::Section => {
+ self.has_sections = true;
+
+ self.ser
+ .sections
+ .entry(key.clone())
+ .or_default()
+ .push(serializer.buf);
+
+ for (name, mut values) in serializer.sections {
+ self.ser
+ .sections
+ .entry(format!("{key}.{name}"))
+ .or_default()
+ .append(&mut values);
+ }
+
+ Ok(())
+ }
+ }
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ if self.has_sections {
+ Ok(Self::Ok::Section)
+ } else {
+ if !self.first {
+ self.ser.buf.push('\n');
+ }
+ Ok(Self::Ok::Simple)
+ }
+ }
+}
+
+impl ser::SerializeTuple for IniSeqSerializer<'_> {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ ser::SerializeSeq::end(self)
+ }
+}
+
+impl ser::SerializeTupleStruct for IniSeqSerializer<'_> {
+ type Ok = SerializedType;
+ type Error = Error;
+
+ fn serialize_field<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + Serialize,
+ {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ ser::SerializeSeq::end(self)
+ }
+}
+
+/// Slimmed down serializer which just supports serializing single values to the given writer and
+/// no compound values.
+///
+/// Used for serializing keys to their string representation.
+struct IniKeySerializer<'a, W: fmt::Write> {
+ /// Target to write any serialized value to.
+ writer: &'a mut W,
+}
+
+impl<'a, W: fmt::Write> IniKeySerializer<'a, W> {
+ fn new(writer: &'a mut W) -> Self {
+ Self { writer }
+ }
+}
+
+macro_rules! forward_to_writer_as_str {
+ ($name:ident($ty:ty), $($rest:tt)* ) => {
+ fn $name(self, v: $ty) -> Result<Self::Ok> {
+ self.writer.write_str(&v.to_string())?;
+ Ok(())
+ }
+
+ forward_to_writer_as_str! { $($rest)* }
+ };
+ () => {};
+}
+
+impl<'a, W: fmt::Write> ser::Serializer for IniKeySerializer<'a, W> {
+ type Ok = ();
+ type Error = Error;
+
+ type SerializeSeq = Impossible<Self::Ok, Self::Error>;
+ type SerializeTuple = Impossible<Self::Ok, Self::Error>;
+ type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
+ type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
+ type SerializeMap = Impossible<Self::Ok, Self::Error>;
+ type SerializeStruct = Impossible<Self::Ok, Self::Error>;
+ type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
+
+ forward_to_writer_as_str! {
+ serialize_bool(bool),
+ serialize_i8(i8),
+ serialize_i16(i16),
+ serialize_i32(i32),
+ serialize_i64(i64),
+ serialize_u8(u8),
+ serialize_u16(u16),
+ serialize_u32(u32),
+ serialize_u64(u64),
+ serialize_f32(f32),
+ serialize_f64(f64),
+ serialize_char(char),
+ serialize_str(&str),
+ }
+
+ fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok> {
+ Err(Error::UnsupportedType("raw bytes"))
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok> {
+ Ok(())
+ }
+
+ fn serialize_some<T>(self, v: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ v.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok> {
+ Ok(())
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
+ Ok(())
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Self::Ok> {
+ self.serialize_str(variant)
+ }
+
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _value: &T,
+ ) -> Result<Self::Ok>
+ where
+ T: ?Sized + Serialize,
+ {
+ Err(Error::UnsupportedType("nested newtype variant"))
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Err(Error::UnsupportedType("nested sequence"))
+ }
+
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
+ Err(Error::UnsupportedType("nested tuple"))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ Err(Error::UnsupportedType("nested tuple struct"))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ Err(Error::UnsupportedType("nested tuple variant"))
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Err(Error::UnsupportedType("nested maps"))
+ }
+
+ fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+ Err(Error::UnsupportedType("nested structs"))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ _variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ Err(Error::UnsupportedType("nested struct variant"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{collections::BTreeMap, ffi::CString, marker::PhantomData};
+
+ use super::{to_string, Error};
+ use serde::Serialize;
+
+ #[test]
+ fn all_supported_types() {
+ #[derive(Serialize)]
+ struct NestedStruct {
+ s: &'static str,
+ x: f64,
+ l: Vec<&'static str>,
+ c: char,
+ }
+
+ #[derive(Serialize)]
+ enum Enum {
+ A,
+ }
+
+ #[derive(Serialize)]
+ struct NewtypeStruct(u8);
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ a: u32,
+ nested: NestedStruct,
+ none: Option<i32>,
+ some: Option<i32>,
+ bytes: [u8; 3],
+ unit: (),
+ unit_struct: PhantomData<u32>,
+ unit_variant: Enum,
+ newtype_struct: NewtypeStruct,
+ list: Vec<u32>,
+ empty_list: Vec<u32>,
+ one_item_list: Vec<u32>,
+ tuple: (u32, &'static str),
+ }
+
+ let serialized = to_string(&TopLevel {
+ a: 1,
+ nested: NestedStruct {
+ s: "foo",
+ x: 123.4567,
+ l: vec!["a", "b", "c"],
+ c: 'Y',
+ },
+ none: None,
+ some: Some(42),
+ bytes: [1, 2, 3],
+ unit: (),
+ unit_struct: PhantomData,
+ unit_variant: Enum::A,
+ newtype_struct: NewtypeStruct(42),
+ list: vec![100, 200, 300],
+ empty_list: Vec::new(),
+ one_item_list: vec![42],
+ tuple: (123, "bar"),
+ })
+ .unwrap();
+
+ pretty_assertions::assert_eq!(
+ "a = 1
+some = 42
+bytes = 1, 2, 3
+unit_variant = A
+newtype_struct = 42
+list = 100, 200, 300
+one_item_list = 42
+tuple = 123, bar
+
+[nested]
+s = foo
+x = 123.4567
+l = a, b, c
+c = Y
+",
+ serialized,
+ );
+ }
+
+ #[test]
+ fn two_levels_nested() {
+ #[derive(Serialize)]
+ struct SecondLevel {
+ x: u32,
+ }
+
+ #[derive(Serialize)]
+ struct FirstLevel {
+ b: f32,
+ second_level: SecondLevel,
+ }
+
+ #[derive(Serialize)]
+ struct NestedStruct {
+ s: &'static str,
+ }
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ a: u32,
+ nested: NestedStruct,
+ first_level: FirstLevel,
+ }
+
+ let serialized = to_string(&TopLevel {
+ a: 1,
+ nested: NestedStruct { s: "foo" },
+ first_level: FirstLevel {
+ b: 12.3,
+ second_level: SecondLevel { x: 100 },
+ },
+ })
+ .unwrap();
+
+ pretty_assertions::assert_eq!(
+ "a = 1
+
+[first_level]
+b = 12.3
+
+[first_level.second_level]
+x = 100
+
+[nested]
+s = foo
+",
+ serialized,
+ );
+ }
+
+ #[test]
+ fn no_top_level_kvs() {
+ #[derive(Serialize)]
+ struct NestedStruct {
+ s: &'static str,
+ }
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ a: NestedStruct,
+ b: NestedStruct,
+ }
+
+ let serialized = to_string(&TopLevel {
+ a: NestedStruct { s: "foo" },
+ b: NestedStruct { s: "bar" },
+ })
+ .unwrap();
+
+ pretty_assertions::assert_eq!(
+ "[a]
+s = foo
+
+[b]
+s = bar
+",
+ serialized,
+ );
+ }
+
+ #[test]
+ fn unsupported_datatypes() {
+ #[derive(Serialize)]
+ enum Enum {
+ A(u32),
+ B(u32, f32),
+ C { a: u8, b: &'static str },
+ }
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ x: Enum,
+ }
+
+ #[derive(Serialize)]
+ struct RawBytes {
+ s: CString,
+ }
+
+ assert_eq!(
+ Err(Error::UnsupportedType("enum newtype variant")),
+ to_string(&TopLevel { x: Enum::A(1) }),
+ );
+
+ assert_eq!(
+ Err(Error::UnsupportedType("enum tuple variant")),
+ to_string(&TopLevel { x: Enum::B(1, 2.) }),
+ );
+
+ assert_eq!(
+ Err(Error::UnsupportedType("enum struct variant")),
+ to_string(&TopLevel {
+ x: Enum::C {
+ a: 100,
+ b: "foobar"
+ }
+ }),
+ );
+
+ assert_eq!(
+ Err(Error::UnsupportedType("raw bytes")),
+ to_string(&RawBytes {
+ s: CString::new("baz").unwrap(),
+ })
+ );
+ }
+
+ #[test]
+ fn multiple_sections_with_same_name() {
+ #[derive(Serialize)]
+ struct NestedStruct {
+ x: u32,
+ }
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ a: u32,
+ nested: Vec<NestedStruct>,
+ }
+
+ let serialized = to_string(&TopLevel {
+ a: 42,
+ nested: vec![
+ NestedStruct { x: 1 },
+ NestedStruct { x: 2 },
+ NestedStruct { x: 3 },
+ ],
+ })
+ .unwrap();
+
+ pretty_assertions::assert_eq!(
+ "a = 42
+
+[nested]
+x = 1
+
+[nested]
+x = 2
+
+[nested]
+x = 3
+",
+ serialized,
+ );
+ }
+
+ #[test]
+ fn unsupported_nested_lists() {
+ #[derive(Serialize)]
+ struct TopLevel {
+ x: Vec<Vec<u32>>,
+ }
+
+ assert_eq!(
+ Err(Error::ExpectedKey),
+ to_string(&TopLevel {
+ x: vec![vec![1, 2], vec![3, 4]],
+ }),
+ );
+ }
+
+ #[test]
+ fn empty_struct_should_produce_nothing() {
+ #[derive(Serialize)]
+ struct Empty {}
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ empty: Empty,
+ }
+
+ let serialized = to_string(&TopLevel { empty: Empty {} }).unwrap();
+ pretty_assertions::assert_eq!("", serialized);
+ }
+
+ #[test]
+ fn deeply_nested() {
+ #[derive(Serialize)]
+ struct ThirdLevel {
+ x: u32,
+ }
+
+ #[derive(Serialize)]
+ struct SecondLevel {
+ third: ThirdLevel,
+ }
+
+ #[derive(Serialize)]
+ struct FirstLevel {
+ second: SecondLevel,
+ }
+
+ #[derive(Serialize)]
+ struct TopLevel {
+ first: FirstLevel,
+ }
+
+ let serialized = to_string(&TopLevel {
+ first: FirstLevel {
+ second: SecondLevel {
+ third: ThirdLevel { x: 1 },
+ },
+ },
+ })
+ .unwrap();
+
+ pretty_assertions::assert_eq!(
+ r#"[first.second.third]
+x = 1
+"#,
+ serialized
+ );
+ }
+
+ #[test]
+ fn ints_as_keys() {
+ let mut map = BTreeMap::new();
+ map.insert(1u32, "one");
+ map.insert(2, "two");
+
+ pretty_assertions::assert_eq!(
+ r#"1 = one
+2 = two
+"#,
+ to_string(&map).unwrap()
+ );
+ }
+}
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 2/8] serde: add base64 module for byte arrays
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 1/8] ini: add crate for INI serialization Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
` (6 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
Allows to directly en-/decode [u8; N] to/from a base64 string, much like
the already existing bytes_as_base64 allows for Vec<u8>.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* no changes
proxmox-serde/src/lib.rs | 91 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/proxmox-serde/src/lib.rs b/proxmox-serde/src/lib.rs
index c16f4efb..a5aafc0d 100644
--- a/proxmox-serde/src/lib.rs
+++ b/proxmox-serde/src/lib.rs
@@ -156,3 +156,94 @@ pub mod string_as_base64 {
<T as StrAsBase64>::de::<'de, D>(deserializer)
}
}
+
+/// Serialize `[u8; N]` or `Option<[u8; N]>` as base64 encoded.
+///
+/// If you do not need the convenience of handling both [u8; N] and Option transparently, you could
+/// also use [`proxmox_base64`] directly.
+///
+/// Usage example:
+/// ```
+/// use serde::{Deserialize, Serialize};
+///
+/// #[derive(Debug, Deserialize, PartialEq, Serialize)]
+/// struct Foo {
+/// #[serde(with = "proxmox_serde::byte_array_as_base64")]
+/// data: [u8; 4],
+/// }
+///
+/// let obj = Foo { data: [1, 2, 3, 4] };
+/// let json = serde_json::to_string(&obj).unwrap();
+/// assert_eq!(json, r#"{"data":"AQIDBA=="}"#);
+///
+/// let deserialized: Foo = serde_json::from_str(&json).unwrap();
+/// assert_eq!(obj, deserialized);
+/// ```
+pub mod byte_array_as_base64 {
+ use serde::{Deserialize, Deserializer, Serializer};
+
+ /// Private trait to enable `byte_array_as_base64` for `Option<[u8; N]>` in addition to `[u8; N]`.
+ #[doc(hidden)]
+ pub trait ByteArrayAsBase64<const N: usize>: Sized {
+ fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
+ fn de<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>;
+ }
+
+ fn finish_deserializing<'de, const N: usize, D: Deserializer<'de>>(
+ string: String,
+ ) -> Result<[u8; N], D::Error> {
+ use serde::de::Error;
+
+ let vec = proxmox_base64::decode(string).map_err(|err| {
+ let msg = format!("base64 decode: {}", err);
+ Error::custom(msg)
+ })?;
+
+ vec.as_slice().try_into().map_err(|_| {
+ let msg = format!("expected {N} bytes, got {}", vec.len());
+ Error::custom(msg)
+ })
+ }
+
+ impl<const N: usize> ByteArrayAsBase64<N> for [u8; N] {
+ fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ serializer.serialize_str(&proxmox_base64::encode(self))
+ }
+
+ fn de<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ finish_deserializing::<'de, N, D>(String::deserialize(deserializer)?)
+ }
+ }
+
+ impl<const N: usize> ByteArrayAsBase64<N> for Option<[u8; N]> {
+ fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ match self {
+ Some(s) => Self::ser(&Some(*s), serializer),
+ None => serializer.serialize_none(),
+ }
+ }
+
+ fn de<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ match Option::<String>::deserialize(deserializer)? {
+ Some(s) => Ok(Some(finish_deserializing::<'de, N, D>(s)?)),
+ None => Ok(None),
+ }
+ }
+ }
+
+ pub fn serialize<const N: usize, S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ T: ByteArrayAsBase64<N>,
+ {
+ <T as ByteArrayAsBase64<N>>::ser(data, serializer)
+ }
+
+ pub fn deserialize<'de, const N: usize, D, T>(deserializer: D) -> Result<T, D::Error>
+ where
+ D: Deserializer<'de>,
+ T: ByteArrayAsBase64<N>,
+ {
+ <T as ByteArrayAsBase64<N>>::de::<'de, D>(deserializer)
+ }
+}
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 1/8] ini: add crate for INI serialization Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 2/8] serde: add base64 module for byte arrays Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 4/8] schema: provide integer schema for node ports Christoph Heiss
` (5 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
Basically a composite data type to provide bit of an better abstraction
to a tuple (host, port).
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* forward to `Display` impl directly where possible
proxmox-network-types/src/endpoint.rs | 154 ++++++++++++++++++++++++++
proxmox-network-types/src/lib.rs | 1 +
2 files changed, 155 insertions(+)
create mode 100644 proxmox-network-types/src/endpoint.rs
diff --git a/proxmox-network-types/src/endpoint.rs b/proxmox-network-types/src/endpoint.rs
new file mode 100644
index 00000000..10f6a813
--- /dev/null
+++ b/proxmox-network-types/src/endpoint.rs
@@ -0,0 +1,154 @@
+//! Implements a wrapper around a (host, port) tuple, where host can either
+//! be a plain IP address or a resolvable hostname.
+
+use std::{
+ fmt::{self, Display},
+ net::IpAddr,
+ str::FromStr,
+};
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+/// Represents either a resolvable hostname or an IPv4/IPv6 address.
+/// IPv6 address are correctly bracketed on [`Display`], and parsing
+/// automatically tries parsing it as an IP address first, falling back to a
+/// plain hostname in the other case.
+#[derive(Clone, Debug, PartialEq)]
+pub enum HostnameOrIpAddr {
+ Hostname(String),
+ IpAddr(IpAddr),
+}
+
+impl Display for HostnameOrIpAddr {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ HostnameOrIpAddr::Hostname(s) => Display::fmt(s, f),
+ HostnameOrIpAddr::IpAddr(addr) => match addr {
+ IpAddr::V4(v4) => Display::fmt(v4, f),
+ IpAddr::V6(v6) => write!(f, "[{v6}]"),
+ },
+ }
+ }
+}
+
+impl<S: Into<String>> From<S> for HostnameOrIpAddr {
+ fn from(value: S) -> Self {
+ let s = value.into();
+ if let Ok(ip_addr) = IpAddr::from_str(&s) {
+ Self::IpAddr(ip_addr)
+ } else {
+ Self::Hostname(s)
+ }
+ }
+}
+
+/// Represents a (host, port) tuple, where the host can either be a resolvable
+/// hostname or an IPv4/IPv6 address.
+#[derive(Clone, Debug, PartialEq, SerializeDisplay, DeserializeFromStr)]
+pub struct ServiceEndpoint {
+ host: HostnameOrIpAddr,
+ port: u16,
+}
+
+impl ServiceEndpoint {
+ pub fn new<S: Into<String>>(host: S, port: u16) -> Self {
+ let s = host.into();
+ Self {
+ host: s.into(),
+ port,
+ }
+ }
+}
+
+impl Display for ServiceEndpoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}:{}", self.host, self.port)
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum ParseError {
+ #[error("host and port must be separated by a colon")]
+ MissingSeparator,
+ #[error("host part missing")]
+ MissingHost,
+ #[error("invalid port: {0}")]
+ InvalidPort(String),
+}
+
+impl FromStr for ServiceEndpoint {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (mut host, port) = s.rsplit_once(':').ok_or(Self::Err::MissingSeparator)?;
+
+ if host.is_empty() {
+ return Err(Self::Err::MissingHost);
+ }
+
+ // [ and ] are not valid characters in a hostname, so strip them in case it
+ // is a IPv6 address.
+ host = host.trim_matches(['[', ']']);
+
+ Ok(ServiceEndpoint {
+ host: host.into(),
+ port: port
+ .parse()
+ .map_err(|err: std::num::ParseIntError| Self::Err::InvalidPort(err.to_string()))?,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::endpoint::HostnameOrIpAddr;
+
+ use super::ServiceEndpoint;
+
+ #[test]
+ fn display_works() {
+ let s = ServiceEndpoint::new("127.0.0.1", 123);
+ assert_eq!(s.to_string(), "127.0.0.1:123");
+
+ let s = ServiceEndpoint::new("fc00:f00d::4321", 123);
+ assert_eq!(s.to_string(), "[fc00:f00d::4321]:123");
+
+ let s = ServiceEndpoint::new("::", 123);
+ assert_eq!(s.to_string(), "[::]:123");
+
+ let s = ServiceEndpoint::new("fc00::", 123);
+ assert_eq!(s.to_string(), "[fc00::]:123");
+
+ let s = ServiceEndpoint::new("example.com", 123);
+ assert_eq!(s.to_string(), "example.com:123");
+ }
+
+ #[test]
+ fn fromstr_works() {
+ assert_eq!(
+ "127.0.0.1:123".parse::<ServiceEndpoint>().unwrap(),
+ ServiceEndpoint {
+ host: HostnameOrIpAddr::IpAddr([127, 0, 0, 1].into()),
+ port: 123
+ }
+ );
+
+ assert_eq!(
+ "[::1]:123".parse::<ServiceEndpoint>().unwrap(),
+ ServiceEndpoint {
+ host: HostnameOrIpAddr::IpAddr(
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1].into()
+ ),
+ port: 123
+ }
+ );
+
+ assert_eq!(
+ "example.com:123".parse::<ServiceEndpoint>().unwrap(),
+ ServiceEndpoint {
+ host: HostnameOrIpAddr::Hostname("example.com".to_owned()),
+ port: 123
+ }
+ );
+ }
+}
diff --git a/proxmox-network-types/src/lib.rs b/proxmox-network-types/src/lib.rs
index 058817b5..3b17488b 100644
--- a/proxmox-network-types/src/lib.rs
+++ b/proxmox-network-types/src/lib.rs
@@ -1,6 +1,7 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![deny(unsafe_op_in_unsafe_fn)]
+pub mod endpoint;
pub mod ip_address;
pub use ip_address::*;
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 4/8] schema: provide integer schema for node ports
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (2 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-31 22:55 ` Thomas Lamprecht
2026-03-30 18:28 ` [PATCH proxmox v3 5/8] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
` (4 subsequent siblings)
8 siblings, 1 reply; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* no changes
proxmox-schema/src/api_types.rs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
index cfad4a10..3e31d97e 100644
--- a/proxmox-schema/src/api_types.rs
+++ b/proxmox-schema/src/api_types.rs
@@ -1,7 +1,7 @@
//! The "basic" api types we generally require along with some of their macros.
use const_format::concatcp;
-use crate::{ApiStringFormat, ArraySchema, Schema, StringSchema};
+use crate::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
#[rustfmt::skip]
const IPV4OCTET: &str = r"(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
@@ -221,6 +221,11 @@ pub const HOST_PORT_SCHEMA: Schema =
.format(&HOST_PORT_FORMAT)
.schema();
+pub const PORT_SCHEMA: Schema = IntegerSchema::new("Node port")
+ .minimum(1)
+ .maximum(65535)
+ .schema();
+
pub const HTTP_URL_SCHEMA: Schema = StringSchema::new("HTTP(s) url with optional port.")
.format(&HTTP_URL_FORMAT)
.schema();
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 5/8] schema: api-types: add ed25519 base64 encoded key schema
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (3 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 4/8] schema: provide integer schema for node ports Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 6/8] wireguard: init configuration support crate Christoph Heiss
` (3 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* no changes
proxmox-schema/src/api_types.rs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
index 3e31d97e..d6a0608c 100644
--- a/proxmox-schema/src/api_types.rs
+++ b/proxmox-schema/src/api_types.rs
@@ -122,6 +122,11 @@ const_regex! {
pub BLOCKDEVICE_NAME_REGEX = r"^(?:(?:h|s|x?v)d[a-z]+)|(?:nvme\d+n\d+)$";
pub BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX = r"^(?:(?:h|s|x?v)d[a-z]+\d*)|(?:nvme\d+n\d+(p\d+)?)$";
+
+ /// Regex to match a base64-encoded ED25519 key.
+ /// A ED25519 key has always 32 bytes of raw key material, base64 needs 4 * (n / 3) characters
+ /// to represent n bytes -> 4 * (32 / 3) = 42.6.., thus 43 + 1 padding character.
+ pub ED25519_BASE64_KEY_REGEX =r"^[a-zA-Z0-9+/-]{43}=";
}
pub const SAFE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SAFE_ID_REGEX);
@@ -289,4 +294,11 @@ fn test_regexes() {
assert!(IP_BRACKET_REGEX.is_match("[2014:b3a::27]"));
assert!(IP_BRACKET_REGEX.is_match("[2014:b3a::192.168.0.1]"));
assert!(IP_BRACKET_REGEX.is_match("[2014:b3a:0102:adf1:1234:4321:4afA:BCDF]"));
+
+ assert!(ED25519_BASE64_KEY_REGEX.is_match("KNpc7alqlLTaWE6RzuzHGioKs7Nqh/z3YxMJojpSelA="));
+ assert!(!ED25519_BASE64_KEY_REGEX.is_match(""));
+ // 31 bytes of data
+ assert!(!ED25519_BASE64_KEY_REGEX.is_match("6zroXbjGs9sdOpr1n/M5hh+UklBxtQ90tGQDnYzJfw=="));
+ // 33 bytes of data
+ assert!(!ED25519_BASE64_KEY_REGEX.is_match("IiC3Nkh4Fn2ukUZUNmdK5K5CWO53Zmk/eGlKO4m6aCD/"));
}
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 6/8] wireguard: init configuration support crate
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (4 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 5/8] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 7/8] wireguard: implement api for PublicKey Christoph Heiss
` (2 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: 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 <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* adapt to INI serializer now living in the `proxmox-ini` crate
* fix wrong vcs url in debcargo.toml
Changes v1 -> v2:
* implement `AsRef<{Private,Preshared}Key>` instead of .raw() method
* scope key generation (and thus proxmox-sys usage) behind a feature
flag, makes it possible to use this crate in e.g. wasm context
* expand test cases a bit
Cargo.toml | 2 +
proxmox-wireguard/Cargo.toml | 26 ++
proxmox-wireguard/debian/changelog | 5 +
proxmox-wireguard/debian/control | 67 +++++
proxmox-wireguard/debian/copyright | 18 ++
proxmox-wireguard/debian/debcargo.toml | 7 +
proxmox-wireguard/src/lib.rs | 391 +++++++++++++++++++++++++
7 files changed, 516 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 4617bbab..735b31e6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ members = [
"proxmox-time-api",
"proxmox-upgrade-checks",
"proxmox-uuid",
+ "proxmox-wireguard",
"proxmox-worker-task",
"pbs-api-types",
"pve-api-types",
@@ -161,6 +162,7 @@ proxmox-fixed-string = { version = "0.1.0", path = "proxmox-fixed-string" }
proxmox-http = { version = "1.0.5", path = "proxmox-http" }
proxmox-http-error = { version = "1.0.0", path = "proxmox-http-error" }
proxmox-human-byte = { version = "1.0.0", path = "proxmox-human-byte" }
+proxmox-ini = { version = "0.1.0", path = "proxmox-ini" }
proxmox-io = { version = "1.2.1", path = "proxmox-io" }
proxmox-lang = { version = "1.5", path = "proxmox-lang" }
proxmox-log = { version = "1.0.0", path = "proxmox-log" }
diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
new file mode 100644
index 00000000..f11346d6
--- /dev/null
+++ b/proxmox-wireguard/Cargo.toml
@@ -0,0 +1,26 @@
+[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-ini.workspace = true
+proxmox-network-types.workspace = true
+proxmox-sys = { workspace = true, optional = true }
+proxmox-serde.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
+
+[features]
+default = ["key-generation"]
+key-generation = ["dep:proxmox-sys"]
diff --git a/proxmox-wireguard/debian/changelog b/proxmox-wireguard/debian/changelog
new file mode 100644
index 00000000..67500279
--- /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> Fri, 13 Feb 2026 15:10:12 +0200
diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control
new file mode 100644
index 00000000..1e3392ef
--- /dev/null
+++ b/proxmox-wireguard/debian/control
@@ -0,0 +1,67 @@
+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.85) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-ed25519-dalek-2+default-dev (>= 2.1-~~) <!nocheck>,
+ librust-proxmox-ini-0.1+default-dev <!nocheck>,
+ librust-proxmox-network-types-1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+serde-json-dev <!nocheck>,
+ librust-proxmox-sys-1+default-dev (>= 1.0.1-~~) <!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.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.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-ini-0.1+default-dev,
+ librust-proxmox-network-types-1+default-dev,
+ librust-proxmox-serde-1+default-dev,
+ librust-proxmox-serde-1+serde-json-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-thiserror-2+default-dev
+Recommends:
+ librust-proxmox-wireguard+key-generation-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-wireguard-0-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0-dev (= ${binary:Version})
+Description: WireGuard configuration support - Rust source code
+ Source code for Debianized Rust crate "proxmox-wireguard"
+
+Package: librust-proxmox-wireguard+key-generation-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-wireguard-dev (= ${binary:Version}),
+ librust-proxmox-sys-1+default-dev (>= 1.0.1-~~)
+Provides:
+ librust-proxmox-wireguard+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0+key-generation-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+key-generation-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0+key-generation-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0+default-dev (= ${binary:Version})
+Description: WireGuard configuration support - feature "key-generation" and 1 more
+ This metapackage enables feature "key-generation" for the Rust proxmox-
+ wireguard crate, by pulling in any additional dependencies needed by that
+ feature.
+ .
+ Additionally, this package also provides the "default" feature.
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..b7864cdb
--- /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.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
new file mode 100644
index 00000000..1712b834
--- /dev/null
+++ b/proxmox-wireguard/src/lib.rs
@@ -0,0 +1,391 @@
+//! 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_ini::Error> for Error {
+ fn from(err: proxmox_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.
+ #[cfg(feature = "key-generation")]
+ 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(),
+ )
+ }
+
+ /// 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)
+ }
+}
+
+impl AsRef<ed25519_dalek::SecretKey> for PrivateKey {
+ /// Returns the raw private key material.
+ fn as_ref(&self) -> &ed25519_dalek::SecretKey {
+ &self.0
+ }
+}
+
+/// 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.
+ #[cfg(feature = "key-generation")]
+ pub fn generate() -> Result<Self, Error> {
+ generate_key().map(Self)
+ }
+
+ /// 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 AsRef<ed25519_dalek::SecretKey> for PresharedKey {
+ /// Returns the raw preshared key material.
+ fn as_ref(&self) -> &ed25519_dalek::SecretKey {
+ &self.0
+ }
+}
+
+/// 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", skip_serializing_if = "Vec::is_empty")]
+ 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_ini::to_string(&self)?)
+ }
+}
+
+/// Generates a new ED25519 private key.
+#[cfg(feature = "key-generation")]
+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,
+ },
+ WireGuardPeer {
+ public_key: mock_private_key(4).public_key(),
+ preshared_key: mock_preshared_key(4),
+ allowed_ips: vec![],
+ endpoint: Some("10.0.0.1:51820".parse().unwrap()),
+ persistent_keepalive: Some(25),
+ },
+ ],
+ };
+
+ 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
+
+[Peer]
+PublicKey = zRSzf5VulTGU/3+3Oz2B3MVh1hp1OAlLfD4aZD7l86o=
+PresharedKey = gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8=
+Endpoint = 10.0.0.1:51820
+PersistentKeepalive = 25
+"
+ );
+ }
+
+ #[test]
+ fn non_listening_peer() {
+ let config = WireGuardConfig {
+ interface: WireGuardInterface {
+ private_key: mock_private_key(0),
+ listen_port: None,
+ 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("10.0.0.1:51820".parse().unwrap()),
+ persistent_keepalive: Some(25),
+ }],
+ };
+
+ pretty_assertions::assert_eq!(
+ config.to_raw_config().unwrap(),
+ "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+
+[Peer]
+PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+AllowedIPs = 192.168.0.0/24
+Endpoint = 10.0.0.1:51820
+PersistentKeepalive = 25
+"
+ );
+ }
+
+ #[test]
+ fn empty_peers() {
+ let config = WireGuardConfig {
+ interface: WireGuardInterface {
+ private_key: mock_private_key(0),
+ listen_port: Some(51830),
+ fw_mark: Some(240),
+ },
+ peers: vec![],
+ };
+
+ pretty_assertions::assert_eq!(
+ config.to_raw_config().unwrap(),
+ "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+ListenPort = 51830
+FwMark = 240
+"
+ );
+ }
+}
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 7/8] wireguard: implement api for PublicKey
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (5 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 6/8] wireguard: init configuration support crate Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 8/8] wireguard: make per-peer preshared key optional Christoph Heiss
2026-03-31 23:10 ` applied: [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Thomas Lamprecht
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
From: Stefan Hanreich <s.hanreich@proxmox.com>
.. such that it can be used in API definitions.
Authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* update d/control
Changes v1 -> v2:
* improve schema description
proxmox-wireguard/Cargo.toml | 3 +++
proxmox-wireguard/debian/control | 19 +++++++++++++++++++
proxmox-wireguard/src/lib.rs | 17 +++++++++++++++++
3 files changed, 39 insertions(+)
diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
index f11346d6..b1abae3d 100644
--- a/proxmox-wireguard/Cargo.toml
+++ b/proxmox-wireguard/Cargo.toml
@@ -13,14 +13,17 @@ rust-version.workspace = true
ed25519-dalek = "2.1"
serde = { workspace = true, features = [ "derive" ] }
thiserror.workspace = true
+proxmox-schema = { workspace = true, optional = true, features = ["api-types"] }
proxmox-ini.workspace = true
proxmox-network-types.workspace = true
proxmox-sys = { workspace = true, optional = true }
proxmox-serde.workspace = true
+regex = { workspace = true, optional = true }
[dev-dependencies]
pretty_assertions.workspace = true
[features]
default = ["key-generation"]
+api-types = ["dep:proxmox-schema", "dep:regex"]
key-generation = ["dep:proxmox-sys"]
diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control
index 1e3392ef..6b5552ff 100644
--- a/proxmox-wireguard/debian/control
+++ b/proxmox-wireguard/debian/control
@@ -37,6 +37,8 @@ Depends:
librust-thiserror-2+default-dev
Recommends:
librust-proxmox-wireguard+key-generation-dev (= ${binary:Version})
+Suggests:
+ librust-proxmox-wireguard+api-types-dev (= ${binary:Version})
Provides:
librust-proxmox-wireguard-0-dev (= ${binary:Version}),
librust-proxmox-wireguard-0.1-dev (= ${binary:Version}),
@@ -44,6 +46,23 @@ Provides:
Description: WireGuard configuration support - Rust source code
Source code for Debianized Rust crate "proxmox-wireguard"
+Package: librust-proxmox-wireguard+api-types-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-wireguard-dev (= ${binary:Version}),
+ librust-proxmox-schema-5+api-types-dev (>= 5.1.0-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.1.0-~~),
+ librust-regex-1+default-dev (>= 1.5-~~)
+Provides:
+ librust-proxmox-wireguard-0+api-types-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+api-types-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0+api-types-dev (= ${binary:Version})
+Description: WireGuard configuration support - feature "api-types"
+ This metapackage enables feature "api-types" for the Rust proxmox-wireguard
+ crate, by pulling in any additional dependencies needed by that feature.
+
Package: librust-proxmox-wireguard+key-generation-dev
Architecture: any
Multi-Arch: same
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 1712b834..646ed750 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -17,6 +17,10 @@ use serde::{Deserialize, Serialize};
use std::fmt;
use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr};
+#[cfg(feature = "api-types")]
+use proxmox_schema::{
+ api_types::ED25519_BASE64_KEY_REGEX, ApiStringFormat, ApiType, StringSchema, UpdaterType,
+};
/// Possible error when handling WireGuard configurations.
#[derive(thiserror::Error, Debug, PartialEq, Clone)]
@@ -42,6 +46,19 @@ pub struct PublicKey(
#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
);
+#[cfg(feature = "api-types")]
+impl ApiType for PublicKey {
+ const API_SCHEMA: proxmox_schema::Schema =
+ StringSchema::new("ED25519 public key (base64 encoded)")
+ .format(&ApiStringFormat::Pattern(&ED25519_BASE64_KEY_REGEX))
+ .schema();
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for PublicKey {
+ type Updater = Option<PublicKey>;
+}
+
/// Private key of a WireGuard peer.
#[derive(Serialize)]
#[serde(transparent)]
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH proxmox v3 8/8] wireguard: make per-peer preshared key optional
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (6 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 7/8] wireguard: implement api for PublicKey Christoph Heiss
@ 2026-03-30 18:28 ` Christoph Heiss
2026-03-31 23:10 ` applied: [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Thomas Lamprecht
8 siblings, 0 replies; 11+ messages in thread
From: Christoph Heiss @ 2026-03-30 18:28 UTC (permalink / raw)
To: pve-devel
Authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* no changes
Changes v1 -> v2:
* no changes
proxmox-wireguard/src/lib.rs | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 646ed750..08579775 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -159,7 +159,7 @@ pub struct WireGuardPeer {
/// 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,
+ pub preshared_key: Option<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.
@@ -257,7 +257,7 @@ mod tests {
},
peers: vec![WireGuardPeer {
public_key: mock_private_key(1).public_key(),
- preshared_key: mock_preshared_key(1),
+ preshared_key: Some(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),
@@ -292,28 +292,28 @@ PersistentKeepalive = 25
peers: vec![
WireGuardPeer {
public_key: mock_private_key(1).public_key(),
- preshared_key: mock_preshared_key(1),
+ preshared_key: Some(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),
+ preshared_key: Some(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),
+ preshared_key: Some(mock_preshared_key(3)),
allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 2, 0), 24).unwrap()],
endpoint: None,
persistent_keepalive: None,
},
WireGuardPeer {
public_key: mock_private_key(4).public_key(),
- preshared_key: mock_preshared_key(4),
+ preshared_key: Some(mock_preshared_key(4)),
allowed_ips: vec![],
endpoint: Some("10.0.0.1:51820".parse().unwrap()),
persistent_keepalive: Some(25),
@@ -363,7 +363,7 @@ PersistentKeepalive = 25
},
peers: vec![WireGuardPeer {
public_key: mock_private_key(1).public_key(),
- preshared_key: mock_preshared_key(1),
+ preshared_key: Some(mock_preshared_key(1)),
allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()],
endpoint: Some("10.0.0.1:51820".parse().unwrap()),
persistent_keepalive: Some(25),
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH proxmox v3 4/8] schema: provide integer schema for node ports
2026-03-30 18:28 ` [PATCH proxmox v3 4/8] schema: provide integer schema for node ports Christoph Heiss
@ 2026-03-31 22:55 ` Thomas Lamprecht
0 siblings, 0 replies; 11+ messages in thread
From: Thomas Lamprecht @ 2026-03-31 22:55 UTC (permalink / raw)
To: Christoph Heiss, pve-devel
Am 30.03.26 um 20:28 schrieb Christoph Heiss:
> Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
> ---
> Changes v2 -> v3:
> * no changes
>
> Changes v1 -> v2:
> * no changes
>
> proxmox-schema/src/api_types.rs | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
> index cfad4a10..3e31d97e 100644
> --- a/proxmox-schema/src/api_types.rs
> +++ b/proxmox-schema/src/api_types.rs
> @@ -1,7 +1,7 @@
> //! The "basic" api types we generally require along with some of their macros.
> use const_format::concatcp;
>
> -use crate::{ApiStringFormat, ArraySchema, Schema, StringSchema};
> +use crate::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
>
> #[rustfmt::skip]
> const IPV4OCTET: &str = r"(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
> @@ -221,6 +221,11 @@ pub const HOST_PORT_SCHEMA: Schema =
> .format(&HOST_PORT_FORMAT)
> .schema();
>
> +pub const PORT_SCHEMA: Schema = IntegerSchema::new("Node port")
nit: Description is a bit odd for a generic port? Could be just "Port" or
"Network port" if we want to have some correlation with network?
> + .minimum(1)
> + .maximum(65535)
> + .schema();
> +
> pub const HTTP_URL_SCHEMA: Schema = StringSchema::new("HTTP(s) url with optional port.")
> .format(&HTTP_URL_FORMAT)
> .schema();
^ permalink raw reply [flat|nested] 11+ messages in thread
* applied: [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
` (7 preceding siblings ...)
2026-03-30 18:28 ` [PATCH proxmox v3 8/8] wireguard: make per-peer preshared key optional Christoph Heiss
@ 2026-03-31 23:10 ` Thomas Lamprecht
8 siblings, 0 replies; 11+ messages in thread
From: Thomas Lamprecht @ 2026-03-31 23:10 UTC (permalink / raw)
To: pve-devel, Christoph Heiss
On Mon, 30 Mar 2026 20:28:34 +0200, Christoph Heiss wrote:
> This series lays the groundwork with initial primitives and configuration
> support for adding WireGuard as a new SDN fabric to our stack in the
> future.
>
> Nothing of this code is actively used anywhere in the stack yet, but
> Stefan already sent out a series adding WireGuard as a new SDN fabric,
> that depends on this series [0].
>
> [...]
Applied, thanks!
[1/8] ini: add crate for INI serialization
commit: 33e23ce3cfdba87a565ce7842ea1e71a6c414b08
[2/8] serde: add base64 module for byte arrays
commit: 9bbe4c660b3c2d86e21e0af33b01abbc98827c4a
[3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction
commit: 826736e28d6a4a7b01f94d87abe4e1c990d9ec11
[4/8] schema: provide integer schema for node ports
commit: 92f4d4a61d1fdafc89a13f22f130a2b5456aa2c5
[5/8] schema: api-types: add ed25519 base64 encoded key schema
commit: cd7125b45124da9431daf4fc71bb0ad23db183df
[6/8] wireguard: init configuration support crate
commit: 4fcb55314b4fbaf19d0523c54fc0cbbb916757fd
[7/8] wireguard: implement api for PublicKey
commit: 89c12bb333f78f453ea412ec2ea2b5a8e00a6d45
[8/8] wireguard: make per-peer preshared key optional
commit: de014abab9f4effcca47aa6aa6d7f30e867bd6ef
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-03-31 23:09 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-30 18:28 [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 1/8] ini: add crate for INI serialization Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 2/8] serde: add base64 module for byte arrays Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 3/8] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 4/8] schema: provide integer schema for node ports Christoph Heiss
2026-03-31 22:55 ` Thomas Lamprecht
2026-03-30 18:28 ` [PATCH proxmox v3 5/8] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 6/8] wireguard: init configuration support crate Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 7/8] wireguard: implement api for PublicKey Christoph Heiss
2026-03-30 18:28 ` [PATCH proxmox v3 8/8] wireguard: make per-peer preshared key optional Christoph Heiss
2026-03-31 23:10 ` applied: [PATCH proxmox v3 0/8] sdn: add wireguard fabric configuration support Thomas Lamprecht
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox