public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support
@ 2026-01-16 15:33 Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer Christoph Heiss
                   ` (10 more replies)
  0 siblings, 11 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 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. I'm
sending it early to a) make it more reviewable as a smaller series and
b) potentially get in some prepratory work early.

Stefan already has a working prototype for adding WireGuard as a new
SDN fabric type, built on top of this.

One of the bigger topics next up will be wiring up actual section config
support for the fabric, which involves some challenges around proper
key handling.

Diffstat
========

proxmox:

Christoph Heiss (6):
  serde: implement ini serializer
  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                             |   2 +
 proxmox-network-types/src/endpoint.rs  | 154 +++++
 proxmox-network-types/src/lib.rs       |   3 +-
 proxmox-schema/src/api_types.rs        |  19 +-
 proxmox-serde/Cargo.toml               |   2 +
 proxmox-serde/debian/control           |   4 +
 proxmox-serde/src/ini.rs               | 901 +++++++++++++++++++++++++
 proxmox-serde/src/lib.rs               |  94 +++
 proxmox-wireguard/Cargo.toml           |  27 +
 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           | 337 +++++++++
 14 files changed, 1638 insertions(+), 2 deletions(-)
 create mode 100644 proxmox-network-types/src/endpoint.rs
 create mode 100644 proxmox-serde/src/ini.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

proxmox-ve-rs:

Christoph Heiss (3):
  sdn-types: add wireguard-specific PersistentKeepalive api type
  ve-config: fabric: refactor fabric config entry impl using macro
  ve-config: sdn: fabrics: add wireguard section config types

 Cargo.toml                                    |   1 +
 proxmox-sdn-types/src/lib.rs                  |   1 +
 proxmox-sdn-types/src/wireguard.rs            |  43 ++++
 proxmox-ve-config/Cargo.toml                  |   2 +
 proxmox-ve-config/debian/control              |  18 +-
 proxmox-ve-config/src/sdn/fabric/frr.rs       |   1 +
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 199 +++++++++++++-----
 .../src/sdn/fabric/section_config/fabric.rs   |  23 ++
 .../src/sdn/fabric/section_config/mod.rs      |  19 ++
 .../src/sdn/fabric/section_config/node.rs     |  33 ++-
 .../sdn/fabric/section_config/protocol/mod.rs |   1 +
 .../section_config/protocol/wireguard.rs      | 162 ++++++++++++++
 12 files changed, 442 insertions(+), 61 deletions(-)
 create mode 100644 proxmox-sdn-types/src/wireguard.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs

-- 
2.52.0


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays Christoph Heiss
                   ` (9 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

The official WireGuard tooling wg(8) uses a (mostly) INI-like format
for consuming configuration.

E.g. `wg syncconf` will be used by in the future by the WireGuard fabric
for applying changes to a particular WireGuard interface.

One of the quirks of the INI format used by wg(8) are that there can be
multiple sections with the same name, which is also explicitly supported
by this serializer.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Cargo.toml                   |   1 +
 proxmox-serde/Cargo.toml     |   2 +
 proxmox-serde/debian/control |   4 +
 proxmox-serde/src/ini.rs     | 901 +++++++++++++++++++++++++++++++++++
 proxmox-serde/src/lib.rs     |   3 +
 5 files changed, 911 insertions(+)
 create mode 100644 proxmox-serde/src/ini.rs

diff --git a/Cargo.toml b/Cargo.toml
index 27a69afa..3cdad8d8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -114,6 +114,7 @@ openssl = "0.10"
 pam-sys = "0.5"
 percent-encoding = "2.1"
 pin-utils = "0.1.0"
+pretty_assertions = "1.4.1"
 proc-macro2 = "1.0"
 quick-xml = "0.36.1"
 quote = "1.0"
diff --git a/proxmox-serde/Cargo.toml b/proxmox-serde/Cargo.toml
index 78d733d0..d4f9fe43 100644
--- a/proxmox-serde/Cargo.toml
+++ b/proxmox-serde/Cargo.toml
@@ -20,6 +20,8 @@ proxmox-time.workspace = true
 
 [dev-dependencies]
 serde_json.workspace = true
+pretty_assertions.workspace = true
 
 [features]
 perl = []
+ini-ser = []
diff --git a/proxmox-serde/debian/control b/proxmox-serde/debian/control
index 62a5033a..d956d54b 100644
--- a/proxmox-serde/debian/control
+++ b/proxmox-serde/debian/control
@@ -36,15 +36,19 @@ Suggests:
  librust-proxmox-serde+serde-json-dev (= ${binary:Version})
 Provides:
  librust-proxmox-serde+default-dev (= ${binary:Version}),
+ librust-proxmox-serde+ini-ser-dev (= ${binary:Version}),
  librust-proxmox-serde+perl-dev (= ${binary:Version}),
  librust-proxmox-serde-1-dev (= ${binary:Version}),
  librust-proxmox-serde-1+default-dev (= ${binary:Version}),
+ librust-proxmox-serde-1+ini-ser-dev (= ${binary:Version}),
  librust-proxmox-serde-1+perl-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-serde-1.0+ini-ser-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0+perl-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0.1-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-serde-1.0.1+ini-ser-dev (= ${binary:Version}),
  librust-proxmox-serde-1.0.1+perl-dev (= ${binary:Version})
 Description: Serde formatting tools - Rust source code
  Source code for Debianized Rust crate "proxmox-serde"
diff --git a/proxmox-serde/src/ini.rs b/proxmox-serde/src/ini.rs
new file mode 100644
index 00000000..3141e6df
--- /dev/null
+++ b/proxmox-serde/src/ini.rs
@@ -0,0 +1,901 @@
+//! 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 and struct variants are not supported.
+
+use std::{
+    collections::BTreeMap,
+    fmt::{self, Display, Write},
+    io,
+};
+
+use serde::{
+    de,
+    ser::{self, Impossible, Serialize},
+};
+
+/// Errors that can occur during INI serialization.
+#[derive(Debug, PartialEq)]
+pub enum Error {
+    Message(String),
+    UnsupportedType(&'static str),
+    ExpectedKey,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Error::Message(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>(msg: T) -> Self {
+        Error::Message(msg.to_string())
+    }
+}
+
+impl de::Error for Error {
+    fn custom<T: fmt::Display>(msg: T) -> Self {
+        Error::Message(msg.to_string())
+    }
+}
+
+impl From<io::Error> for Error {
+    fn from(err: io::Error) -> Self {
+        Self::Message(err.to_string())
+    }
+}
+
+impl From<fmt::Error> for Error {
+    fn from(err: fmt::Error) -> Self {
+        Self::Message(err.to_string())
+    }
+}
+
+/// Return type used throughout the serializer.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// Implements a serde serializer for transforming Rust values into the INI
+/// format.
+struct IniSerializer {
+    /// Last key observed during serialization
+    last_key: Option<String>,
+    /// Key-value pairs on this level of the serialization tree
+    kvs: String,
+    /// Nested sections under this part of the tree. Multiple sections with the
+    /// same name are allowed.
+    sections: BTreeMap<String, Vec<IniSerializer>>,
+}
+
+impl IniSerializer {
+    /// Creates a new INI serializer.
+    fn new() -> Self {
+        IniSerializer {
+            last_key: None,
+            kvs: 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<()> {
+        write!(w, "{}", self.kvs)?;
+
+        let mut is_first_byte = self.kvs.is_empty();
+        self.sections.iter().try_for_each(|(name, sections)| {
+            Self::write_nested(&mut w, name, sections, &mut is_first_byte)
+        })
+    }
+
+    /// Internal, recursive method for writing out serialized INI to a target implementing [`io::Write`].
+    fn write_nested<W: io::Write>(
+        w: &mut W,
+        name: &str,
+        sections: &[IniSerializer],
+        is_first_byte: &mut bool,
+    ) -> Result<()> {
+        for section in sections {
+            if !section.kvs.is_empty() {
+                if !*is_first_byte {
+                    writeln!(w)?;
+                }
+
+                write!(w, "[{name}]\n{}", section.kvs)?;
+                *is_first_byte = false;
+            }
+
+            section.sections.iter().try_for_each(|(secname, secs)| {
+                Self::write_nested(w, &format!("{name}.{secname}"), secs, is_first_byte)
+            })?;
+        }
+
+        Ok(())
+    }
+
+    /// Serializes a single key-value pair, expecting some previously picked up key.
+    fn serialize_value_as_string<T: Display>(&mut self, v: T) -> Result<()> {
+        let key = self.last_key.take().ok_or(Error::ExpectedKey)?;
+        writeln!(&mut self.kvs, "{key} = {v}")?;
+        Ok(())
+    }
+}
+
+/// 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)?;
+
+    String::from_utf8(buf).map_err(|err| Error::Message(err.to_string()))
+}
+
+impl<'a> ser::Serializer for &'a mut IniSerializer {
+    type Ok = ();
+    type Error = Error;
+
+    type SerializeSeq = IniSeqSerializer<'a>;
+    type SerializeTuple = IniSeqSerializer<'a>;
+    type SerializeTupleStruct = IniSeqSerializer<'a>;
+    type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
+    type SerializeMap = Self;
+    type SerializeStruct = Self;
+    type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
+
+    fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_char(self, v: char) -> Result<Self::Ok> {
+        self.serialize_value_as_string(v)
+    }
+
+    fn serialize_str(self, v: &str) -> Result<Self::Ok> {
+        if self.last_key.is_none() {
+            self.last_key = Some(v.to_owned());
+            Ok(())
+        } else {
+            self.serialize_value_as_string(v)
+        }
+    }
+
+    fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok> {
+        Err(Error::UnsupportedType("raw bytes"))
+    }
+
+    fn serialize_none(self) -> Result<Self::Ok> {
+        self.serialize_value_as_string("")
+    }
+
+    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.last_key = None;
+        Ok(())
+    }
+
+    fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
+        self.last_key = None;
+        self.serialize_unit()
+    }
+
+    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)
+    }
+
+    // Serializes as externally tagged representation.
+    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.last_key.take(), 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))
+    }
+
+    // Serializes as externally tagged representation.
+    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(self)
+    }
+
+    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"))
+    }
+}
+
+impl ser::SerializeMap for &'_ mut IniSerializer {
+    type Ok = ();
+    type Error = Error;
+
+    fn serialize_key<T>(&mut self, key: &T) -> Result<()>
+    where
+        T: ?Sized + Serialize,
+    {
+        let mut buf = String::new();
+        self.last_key = None;
+        key.serialize(&mut IniValueSerializer::new(&mut buf, self))?;
+        self.last_key = Some(buf);
+
+        Ok(())
+    }
+
+    fn serialize_value<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + Serialize,
+    {
+        let key = self.last_key.clone().ok_or(Error::ExpectedKey)?;
+        let mut buf = String::new();
+
+        if value
+            .serialize(&mut IniValueSerializer::new(&mut buf, self))
+            .is_ok()
+        {
+            // Value serialized as a primitive type, so write it out
+            if !buf.is_empty() {
+                writeln!(self.kvs, "{buf}")?;
+            }
+            self.last_key = None;
+        } else {
+            // Otherwise its a struct or map, so do a recursive serialization
+            let mut serializer = IniSerializer::new();
+            serializer.last_key = Some(key.clone());
+            value.serialize(&mut serializer)?;
+
+            self.sections.entry(key).or_default().push(serializer);
+        }
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<()> {
+        self.last_key = None;
+        Ok(())
+    }
+}
+
+impl ser::SerializeStruct for &'_ mut IniSerializer {
+    type Ok = ();
+    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<()> {
+        ser::SerializeMap::end(self)
+    }
+}
+
+struct IniSeqSerializer<'a> {
+    name: Option<String>,
+    ser: &'a mut IniSerializer,
+    kvs: String,
+}
+
+impl<'a> IniSeqSerializer<'a> {
+    pub fn new(name: Option<String>, ser: &'a mut IniSerializer) -> Self {
+        Self {
+            name,
+            ser,
+            kvs: String::new(),
+        }
+    }
+}
+
+impl ser::SerializeSeq for IniSeqSerializer<'_> {
+    type Ok = ();
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + Serialize,
+    {
+        let mut buf = String::new();
+        if value
+            .serialize(&mut IniValueSerializer::new(&mut buf, self.ser))
+            .is_ok()
+        {
+            // Value serialized as a primitive type, so write it out
+            if let Some(name) = &self.name.take() {
+                write!(self.kvs, "{name} = ")?;
+            } else {
+                write!(self.kvs, ", ")?;
+            }
+            write!(self.kvs, "{buf}")?;
+        } else if let Some(name) = &self.name {
+            // Otherwise its a struct or map, so do a recursive serialization
+            let mut serializer = IniSerializer::new();
+            value.serialize(&mut serializer)?;
+
+            self.ser
+                .sections
+                .entry(name.clone())
+                .or_default()
+                .push(serializer)
+        } else {
+            return Err(Error::Message(
+                "got non-primitive value but no name!".into(),
+            ));
+        }
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<()> {
+        if !self.kvs.is_empty() {
+            Ok(writeln!(self.ser.kvs, "{}", self.kvs)?)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl ser::SerializeTuple for IniSeqSerializer<'_> {
+    type Ok = ();
+    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<()> {
+        ser::SerializeSeq::end(self)
+    }
+}
+
+impl ser::SerializeTupleStruct for IniSeqSerializer<'_> {
+    type Ok = ();
+    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<()> {
+        ser::SerializeSeq::end(self)
+    }
+}
+
+struct IniValueSerializer<'a, W: fmt::Write> {
+    writer: &'a mut W,
+    ser: &'a mut IniSerializer,
+}
+
+impl<'a, W: fmt::Write> IniValueSerializer<'a, W> {
+    fn new(writer: &'a mut W, ser: &'a mut IniSerializer) -> Self {
+        Self { writer, ser }
+    }
+}
+
+impl<W: fmt::Write> IniValueSerializer<'_, W> {
+    fn serialize_value<T: Display>(&mut self, v: T) -> Result<()> {
+        if let Some(key) = self.ser.last_key.take() {
+            write!(self.writer, "{key} = ")?;
+        }
+
+        Ok(write!(self.writer, "{v}")?)
+    }
+}
+
+impl<'a, W: fmt::Write> ser::Serializer for &'a mut IniValueSerializer<'a, W> {
+    type Ok = ();
+    type Error = Error;
+
+    type SerializeSeq = IniSeqSerializer<'a>;
+    type SerializeTuple = IniSeqSerializer<'a>;
+    type SerializeTupleStruct = IniSeqSerializer<'a>;
+    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>;
+
+    fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_char(self, v: char) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_str(self, v: &str) -> Result<Self::Ok> {
+        self.serialize_value(v)
+    }
+
+    fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok> {
+        Err(Error::UnsupportedType("raw bytes"))
+    }
+
+    fn serialize_none(self) -> Result<Self::Ok> {
+        self.serialize_value("")
+    }
+
+    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_value("")
+    }
+
+    fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
+        self.serialize_unit()
+    }
+
+    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)
+    }
+
+    // Serializes as externally tagged representation.
+    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("newtype variant"))
+    }
+
+    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+        Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser))
+    }
+
+    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
+        Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser))
+    }
+
+    fn serialize_tuple_struct(
+        self,
+        _name: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleStruct> {
+        Ok(IniSeqSerializer::new(self.ser.last_key.take(), self.ser))
+    }
+
+    // Serializes as externally tagged representation.
+    fn serialize_tuple_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleVariant> {
+        Err(Error::UnsupportedType("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("struct variant"))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::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>,
+        }
+
+        #[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,
+        }
+
+        let serialized = to_string(&TopLevel {
+            a: 1,
+            nested: NestedStruct {
+                s: "foo",
+                x: 123.4567,
+                l: vec!["a", "b", "c"],
+            },
+            none: None,
+            some: Some(42),
+            bytes: [1, 2, 3],
+            unit: (),
+            unit_struct: PhantomData,
+            unit_variant: Enum::A,
+            newtype_struct: NewtypeStruct(42),
+        })
+        .unwrap();
+
+        eprintln!("{}", &serialized);
+        pretty_assertions::assert_eq!(
+            "a = 1
+none =\x20
+some = 42
+bytes = 1, 2, 3
+unit =\x20
+unit_struct =\x20
+unit_variant = A
+newtype_struct = 42
+
+[nested]
+s = foo
+x = 123.4567
+l = a, b, c
+",
+            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() {
+        let a = 1u32;
+        assert_eq!(to_string(&a), Err(Error::ExpectedKey));
+
+        #[derive(Serialize)]
+        enum Enum {
+            A(u32),
+            B(u32, f32),
+            C { a: u8, b: &'static str },
+        }
+
+        #[derive(Serialize)]
+        struct TopLevel {
+            x: Enum,
+        }
+
+        assert_eq!(
+            to_string(&TopLevel { x: Enum::A(1) }),
+            Err(Error::UnsupportedType("enum newtype variant"))
+        );
+
+        assert_eq!(
+            to_string(&TopLevel { x: Enum::B(1, 2.) }),
+            Err(Error::UnsupportedType("enum tuple variant"))
+        );
+
+        assert_eq!(
+            to_string(&TopLevel {
+                x: Enum::C {
+                    a: 100,
+                    b: "foobar"
+                }
+            }),
+            Err(Error::UnsupportedType("enum struct variant"))
+        );
+    }
+
+    #[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,
+        );
+    }
+}
diff --git a/proxmox-serde/src/lib.rs b/proxmox-serde/src/lib.rs
index c16f4efb..28c3054d 100644
--- a/proxmox-serde/src/lib.rs
+++ b/proxmox-serde/src/lib.rs
@@ -11,6 +11,9 @@ pub mod json;
 #[cfg(feature = "perl")]
 pub mod perl;
 
+#[cfg(feature = "ini-ser")]
+pub mod ini;
+
 /// Serialize Unix epoch (i64) as RFC3339.
 ///
 /// Usage example:
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
                   ` (8 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 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>
---
 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 28c3054d..c3ec4f41 100644
--- a/proxmox-serde/src/lib.rs
+++ b/proxmox-serde/src/lib.rs
@@ -159,3 +159,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.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports Christoph Heiss
                   ` (7 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 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>
---
 proxmox-network-types/src/endpoint.rs | 154 ++++++++++++++++++++++++++
 proxmox-network-types/src/lib.rs      |   3 +-
 2 files changed, 156 insertions(+), 1 deletion(-)
 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..24e33c7f
--- /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) => write!(f, "{s}"),
+            HostnameOrIpAddr::IpAddr(addr) => match addr {
+                IpAddr::V4(v4) => write!(f, "{v4}"),
+                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 ee26b1c1..1bacbaf3 100644
--- a/proxmox-network-types/src/lib.rs
+++ b/proxmox-network-types/src/lib.rs
@@ -1,5 +1,6 @@
 #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
-#![deny(unsafe_op_in_unsafe_fn)]
+#![deny(unsafe_code, unsafe_op_in_unsafe_fn)]
 
+pub mod endpoint;
 pub mod ip_address;
 pub mod mac_address;
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (2 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
                   ` (6 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 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.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (3 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate Christoph Heiss
                   ` (5 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 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.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (4 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey Christoph Heiss
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 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>
---
 Cargo.toml                             |   1 +
 proxmox-wireguard/Cargo.toml           |  21 ++
 proxmox-wireguard/debian/changelog     |   5 +
 proxmox-wireguard/debian/control       |  48 ++++
 proxmox-wireguard/debian/copyright     |  18 ++
 proxmox-wireguard/debian/debcargo.toml |   7 +
 proxmox-wireguard/src/lib.rs           | 320 +++++++++++++++++++++++++
 7 files changed, 420 insertions(+)
 create mode 100644 proxmox-wireguard/Cargo.toml
 create mode 100644 proxmox-wireguard/debian/changelog
 create mode 100644 proxmox-wireguard/debian/control
 create mode 100644 proxmox-wireguard/debian/copyright
 create mode 100644 proxmox-wireguard/debian/debcargo.toml
 create mode 100644 proxmox-wireguard/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index 3cdad8d8..59714664 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,7 @@ members = [
     "proxmox-time-api",
     "proxmox-upgrade-checks",
     "proxmox-uuid",
+    "proxmox-wireguard",
     "proxmox-worker-task",
     "pbs-api-types",
     "pve-api-types",
diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
new file mode 100644
index 00000000..5976aa90
--- /dev/null
+++ b/proxmox-wireguard/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "proxmox-wireguard"
+description = "WireGuard configuration support"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+ed25519-dalek = "2.1"
+serde = { workspace = true, features = [ "derive" ] }
+thiserror.workspace = true
+proxmox-serde = { workspace = true, features = [ "ini-ser" ] }
+proxmox-network-types.workspace = true
+proxmox-sys.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
diff --git a/proxmox-wireguard/debian/changelog b/proxmox-wireguard/debian/changelog
new file mode 100644
index 00000000..a7120b87
--- /dev/null
+++ b/proxmox-wireguard/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-wireguard (0.1.0-1) unstable; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 15 May 2025 14:54:37 +0200
diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control
new file mode 100644
index 00000000..4adc1ac2
--- /dev/null
+++ b/proxmox-wireguard/debian/control
@@ -0,0 +1,48 @@
+Source: rust-proxmox-wireguard
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-ed25519-dalek-2+default-dev (>= 2.1-~~) <!nocheck>,
+ librust-proxmox-network-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+ini-ser-dev <!nocheck>,
+ librust-proxmox-serde-1+serde-json-dev <!nocheck>,
+ librust-proxmox-sys-1+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-thiserror-2+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-wireguard
+
+Package: librust-proxmox-wireguard-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-ed25519-dalek-2+default-dev (>= 2.1-~~),
+ librust-proxmox-network-types-0.1+default-dev,
+ librust-proxmox-serde-1+default-dev,
+ librust-proxmox-serde-1+ini-ser-dev,
+ librust-proxmox-serde-1+serde-json-dev,
+ librust-proxmox-sys-1+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-thiserror-2+default-dev
+Provides:
+ librust-proxmox-wireguard+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1.0+default-dev (= ${binary:Version})
+Description: WireGuard configuration support - Rust source code
+ Source code for Debianized Rust crate "proxmox-wireguard"
diff --git a/proxmox-wireguard/debian/copyright b/proxmox-wireguard/debian/copyright
new file mode 100644
index 00000000..1ea8a56b
--- /dev/null
+++ b/proxmox-wireguard/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-wireguard/debian/debcargo.toml b/proxmox-wireguard/debian/debcargo.toml
new file mode 100644
index 00000000..87a787e6
--- /dev/null
+++ b/proxmox-wireguard/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git"
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
new file mode 100644
index 00000000..840767d8
--- /dev/null
+++ b/proxmox-wireguard/src/lib.rs
@@ -0,0 +1,320 @@
+//! Implements a interface for handling WireGuard configurations and serializing them into the
+//! INI-style format as described in wg(8) "CONFIGURATION FILE FORMAT".
+//!
+//! WireGuard keys are 32-bytes securely-generated values, encoded as base64
+//! for any usage where users might come in contact with them.
+//!
+//! [`PrivateKey`], [`PublicKey`] and [`PresharedKey`] implement all the needed
+//! key primitives.
+//!
+//! By design there is no key pair, as keys should be treated as opaque from a
+//! configuration perspective and not worked with.
+
+#![forbid(unsafe_code, missing_docs)]
+
+use ed25519_dalek::SigningKey;
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr};
+
+/// Possible error when handling WireGuard configurations.
+#[derive(thiserror::Error, Debug, PartialEq, Clone)]
+pub enum Error {
+    /// (Private) key generation failed
+    #[error("failed to generate private key: {0}")]
+    KeyGenFailed(String),
+    /// Serialization  to the WireGuard INI format failed
+    #[error("failed to serialize config: {0}")]
+    SerializationFailed(String),
+}
+
+impl From<proxmox_serde::ini::Error> for Error {
+    fn from(err: proxmox_serde::ini::Error) -> Self {
+        Self::SerializationFailed(err.to_string())
+    }
+}
+
+/// Public key of a WireGuard peer.
+#[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)]
+#[serde(transparent)]
+pub struct PublicKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH],
+);
+
+/// Private key of a WireGuard peer.
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct PrivateKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
+);
+
+impl fmt::Debug for PrivateKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "<private-key>")
+    }
+}
+
+impl PrivateKey {
+    /// Length of the raw private key data in bytes.
+    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
+
+    /// Generates a new private key suitable for use with WireGuard.
+    pub fn generate() -> Result<Self, Error> {
+        generate_key().map(Self)
+    }
+
+    /// Calculates the public key from the private key.
+    pub fn public_key(&self) -> PublicKey {
+        PublicKey(
+            ed25519_dalek::SigningKey::from_bytes(&self.0)
+                .verifying_key()
+                .to_bytes(),
+        )
+    }
+
+    /// Returns the raw private key material.
+    #[must_use]
+    pub fn raw(&self) -> &ed25519_dalek::SecretKey {
+        &self.0
+    }
+
+    /// Builds a new [`PrivateKey`] from raw key material.
+    #[must_use]
+    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
+        // [`SigningKey`] takes care of correct key clamping.
+        Self(SigningKey::from(&data).to_bytes())
+    }
+}
+
+impl From<ed25519_dalek::SecretKey> for PrivateKey {
+    fn from(value: ed25519_dalek::SecretKey) -> Self {
+        Self(value)
+    }
+}
+
+/// Preshared key between two WireGuard peers.
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(transparent)]
+pub struct PresharedKey(
+    #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey,
+);
+
+impl fmt::Debug for PresharedKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "<preshared-key>")
+    }
+}
+
+impl PresharedKey {
+    /// Length of the raw private key data in bytes.
+    pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH;
+
+    /// Generates a new preshared key suitable for use with WireGuard.
+    pub fn generate() -> Result<Self, Error> {
+        generate_key().map(Self)
+    }
+
+    /// Returns the raw private key material.
+    #[must_use]
+    pub fn raw(&self) -> &ed25519_dalek::SecretKey {
+        &self.0
+    }
+
+    /// Builds a new [`PrivateKey`] from raw key material.
+    #[must_use]
+    pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self {
+        // [`SigningKey`] takes care of correct key clamping.
+        Self(SigningKey::from(&data).to_bytes())
+    }
+}
+
+/// A single WireGuard peer.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardPeer {
+    /// Public key, matching the private key of of the remote peer.
+    pub public_key: PublicKey,
+    /// Additional key preshared between two peers. Adds an additional layer of symmetric-key
+    /// cryptography to be mixed into the already existing public-key cryptography, for
+    /// post-quantum resistance.
+    pub preshared_key: PresharedKey,
+    /// List of IPv4/v6 CIDRs from which incoming traffic for this peer is allowed and to which
+    /// outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may be specified for
+    /// matching all IPv4 addresses, and ::/0 may be specified for matching all IPv6 addresses.
+    #[serde(rename = "AllowedIPs")]
+    pub allowed_ips: Vec<Cidr>,
+    /// Remote peer endpoint address to connect to. Optional; only needed on the connecting side.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub endpoint: Option<ServiceEndpoint>,
+    /// A seconds interval, between 1 and 65535 inclusive, of how often to send an authenticated
+    /// empty packet to the peer for the purpose of keeping a stateful firewall or NAT mapping
+    /// valid persistently. For example, if the interface very rarely sends traffic, but it might
+    /// at anytime receive traffic from a peer, and it is behind NAT, the interface might benefit
+    /// from having a persistent keepalive interval of 25 seconds. If unset or set to 0, it is
+    /// turned off.
+    #[serde(skip_serializing_if = "persistent_keepalive_is_off")]
+    pub persistent_keepalive: Option<u16>,
+}
+
+/// Determines whether the given `PersistentKeepalive` value means that it is
+/// turned off. Useful for usage with serde's `skip_serializing_if`.
+fn persistent_keepalive_is_off(value: &Option<u16>) -> bool {
+    value.map(|v| v == 0).unwrap_or(true)
+}
+
+/// Properties of a WireGuard interface.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardInterface {
+    /// Private key for this interface.
+    pub private_key: PrivateKey,
+    /// Port to listen on. Optional; if not specified, chosen randomly. Only needed on the "server"
+    /// side.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub listen_port: Option<u16>,
+    /// Fwmark for outgoing packets.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub fw_mark: Option<u32>,
+}
+
+/// Top-level WireGuard configuration for WireGuard network interface. Holds all
+/// parameters for the interface itself, as well as its remote peers.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "PascalCase")]
+pub struct WireGuardConfig {
+    /// The WireGuard-specific network interface configuration.
+    pub interface: WireGuardInterface,
+    /// Peers for this WireGuard interface.
+    #[serde(rename = "Peer")]
+    pub peers: Vec<WireGuardPeer>,
+}
+
+impl WireGuardConfig {
+    /// Generate a raw, INI-style configuration file as accepted by wg(8).
+    pub fn to_raw_config(self) -> Result<String, Error> {
+        Ok(proxmox_serde::ini::to_string(&self)?)
+    }
+}
+
+/// Generates a new ED25519 private key.
+fn generate_key() -> Result<ed25519_dalek::SecretKey, Error> {
+    let mut secret = ed25519_dalek::SecretKey::default();
+    proxmox_sys::linux::fill_with_random_data(&mut secret)
+        .map_err(|err| Error::KeyGenFailed(err.to_string()))?;
+
+    // [`SigningKey`] takes care of correct key clamping.
+    Ok(SigningKey::from(&secret).to_bytes())
+}
+
+#[cfg(test)]
+mod tests {
+    use std::net::Ipv4Addr;
+
+    use proxmox_network_types::ip_address::Cidr;
+
+    use crate::{PresharedKey, PrivateKey, WireGuardConfig, WireGuardInterface, WireGuardPeer};
+
+    fn mock_private_key(v: u8) -> PrivateKey {
+        let base = v * 32;
+        PrivateKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
+    }
+
+    fn mock_preshared_key(v: u8) -> PresharedKey {
+        let base = v * 32;
+        PresharedKey((base..base + 32).collect::<Vec<u8>>().try_into().unwrap())
+    }
+
+    #[test]
+    fn single_peer() {
+        let config = WireGuardConfig {
+            interface: WireGuardInterface {
+                private_key: mock_private_key(0),
+                listen_port: Some(51820),
+                fw_mark: Some(127),
+            },
+            peers: vec![WireGuardPeer {
+                public_key: mock_private_key(1).public_key(),
+                preshared_key: mock_preshared_key(1),
+                allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()],
+                endpoint: Some("foo.example.com:51820".parse().unwrap()),
+                persistent_keepalive: Some(25),
+            }],
+        };
+
+        pretty_assertions::assert_eq!(
+            config.to_raw_config().unwrap(),
+            "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+ListenPort = 51820
+FwMark = 127
+
+[Peer]
+PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+AllowedIPs = 192.168.0.0/24
+Endpoint = foo.example.com:51820
+PersistentKeepalive = 25
+"
+        );
+    }
+
+    #[test]
+    fn multiple_peers() {
+        let config = WireGuardConfig {
+            interface: WireGuardInterface {
+                private_key: mock_private_key(0),
+                listen_port: Some(51820),
+                fw_mark: None,
+            },
+            peers: vec![
+                WireGuardPeer {
+                    public_key: mock_private_key(1).public_key(),
+                    preshared_key: mock_preshared_key(1),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 0, 0), 24).unwrap()],
+                    endpoint: Some("foo.example.com:51820".parse().unwrap()),
+                    persistent_keepalive: None,
+                },
+                WireGuardPeer {
+                    public_key: mock_private_key(2).public_key(),
+                    preshared_key: mock_preshared_key(2),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap()],
+                    endpoint: None,
+                    persistent_keepalive: Some(25),
+                },
+                WireGuardPeer {
+                    public_key: mock_private_key(3).public_key(),
+                    preshared_key: mock_preshared_key(3),
+                    allowed_ips: vec![Cidr::new_v4(Ipv4Addr::new(192, 168, 2, 0), 24).unwrap()],
+                    endpoint: None,
+                    persistent_keepalive: None,
+                },
+            ],
+        };
+
+        pretty_assertions::assert_eq!(
+            config.to_raw_config().unwrap(),
+            "[Interface]
+PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
+ListenPort = 51820
+
+[Peer]
+PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc=
+PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=
+AllowedIPs = 192.168.0.0/24
+Endpoint = foo.example.com:51820
+
+[Peer]
+PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0=
+PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8=
+AllowedIPs = 192.168.1.0/24
+PersistentKeepalive = 25
+
+[Peer]
+PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U=
+PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8=
+AllowedIPs = 192.168.2.0/24
+"
+        );
+    }
+}
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (5 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional Christoph Heiss
                   ` (3 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

.. 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>
---
 proxmox-wireguard/Cargo.toml     |  6 ++++++
 proxmox-wireguard/debian/control | 19 +++++++++++++++++++
 proxmox-wireguard/src/lib.rs     | 17 +++++++++++++++++
 3 files changed, 42 insertions(+)

diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml
index 5976aa90..c01a4902 100644
--- a/proxmox-wireguard/Cargo.toml
+++ b/proxmox-wireguard/Cargo.toml
@@ -13,9 +13,15 @@ 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-serde = { workspace = true, features = [ "ini-ser" ] }
 proxmox-network-types.workspace = true
 proxmox-sys.workspace = true
+regex = { workspace = true, optional = true }
 
 [dev-dependencies]
 pretty_assertions.workspace = true
+
+[features]
+default = []
+api-types = ["dep:proxmox-schema", "dep:regex"]
diff --git a/proxmox-wireguard/debian/control b/proxmox-wireguard/debian/control
index 4adc1ac2..f613b839 100644
--- a/proxmox-wireguard/debian/control
+++ b/proxmox-wireguard/debian/control
@@ -36,6 +36,8 @@ Depends:
  librust-serde-1+default-dev,
  librust-serde-1+derive-dev,
  librust-thiserror-2+default-dev
+Suggests:
+ librust-proxmox-wireguard+api-types-dev (= ${binary:Version})
 Provides:
  librust-proxmox-wireguard+default-dev (= ${binary:Version}),
  librust-proxmox-wireguard-0-dev (= ${binary:Version}),
@@ -46,3 +48,20 @@ Provides:
  librust-proxmox-wireguard-0.1.0+default-dev (= ${binary:Version})
 Description: WireGuard configuration support - Rust source code
  Source code for Debianized Rust crate "proxmox-wireguard"
+
+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.0.1-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~),
+ 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.
diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 840767d8..d4697560 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.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (6 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type Christoph Heiss
                   ` (2 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

Authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-wireguard/src/lib.rs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index d4697560..593713d8 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -155,7 +155,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.
@@ -252,7 +252,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),
@@ -287,21 +287,21 @@ 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,
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (7 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types Christoph Heiss
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-sdn-types/src/lib.rs       |  1 +
 proxmox-sdn-types/src/wireguard.rs | 43 ++++++++++++++++++++++++++++++
 2 files changed, 44 insertions(+)
 create mode 100644 proxmox-sdn-types/src/wireguard.rs

diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
index 1656f1d..3328b45 100644
--- a/proxmox-sdn-types/src/lib.rs
+++ b/proxmox-sdn-types/src/lib.rs
@@ -1,3 +1,4 @@
 pub mod area;
 pub mod net;
 pub mod openfabric;
+pub mod wireguard;
diff --git a/proxmox-sdn-types/src/wireguard.rs b/proxmox-sdn-types/src/wireguard.rs
new file mode 100644
index 0000000..4c79b50
--- /dev/null
+++ b/proxmox-sdn-types/src/wireguard.rs
@@ -0,0 +1,43 @@
+//! API types for the WireGuard fabric.
+
+use std::fmt::Display;
+
+use proxmox_schema::{api, UpdaterType};
+use serde::{Deserialize, Serialize};
+
+/// Persistent keep-alive interval. Specifies how often a authenticated, empty
+/// packet will be sent to the peer to keep e.g. stateful firewall open or NAT
+/// mappings.
+///
+/// Interval in seconds, between 1 and 65536 inclusive.
+#[api(
+    type: Integer,
+    minimum: 1,
+)]
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct PersistentKeepalive(
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16,
+);
+
+impl PersistentKeepalive {
+    /// Determines whether the given `PersistentKeepalive` value means that it is
+    /// turned off.
+    pub fn is_off(&self) -> bool {
+        self.0 == 0
+    }
+
+    pub fn raw(&self) -> u16 {
+        self.0
+    }
+}
+
+impl UpdaterType for PersistentKeepalive {
+    type Updater = Option<PersistentKeepalive>;
+}
+
+impl Display for PersistentKeepalive {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (8 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types Christoph Heiss
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

It's always the same, so simplify future additions using a simple macro.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/mod.rs | 80 ++++++++-----------------
 1 file changed, 24 insertions(+), 56 deletions(-)

diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 677a309..d0add92 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -162,65 +162,33 @@ where
     }
 }
 
-impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
-    /// Get the OpenFabric fabric config.
-    ///
-    /// This method is implemented for [`Entry<OpenfabricProperties, OpenfabricNodeProperties>`],
-    /// so it is guaranteed that a [`FabricSection<OpenfabricProperties>`] is returned.
-    pub fn fabric_section(&self) -> &FabricSection<OpenfabricProperties> {
-        if let Fabric::Openfabric(section) = &self.fabric {
-            return section;
+macro_rules! impl_entry {
+    ($variant:ident, $propty:ty, $nodepropty:ty) => {
+        impl Entry<$propty, $nodepropty> {
+            pub fn fabric_section(&self) -> &FabricSection<$propty> {
+                if let Fabric::$variant(section) = &self.fabric {
+                    return section;
+                }
+
+                unreachable!();
+            }
+
+            pub fn node_section(
+                &self,
+                id: &NodeId,
+            ) -> Result<&NodeSection<$nodepropty>, FabricConfigError> {
+                if let Node::$variant(section) = self.get_node(id)? {
+                    return Ok(section);
+                }
+
+                unreachable!();
+            }
         }
-
-        unreachable!();
-    }
-
-    /// Get the OpenFabric node config for the given node_id.
-    ///
-    /// This method is implemented for [`Entry<OpenfabricProperties, OpenfabricNodeProperties>`],
-    /// so it is guaranteed that a [`NodeSection<OpenfabricNodeProperties>`] is returned.
-    /// An error is returned if the node is not found.
-    pub fn node_section(
-        &self,
-        id: &NodeId,
-    ) -> Result<&NodeSection<OpenfabricNodeProperties>, FabricConfigError> {
-        if let Node::Openfabric(section) = self.get_node(id)? {
-            return Ok(section);
-        }
-
-        unreachable!();
-    }
+    };
 }
 
-impl Entry<OspfProperties, OspfNodeProperties> {
-    /// Get the OSPF fabric config.
-    ///
-    /// This method is implemented for [`Entry<OspfProperties, OspfNodeProperties>`],
-    /// so it is guaranteed that a [`FabricSection<OspfProperties>`] is returned.
-    pub fn fabric_section(&self) -> &FabricSection<OspfProperties> {
-        if let Fabric::Ospf(section) = &self.fabric {
-            return section;
-        }
-
-        unreachable!();
-    }
-
-    /// Get the OSPF node config for the given node_id.
-    ///
-    /// This method is implemented for [`Entry<OspfProperties, OspfNodeProperties>`],
-    /// so it is guaranteed that a [`NodeSection<OspfNodeProperties>`] is returned.
-    /// An error is returned if the node is not found.
-    pub fn node_section(
-        &self,
-        id: &NodeId,
-    ) -> Result<&NodeSection<OspfNodeProperties>, FabricConfigError> {
-        if let Node::Ospf(section) = self.get_node(id)? {
-            return Ok(section);
-        }
-
-        unreachable!();
-    }
-}
+impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties);
+impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
 
 /// All possible entries in a [`FabricConfig`].
 ///
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

* [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types
  2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
                   ` (9 preceding siblings ...)
  2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro Christoph Heiss
@ 2026-01-16 15:33 ` Christoph Heiss
  10 siblings, 0 replies; 12+ messages in thread
From: Christoph Heiss @ 2026-01-16 15:33 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Cargo.toml                                    |   1 +
 proxmox-ve-config/Cargo.toml                  |   2 +
 proxmox-ve-config/debian/control              |  18 +-
 proxmox-ve-config/src/sdn/fabric/frr.rs       |   1 +
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 119 +++++++++++++
 .../src/sdn/fabric/section_config/fabric.rs   |  23 +++
 .../src/sdn/fabric/section_config/mod.rs      |  19 ++
 .../src/sdn/fabric/section_config/node.rs     |  33 +++-
 .../sdn/fabric/section_config/protocol/mod.rs |   1 +
 .../section_config/protocol/wireguard.rs      | 162 ++++++++++++++++++
 10 files changed, 374 insertions(+), 5 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs

diff --git a/Cargo.toml b/Cargo.toml
index 99bd54a..d925d5a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,3 +29,4 @@ proxmox-network-types = { version = "0.1.1" }
 proxmox-schema = { version = "5" }
 proxmox-sdn-types = { version = "0.1", path = "proxmox-sdn-types" }
 proxmox-serde = { version = "1.0.0" }
+proxmox-wireguard = { version = "0.1.0" }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 3a4dd61..130430c 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -22,6 +22,7 @@ proxmox-frr = { workspace = true, optional = true }
 proxmox-network-types = { workspace = true, features = [ "api-types" ] }
 proxmox-schema = { workspace = true, features = [ "api-types" ] }
 proxmox-sdn-types = { workspace = true }
+proxmox-wireguard = { workspace = true, optional = true }
 proxmox-section-config = { version = "3" }
 proxmox-serde = { workspace = true, features = [ "perl" ]}
 proxmox-sys = "1"
@@ -29,6 +30,7 @@ proxmox-sortable-macro = "1"
 
 [features]
 frr = ["dep:proxmox-frr"]
+wireguard = ["dep:proxmox-wireguard"]
 
 [dev-dependencies]
 insta = "1.21"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index b211827..9cbe1b8 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -58,7 +58,8 @@ Depends:
  librust-thiserror-2+default-dev,
  librust-tracing-0.1+default-dev (>= 0.1.37-~~)
 Suggests:
- librust-proxmox-ve-config+frr-dev (= ${binary:Version})
+ librust-proxmox-ve-config+frr-dev (= ${binary:Version}),
+ librust-proxmox-ve-config+wireguard-dev (= ${binary:Version})
 Provides:
  librust-proxmox-ve-config+default-dev (= ${binary:Version}),
  librust-proxmox-ve-config-0-dev (= ${binary:Version}),
@@ -84,3 +85,18 @@ Provides:
 Description: Rust crate "proxmox-ve-config" - feature "frr"
  This metapackage enables feature "frr" for the Rust proxmox-ve-config crate, by
  pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-ve-config+wireguard-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-ve-config-dev (= ${binary:Version}),
+ librust-proxmox-wireguard-0.1+default-dev
+Provides:
+ librust-proxmox-ve-config-0+wireguard-dev (= ${binary:Version}),
+ librust-proxmox-ve-config-0.4+wireguard-dev (= ${binary:Version}),
+ librust-proxmox-ve-config-0.4.6+wireguard-dev (= ${binary:Version})
+Description: Rust crate "proxmox-ve-config" - feature "wireguard"
+ This metapackage enables feature "wireguard" for the Rust proxmox-ve-config
+ crate, by pulling in any additional dependencies needed by that feature.
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 10025b3..fc41410 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -232,6 +232,7 @@ pub fn build_fabric(
 
                 frr_config.protocol_routemaps.insert(protocol_routemap);
             }
+            FabricEntry::WireGuard(_) => {} // not a frr fabric
         }
     }
     Ok(())
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index d0add92..a3b9606 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -7,6 +7,10 @@ use std::marker::PhantomData;
 use std::ops::Deref;
 
 use anyhow::Error;
+use section_config::protocol::wireguard::{
+    WireGuardDeletableProperties, WireGuardNodeDeletableProperties, WireGuardNodeProperties,
+    WireGuardNodePropertiesUpdater, WireGuardProperties, WireGuardPropertiesUpdater,
+};
 use serde::{Deserialize, Serialize};
 
 use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
@@ -189,6 +193,7 @@ macro_rules! impl_entry {
 
 impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties);
 impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
+impl_entry!(WireGuard, WireGuardProperties, WireGuardNodeProperties);
 
 /// All possible entries in a [`FabricConfig`].
 ///
@@ -198,6 +203,7 @@ impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
 pub enum FabricEntry {
     Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
     Ospf(Entry<OspfProperties, OspfNodeProperties>),
+    WireGuard(Entry<WireGuardProperties, WireGuardNodeProperties>),
 }
 
 impl FabricEntry {
@@ -209,6 +215,9 @@ impl FabricEntry {
                 entry.add_node(node_section)
             }
             (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+            (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => {
+                entry.add_node(node_section)
+            }
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -219,6 +228,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node(id),
             FabricEntry::Ospf(entry) => entry.get_node(id),
+            FabricEntry::WireGuard(entry) => entry.get_node(id),
         }
     }
 
@@ -228,6 +238,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
             FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+            FabricEntry::WireGuard(entry) => entry.get_node_mut(id),
         }
     }
 
@@ -307,6 +318,52 @@ impl FabricEntry {
 
                 Ok(())
             }
+            (Node::WireGuard(node_section), NodeUpdater::WireGuard(updater)) => {
+                let NodeDataUpdater::<
+                    WireGuardNodePropertiesUpdater,
+                    WireGuardNodeDeletableProperties,
+                > {
+                    ip,
+                    ip6,
+                    properties:
+                        WireGuardNodePropertiesUpdater {
+                            interfaces,
+                            listen_port,
+                        },
+                    delete,
+                } = updater;
+
+                if let Some(ip) = ip {
+                    node_section.ip = Some(ip);
+                }
+
+                if let Some(ip) = ip6 {
+                    node_section.ip6 = Some(ip);
+                }
+
+                if let Some(interfaces) = interfaces {
+                    node_section.properties.interfaces = interfaces;
+                }
+
+                if let Some(listen_port) = listen_port {
+                    node_section.properties.listen_port = Some(listen_port);
+                }
+
+                for property in delete {
+                    match property {
+                        NodeDeletableProperties::Ip => node_section.ip = None,
+                        NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+                        NodeDeletableProperties::Protocol(
+                            WireGuardNodeDeletableProperties::Interfaces,
+                        ) => node_section.properties.interfaces = Vec::new(),
+                        NodeDeletableProperties::Protocol(
+                            WireGuardNodeDeletableProperties::ListenPort,
+                        ) => node_section.properties.listen_port = None,
+                    }
+                }
+
+                Ok(())
+            }
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -316,6 +373,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.nodes.iter(),
             FabricEntry::Ospf(entry) => entry.nodes.iter(),
+            FabricEntry::WireGuard(entry) => entry.nodes.iter(),
         }
     }
 
@@ -324,6 +382,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.delete_node(id),
             FabricEntry::Ospf(entry) => entry.delete_node(id),
+            FabricEntry::WireGuard(entry) => entry.delete_node(id),
         }
     }
 
@@ -333,6 +392,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.into_pair(),
             FabricEntry::Ospf(entry) => entry.into_pair(),
+            FabricEntry::WireGuard(entry) => entry.into_pair(),
         }
     }
 
@@ -341,6 +401,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &entry.fabric,
             FabricEntry::Ospf(entry) => &entry.fabric,
+            FabricEntry::WireGuard(entry) => &entry.fabric,
         }
     }
 
@@ -349,6 +410,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &mut entry.fabric,
             FabricEntry::Ospf(entry) => &mut entry.fabric,
+            FabricEntry::WireGuard(entry) => &mut entry.fabric,
         }
     }
 }
@@ -360,6 +422,7 @@ impl From<Fabric> for FabricEntry {
                 FabricEntry::Openfabric(Entry::new(fabric_section))
             }
             Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+            Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)),
         }
     }
 }
@@ -541,6 +604,13 @@ impl Validatable for FabricConfig {
                             return Err(FabricConfigError::DuplicateInterface);
                         }
                     }
+                    Node::WireGuard(node_section) => {
+                        if !node_section.properties().interfaces().all(|interface| {
+                            node_interfaces.insert((node_id, interface.name.as_str()))
+                        }) {
+                            return Err(FabricConfigError::DuplicateInterface);
+                        }
+                    }
                 }
             }
 
@@ -695,6 +765,55 @@ impl FabricConfig {
 
                 Ok(())
             }
+            (Fabric::WireGuard(fabric_section), FabricUpdater::WireGuard(updater)) => {
+                let FabricSectionUpdater::<
+                    WireGuardPropertiesUpdater,
+                    WireGuardDeletableProperties,
+                > {
+                    ip_prefix,
+                    ip6_prefix,
+                    properties:
+                        WireGuardPropertiesUpdater {
+                            persistent_keepalive,
+                            listen_port,
+                        },
+                    delete,
+                } = updater;
+
+                if let Some(prefix) = ip_prefix {
+                    fabric_section.ip_prefix = Some(prefix);
+                }
+
+                if let Some(prefix) = ip6_prefix {
+                    fabric_section.ip6_prefix = Some(prefix);
+                }
+
+                if let Some(keepalive) = persistent_keepalive {
+                    fabric_section.properties.persistent_keepalive = Some(keepalive);
+                }
+
+                if let Some(listen_port) = listen_port {
+                    fabric_section.properties.listen_port = listen_port;
+                }
+
+                for property in delete {
+                    match property {
+                        FabricDeletableProperties::IpPrefix => {
+                            fabric_section.ip_prefix = None;
+                        }
+                        FabricDeletableProperties::Ip6Prefix => {
+                            fabric_section.ip6_prefix = None;
+                        }
+                        FabricDeletableProperties::Protocol(
+                            WireGuardDeletableProperties::PersistentKeepalive,
+                        ) => {
+                            fabric_section.properties.persistent_keepalive = None;
+                        }
+                    }
+                }
+
+                Ok(())
+            }
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 38911a6..fbfd1a8 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -16,6 +16,10 @@ use crate::sdn::fabric::section_config::protocol::ospf::{
 };
 use crate::sdn::fabric::FabricConfigError;
 
+use super::protocol::wireguard::{
+    WireGuardDeletableProperties, WireGuardProperties, WireGuardPropertiesUpdater,
+};
+
 pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
 
 const_regex! {
@@ -139,6 +143,10 @@ impl UpdaterType for FabricSection<OspfProperties> {
     type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
 }
 
+impl UpdaterType for FabricSection<WireGuardProperties> {
+    type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>;
+}
+
 /// Enum containing all types of fabrics.
 ///
 /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
@@ -159,6 +167,7 @@ impl UpdaterType for FabricSection<OspfProperties> {
 pub enum Fabric {
     Openfabric(FabricSection<OpenfabricProperties>),
     Ospf(FabricSection<OspfProperties>),
+    WireGuard(FabricSection<WireGuardProperties>),
 }
 
 impl UpdaterType for Fabric {
@@ -173,6 +182,7 @@ impl Fabric {
         match self {
             Self::Openfabric(fabric_section) => fabric_section.id(),
             Self::Ospf(fabric_section) => fabric_section.id(),
+            Self::WireGuard(fabric_section) => fabric_section.id(),
         }
     }
 
@@ -183,6 +193,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
+            Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(),
         }
     }
 
@@ -193,6 +204,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
+            Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
         }
     }
 
@@ -203,6 +215,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
+            Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(),
         }
     }
 
@@ -213,6 +226,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
+            Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
         }
     }
 }
@@ -225,6 +239,7 @@ impl Validatable for Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.validate(),
             Fabric::Ospf(fabric_section) => fabric_section.validate(),
+            Fabric::WireGuard(fabric_section) => fabric_section.validate(),
         }
     }
 }
@@ -241,12 +256,19 @@ impl From<FabricSection<OspfProperties>> for Fabric {
     }
 }
 
+impl From<FabricSection<WireGuardProperties>> for Fabric {
+    fn from(section: FabricSection<WireGuardProperties>) -> Self {
+        Fabric::WireGuard(section)
+    }
+}
+
 /// Enum containing all updater types for fabrics
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case", tag = "protocol")]
 pub enum FabricUpdater {
     Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
     Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
+    WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater),
 }
 
 impl Updater for FabricUpdater {
@@ -254,6 +276,7 @@ impl Updater for FabricUpdater {
         match self {
             FabricUpdater::Openfabric(updater) => updater.is_empty(),
             FabricUpdater::Ospf(updater) => updater.is_empty(),
+            FabricUpdater::WireGuard(updater) => updater.is_empty(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index d02d4ae..454145d 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -4,6 +4,7 @@ pub mod node;
 pub mod protocol;
 
 use const_format::concatcp;
+use protocol::wireguard::{WireGuardNodeProperties, WireGuardProperties};
 use serde::{Deserialize, Serialize};
 
 use crate::sdn::fabric::section_config::{
@@ -31,8 +32,10 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
         match section {
             Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
+            Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
             Section::OspfNode(node_section) => Self::Node(node_section.into()),
+            Section::WireGuardNode(node_section) => Self::Node(node_section.into()),
         }
     }
 }
@@ -62,8 +65,10 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION
 pub enum Section {
     OpenfabricFabric(FabricSection<OpenfabricProperties>),
     OspfFabric(FabricSection<OspfProperties>),
+    WireGuardFabric(FabricSection<WireGuardProperties>),
     OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
     OspfNode(NodeSection<OspfNodeProperties>),
+    WireGuardNode(NodeSection<WireGuardNodeProperties>),
 }
 
 impl From<FabricSection<OpenfabricProperties>> for Section {
@@ -78,6 +83,12 @@ impl From<FabricSection<OspfProperties>> for Section {
     }
 }
 
+impl From<FabricSection<WireGuardProperties>> for Section {
+    fn from(section: FabricSection<WireGuardProperties>) -> Self {
+        Self::WireGuardFabric(section)
+    }
+}
+
 impl From<NodeSection<OpenfabricNodeProperties>> for Section {
     fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
         Self::OpenfabricNode(section)
@@ -90,11 +101,18 @@ impl From<NodeSection<OspfNodeProperties>> for Section {
     }
 }
 
+impl From<NodeSection<WireGuardNodeProperties>> for Section {
+    fn from(section: NodeSection<WireGuardNodeProperties>) -> Self {
+        Self::WireGuardNode(section)
+    }
+}
+
 impl From<Fabric> for Section {
     fn from(fabric: Fabric) -> Self {
         match fabric {
             Fabric::Openfabric(fabric_section) => fabric_section.into(),
             Fabric::Ospf(fabric_section) => fabric_section.into(),
+            Fabric::WireGuard(fabric_section) => fabric_section.into(),
         }
     }
 }
@@ -104,6 +122,7 @@ impl From<Node> for Section {
         match node {
             Node::Openfabric(node_section) => node_section.into(),
             Node::Ospf(node_section) => node_section.into(),
+            Node::WireGuard(node_section) => node_section.into(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 17d2f0b..5397b17 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -16,6 +16,8 @@ use crate::sdn::fabric::section_config::{
 };
 use crate::sdn::fabric::FabricConfigError;
 
+use super::protocol::wireguard::WireGuardNodeProperties;
+
 pub const NODE_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]){0,61}(?:[a-zA-Z0-9]){0,1})";
 
 const_regex! {
@@ -147,8 +149,8 @@ impl<T> NodeSection<T> {
     /// Get the IPv4 address (Router-ID) of the [`NodeSection`].
     ///
     /// Either the [`NodeSection::ip`] (IPv4) address or the [`NodeSection::ip6`] (IPv6) address *must*
-    /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
-    /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+    /// be set. This is checked during the validation, so it's guaranteed. OpenFabric and WireGuard
+    /// can also be used dual-stack, so both IPv4 and IPv6 addresses can be set.
     pub fn ip(&self) -> Option<std::net::Ipv4Addr> {
         self.ip.as_deref().copied()
     }
@@ -156,8 +158,8 @@ impl<T> NodeSection<T> {
     /// Get the IPv6 address (Router-ID) of the [`NodeSection`].
     ///
     /// Either the [`NodeSection::ip`] (IPv4) address or the [`NodeSection::ip6`] (IPv6) address *must*
-    /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
-    /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+    /// be set. This is checked during the validation, so it's guaranteed. OpenFabric and WireGuard
+    /// can also be used dual-stack, so both IPv4 and IPv6 addresses can be set.
     pub fn ip6(&self) -> Option<std::net::Ipv6Addr> {
         self.ip6.as_deref().copied()
     }
@@ -186,6 +188,7 @@ impl<T: ApiType> ApiType for NodeSection<T> {
 pub enum Node {
     Openfabric(NodeSection<OpenfabricNodeProperties>),
     Ospf(NodeSection<OspfNodeProperties>),
+    WireGuard(NodeSection<WireGuardNodeProperties>),
 }
 
 impl Node {
@@ -194,6 +197,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.id(),
             Node::Ospf(node_section) => node_section.id(),
+            Node::WireGuard(node_section) => node_section.id(),
         }
     }
 
@@ -202,6 +206,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip(),
             Node::Ospf(node_section) => node_section.ip(),
+            Node::WireGuard(node_section) => node_section.ip(),
         }
     }
 
@@ -210,6 +215,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip6(),
             Node::Ospf(node_section) => node_section.ip6(),
+            Node::WireGuard(node_section) => node_section.ip6(),
         }
     }
 }
@@ -221,6 +227,7 @@ impl Validatable for Node {
         match self {
             Node::Openfabric(node_section) => node_section.validate(),
             Node::Ospf(node_section) => node_section.validate(),
+            Node::WireGuard(node_section) => node_section.validate(),
         }
     }
 }
@@ -237,6 +244,12 @@ impl From<NodeSection<OspfNodeProperties>> for Node {
     }
 }
 
+impl From<NodeSection<WireGuardNodeProperties>> for Node {
+    fn from(value: NodeSection<WireGuardNodeProperties>) -> Self {
+        Self::WireGuard(value)
+    }
+}
+
 /// API types for SDN fabric node configurations.
 ///
 /// This module provides specialized types that are used for API interactions when retrieving,
@@ -263,6 +276,7 @@ pub mod api {
             OpenfabricNodePropertiesUpdater,
         },
         ospf::{OspfNodeDeletableProperties, OspfNodeProperties, OspfNodePropertiesUpdater},
+        wireguard::{WireGuardNodeDeletableProperties, WireGuardNodePropertiesUpdater},
     };
 
     use super::*;
@@ -320,6 +334,7 @@ pub mod api {
     pub enum Node {
         Openfabric(NodeData<OpenfabricNodeProperties>),
         Ospf(NodeData<OspfNodeProperties>),
+        WireGuard(NodeData<WireGuardNodeProperties>),
     }
 
     impl From<super::Node> for Node {
@@ -327,6 +342,7 @@ pub mod api {
             match value {
                 super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
             }
         }
     }
@@ -336,6 +352,7 @@ pub mod api {
             match value {
                 Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
             }
         }
     }
@@ -349,6 +366,11 @@ pub mod api {
         type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
     }
 
+    impl UpdaterType for NodeData<WireGuardNodeProperties> {
+        type Updater =
+            NodeDataUpdater<WireGuardNodePropertiesUpdater, WireGuardNodeDeletableProperties>;
+    }
+
     #[derive(Debug, Clone, Serialize, Deserialize)]
     pub struct NodeDataUpdater<T, D> {
         #[serde(skip_serializing_if = "Option::is_none")]
@@ -384,6 +406,9 @@ pub mod api {
             NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
         ),
         Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+        WireGuard(
+            NodeDataUpdater<WireGuardNodePropertiesUpdater, WireGuardNodeDeletableProperties>,
+        ),
     }
 
     #[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index c1ec847..fd77426 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1,2 +1,3 @@
 pub mod openfabric;
 pub mod ospf;
+pub mod wireguard;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
new file mode 100644
index 0000000..2cc44fc
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -0,0 +1,162 @@
+//! API and section-config interface for WireGuard as an SDN fabric.
+
+use std::ops::{Deref, DerefMut};
+
+use anyhow::Result;
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use proxmox_schema::{
+    api, api_types::PORT_SCHEMA, property_string::PropertyString, ApiStringFormat, Updater,
+};
+use proxmox_sdn_types::wireguard::PersistentKeepalive;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    common::valid::Validatable,
+    sdn::fabric::{
+        section_config::{fabric::FabricSection, interface::InterfaceName, node::NodeSection},
+        FabricConfigError,
+    },
+};
+
+/// Protocol-specific options for an WireGuard fabric.
+#[api]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater, Hash)]
+pub struct WireGuardProperties {
+    /// Fabric-wide persistent keepalive interval between peers.
+    // While this is actually a per-peer property,
+    // we only allow setting it on a per-fabric level (for now) keep configuration
+    // simpler, as you rarely actually want specific intervals per peer, especially
+    // in a cluster environment.
+    #[serde(skip_serializing_if = "persistent_keepalive_is_off")]
+    pub(crate) persistent_keepalive: Option<PersistentKeepalive>,
+    /// Fabric-wide listen port for WireGuard traffic.
+    pub(crate) listen_port: u16,
+}
+
+impl Validatable for FabricSection<WireGuardProperties> {
+    type Error = FabricConfigError;
+
+    /// Validates a [FabricSection<WireGuardProperties>].
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum WireGuardDeletableProperties {
+    PersistentKeepalive,
+}
+
+/// Properties of a WireGuard node.
+#[api(
+    properties: {
+        interfaces: {
+            type: Array,
+            optional: true,
+            items: {
+                type: String,
+                description: "WireGuard interface",
+                format: &ApiStringFormat::PropertyString(&WireGuardInterfaceProperties::API_SCHEMA),
+            }
+        },
+        "listen-port": {
+            optional: true,
+            schema: PORT_SCHEMA,
+        },
+    }
+)]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater, Hash)]
+#[serde(rename_all = "kebab-case")]
+pub struct WireGuardNodeProperties {
+    /// Interface properties for this node.
+    #[serde(default)]
+    pub(crate) interfaces: Vec<PropertyString<WireGuardInterfaceProperties>>,
+    /// Listen port for WireGuard on this node. Overrides the fabric-wide setting.
+    pub(crate) listen_port: Option<u16>,
+    // TODO: add public key to "pin" them in the section config?
+}
+
+impl WireGuardNodeProperties {
+    pub fn interfaces(&self) -> impl Iterator<Item = &WireGuardInterfaceProperties> {
+        self.interfaces
+            .iter()
+            .map(|property_string| property_string.deref())
+    }
+
+    /// Returns an iterator over all the interfaces (mutable).
+    pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut WireGuardInterfaceProperties> {
+        self.interfaces
+            .iter_mut()
+            .map(|property_string| property_string.deref_mut())
+    }
+}
+
+impl Validatable for NodeSection<WireGuardNodeProperties> {
+    type Error = FabricConfigError;
+
+    /// Validates the [FabricSection<WireGuardNodeProperties>].
+    ///
+    /// Checks if we have either an IPv4 or an IPv6 address. If neither is set, return an error.
+    fn validate(&self) -> Result<(), Self::Error> {
+        if self.ip().is_none() && self.ip6().is_none() {
+            return Err(FabricConfigError::NodeNoIp(self.id().to_string()));
+        }
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum WireGuardNodeDeletableProperties {
+    Interfaces,
+    ListenPort,
+}
+
+/// Properties of a WireGuard interface.
+#[api]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater, Hash)]
+pub struct WireGuardInterfaceProperties {
+    /// Name for this WireGuard interface.
+    pub(crate) name: InterfaceName,
+
+    /// If ip and ip6 are unset, then this is an point-to-point interface.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub(crate) ip: Option<Ipv4Cidr>,
+
+    /// If ip6 and ip are unset, then this is an point-to-point interface.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub(crate) ip6: Option<Ipv6Cidr>,
+}
+
+impl WireGuardInterfaceProperties {
+    /// Get the name of the interface.
+    pub fn name(&self) -> &InterfaceName {
+        &self.name
+    }
+
+    /// Set the name of the interface.
+    pub fn set_name(&mut self, name: InterfaceName) {
+        self.name = name
+    }
+
+    /// Get the ip (IPv4) of the interface.
+    pub fn ip(&self) -> Option<&Ipv4Cidr> {
+        self.ip.as_ref()
+    }
+
+    /// Get the ip6 (IPv6) of the interface.
+    pub fn ip6(&self) -> Option<&Ipv6Cidr> {
+        self.ip6.as_ref()
+    }
+}
+
+/// 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<PersistentKeepalive>) -> bool {
+    value
+        .as_ref()
+        .map(PersistentKeepalive::is_off)
+        .unwrap_or(true)
+}
-- 
2.52.0



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-01-16 15:34 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-16 15:33 [pve-devel] [PATCH proxmox{, -ve-rs} 00/11] sdn: add wireguard fabric configuration support Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 01/11] serde: implement ini serializer Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 02/11] serde: add base64 module for byte arrays Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 03/11] network-types: add ServiceEndpoint type as host/port tuple abstraction Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 04/11] schema: provide integer schema for node ports Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 05/11] schema: api-types: add ed25519 base64 encoded key schema Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 06/11] wireguard: init configuration support crate Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 07/11] wireguard: implement api for PublicKey Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox 08/11] wireguard: make per-peer preshared key optional Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 09/11] sdn-types: add wireguard-specific PersistentKeepalive api type Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 10/11] ve-config: fabric: refactor fabric config entry impl using macro Christoph Heiss
2026-01-16 15:33 ` [pve-devel] [PATCH proxmox-ve-rs 11/11] ve-config: sdn: fabrics: add wireguard section config types Christoph Heiss

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