all lists on 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal