public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal