public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics
@ 2026-06-17 11:09 Stefan Hanreich
  2026-06-17 11:09 ` [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema Stefan Hanreich
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:09 UTC (permalink / raw)
  To: pve-devel

## Introduction

This patch series adds status reporting for WireGuard fabrics to pvestatd and
exposes additional information about WireGuard interfaces and neighbors (=
peers) via the pre-existing interface/neighbors endpoints for fabrics.


## Refactoring

This patch series includes some refactoring of existing networking code. I have
extracted the iproute2 helpers from proxmox-network-api into their own
microcrate proxmox-iproute2. This avoids having to pull in the full
proxmox-network-api crate when only the iproute2 helpers are required. This was
the case for several projects (firewall, pdm) and would have been the case for
the status reporting here as well.


## Implementation

This patch series uses the `wg show all dump` CLI command included in the
wireguard-tools package to obtain the current state of WireGuard interfaces and
neighbors. Matching peers to the respective section config entry happens based
on endpoints. Initially, public keys were used - but they are potentially not
unique. Relying on endpoints instead should be more reliable.

Status reporting for WireGuard is trickier than for the other protocols in the
stack, since there's not really a notion of up / down and no way of
distinguishing whether a connection hasn't been used for awhile or has gone
down. Utilizing persistent keepalive can fix this, but this is currently
optional so we have to find a solution for both cases.

The general state of the interface is determined by looking at the flags of the
virtual wireguard interface. If the flags of the interface contain the 'UP'
flag, then the interface is considered 'up'. If it does not contain the 'UP'
flag, then it is considered 'down'. If the interface does not exist at all, or
has the wrong interface type, then it has state 'error'.

State for neighbors has been omitted since there is no surefire way of always
determining the state of a Wireguard peer, but a potential solution of tackling
this has been proposed below in the 'Open questions' section.


## Open questions

The neighbor status endpoint has some non-optional strings in the return schema.
Initially, I returned hard-coded strings for those fields for the WireGuard
fabric, but decided to omit them instead, since they could be potentially
misleading and don't really add anything. Still a bit unsure about this, since
it is technically a breaking API change. It should be easy to just add this in a
new iteration again - even if only 'n/a' or something similar is returned for
the non-optional fields.

For the neighbor status two fields were omitted:

* uptime
The main reason here being that there's no way of obtaining the uptime of an
interface, since the kernel simply doesn't track it. We could track it ourselves
in a runtime dir, but that seems clunky. It also doesn't really add a lot in
the case of WireGuard, as compared to BGP / Openfabric / OSPF.

* status
WireGuard is a non-chatty protocol by default, which poses some problems for
status reporting: As long as traffic is being sent to a peer, handshakes are
exchanged every 180 seconds. If there is no traffic being sent over an interface
(and persistent keepalive is inactive), then the latest handshake cannot be
reliably used to gauge the status of the peer.
One idea to handle this, which I didn't implement yet, would be to have three
states (names are subject to change): 'active', 'idle', 'inactive'. A neighbor
is active if the last handshake occurred in that 180 second window (potentially
including some buffer). Otherwise, if the last handshake is more than 180
seconds in the past, then the neighbor is marked as 'idle'. If a handshake never
occurred, then the neighbor would be 'inactive'. The time window would not
change with persistent keepalive, since that doesn't affect the interval of
handshakes. We could however infer that the connection to a peer has been lost
if persistent keepalive is set. In that case we could potentially introduce a
fourth state, 'failed', if no handshake occured within the last (180 +
keepalive_interval) seconds.
My worry with this approach mainly is that users would configure the fabric,
check the status of the interfaces, see that the peer is 'inactive' and then
falsely assume that something must have went wrong with configuring the fabric,
even though the state only exists because no traffic has been sent yet.


## Dependencies

Quite a few, due to extracting the iproute2-related stuff:

New proxmox-network-api version breaks old proxmox-firewall,
proxmox-datacenter-manager, proxmox-backup, proxmox-network-interface-pinning
and therefore would need a major version bump.

proxmox-firewall, proxmox-datacenter-manager, proxmox-backup,
proxmox-network-interface-pinning, proxmox-network-api all depend on the new
proxmox-iproute2 crate.

libpve-rs-perl depends on librust-proxmox-ve-config
libpve-rs-perl depends on proxmox-iproute2
pve-network depends on libpve-rs-perl
pve-manager depends on libpve-network-api-perl


proxmox:

Stefan Hanreich (4):
  iproute2: schema: move iproute2 helpers to new create / schema
  iproute2: add missing getters
  iproute2: add support for parsing interface flags
  wireguard: derive additional traits for public key

 Cargo.toml                               |   3 +
 proxmox-iproute2/Cargo.toml              |  18 +
 proxmox-iproute2/debian/changelog        |   5 +
 proxmox-iproute2/debian/control          |  42 +++
 proxmox-iproute2/debian/debcargo.toml    |   7 +
 proxmox-iproute2/src/lib.rs              | 416 +++++++++++++++++++++++
 proxmox-network-api/Cargo.toml           |   1 +
 proxmox-network-api/debian/control       |   2 +
 proxmox-network-api/src/config/helper.rs | 367 +-------------------
 proxmox-network-api/src/config/mod.rs    |   8 +-
 proxmox-network-api/src/config/parser.rs |   5 +-
 proxmox-schema/src/api_types.rs          |   5 +
 proxmox-wireguard/src/lib.rs             |   5 +-
 13 files changed, 510 insertions(+), 374 deletions(-)
 create mode 100644 proxmox-iproute2/Cargo.toml
 create mode 100644 proxmox-iproute2/debian/changelog
 create mode 100644 proxmox-iproute2/debian/control
 create mode 100644 proxmox-iproute2/debian/debcargo.toml
 create mode 100644 proxmox-iproute2/src/lib.rs


proxmox-backup:

Stefan Hanreich (1):
  metric_collection: switch to proxmox-iproute2 crate

 Cargo.toml                          | 2 ++
 src/server/metric_collection/mod.rs | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)


proxmox-datacenter-manager:

Stefan Hanreich (1):
  metric_collection: switch to proxmox-iproute2 crate

 Cargo.toml                                            | 1 +
 server/Cargo.toml                                     | 1 +
 server/src/metric_collection/local_collection_task.rs | 6 +++---
 3 files changed, 5 insertions(+), 3 deletions(-)


proxmox-firewall:

Stefan Hanreich (1):
  firewall config: switch to proxmox-iproute2 crate

 Cargo.toml                     | 2 +-
 proxmox-firewall/Cargo.toml    | 2 +-
 proxmox-firewall/src/config.rs | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)


proxmox-network-interface-pinning:

Stefan Hanreich (1):
  network-interface-pinning: switch to proxmox-iproute2 crate

 Cargo.toml  |  1 +
 src/main.rs | 19 ++++++++-----------
 2 files changed, 9 insertions(+), 11 deletions(-)


proxmox-ve-rs:

Stefan Hanreich (1):
  fabric: wireguard: add helper for findings peer based on endpoint

 proxmox-ve-config/src/sdn/fabric/mod.rs       | 115 +++++++++++++++++-
 .../section_config/protocol/wireguard.rs      |   8 ++
 2 files changed, 121 insertions(+), 2 deletions(-)


proxmox-perl-rs:

Stefan Hanreich (1):
  sdn status: fabrics: add status reporting for wireguard

 pve-rs/Cargo.toml                  |   1 +
 pve-rs/src/bindings/sdn/fabrics.rs |  41 ++-
 pve-rs/src/sdn/status.rs           | 529 ++++++++++++++++++++++++++++-
 3 files changed, 562 insertions(+), 9 deletions(-)


pve-network:

Stefan Hanreich (1):
  api: fabric status: add schema for wireguard properties

 src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 71 +++++++++++++++++++++++-
 1 file changed, 68 insertions(+), 3 deletions(-)


pve-manager:

Stefan Hanreich (1):
  ui: fabric content: add wireguard protocol

 www/manager6/sdn/FabricsContentView.js | 173 +++++++++++++++++++------
 www/manager6/sdn/NetworkBrowser.js     |  40 +++---
 2 files changed, 154 insertions(+), 59 deletions(-)


pve-docs:

Stefan Hanreich (1):
  sdn: add documentation for wireguard status reporting

 pve-gui.adoc |  1 +
 pvesdn.adoc  | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)


Summary over all repositories:
  33 files changed, 1500 insertions(+), 465 deletions(-)

-- 
Generated by murpp 0.12.0




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

* [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
@ 2026-06-17 11:09 ` Stefan Hanreich
  2026-06-17 11:09 ` [PATCH proxmox 02/13] iproute2: add missing getters Stefan Hanreich
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:09 UTC (permalink / raw)
  To: pve-devel

Previously, the helpers related to iproute2 have been contained in
proxmox-network-api, which required crates that are only interested in
the iproute2 parsing to pull in the full proxmox-network-api crate.

The helpers for iproute2 have been included in proxmox-network-api
mainly due to the proxmox-network-interface-pinning tool. Since then,
several other callsites (proxmox-datacenter-manager, proxmox-firewall)
have been added, that require only the iproute2 part specifically.
Also, status reporting for the WireGuard fabric will utilize the
iproute2 helpers as well, without any need for the proxmox-network-api
itself.

PHYSICAL_NIC_REGEX has been contained in proxmox-network-api as well,
but is now required by both proxmox-network-api as well as
proxmox-iproute2. Move it to proxmox-schema, which contains many
network-related regexes as well, so it can be used conveniently by
both crates.

No functional changes intended.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                               |   3 +
 proxmox-iproute2/Cargo.toml              |  18 ++
 proxmox-iproute2/debian/changelog        |   5 +
 proxmox-iproute2/debian/control          |  42 +++
 proxmox-iproute2/debian/debcargo.toml    |   7 +
 proxmox-iproute2/src/lib.rs              | 368 +++++++++++++++++++++++
 proxmox-network-api/Cargo.toml           |   1 +
 proxmox-network-api/debian/control       |   2 +
 proxmox-network-api/src/config/helper.rs | 367 +---------------------
 proxmox-network-api/src/config/mod.rs    |   8 +-
 proxmox-network-api/src/config/parser.rs |   5 +-
 proxmox-schema/src/api_types.rs          |   5 +
 12 files changed, 458 insertions(+), 373 deletions(-)
 create mode 100644 proxmox-iproute2/Cargo.toml
 create mode 100644 proxmox-iproute2/debian/changelog
 create mode 100644 proxmox-iproute2/debian/control
 create mode 100644 proxmox-iproute2/debian/debcargo.toml
 create mode 100644 proxmox-iproute2/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index bef718ec..bfb57908 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ members = [
     "proxmox-ini",
     "proxmox-installer-types",
     "proxmox-io",
+    "proxmox-iproute2",
     "proxmox-lang",
     "proxmox-ldap",
     "proxmox-log",
@@ -171,10 +172,12 @@ proxmox-http = { version = "1.0.5", path = "proxmox-http" }
 proxmox-http-error = { version = "1.0.0", path = "proxmox-http-error" }
 proxmox-human-byte = { version = "1.0.0", path = "proxmox-human-byte" }
 proxmox-ini = { version = "0.1.1", path = "proxmox-ini" }
+proxmox-iproute2 = { version = "0.1.0", path = "proxmox-iproute2" }
 proxmox-io = { version = "1.2.1", path = "proxmox-io" }
 proxmox-lang = { version = "1.5", path = "proxmox-lang" }
 proxmox-log = { version = "1.0.0", path = "proxmox-log" }
 proxmox-login = { version = "1.0.0", path = "proxmox-login" }
+proxmox-network-api = { version = "1.0.5", path = "proxmox-network-api" }
 proxmox-network-types = { version = "1.0.2", path = "proxmox-network-types" }
 proxmox-parallel-handler = { version = "1.0.0", path = "proxmox-parallel-handler" }
 proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
diff --git a/proxmox-iproute2/Cargo.toml b/proxmox-iproute2/Cargo.toml
new file mode 100644
index 00000000..ea54f561
--- /dev/null
+++ b/proxmox-iproute2/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "proxmox-iproute2"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+
+proxmox-network-types.workspace = true
+proxmox-schema = { workspace = true, features = [ "api-types"] }
diff --git a/proxmox-iproute2/debian/changelog b/proxmox-iproute2/debian/changelog
new file mode 100644
index 00000000..418f5f62
--- /dev/null
+++ b/proxmox-iproute2/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-iproute2 (0.1.0-1) trixie; urgency=medium
+
+  * Initial packaging (split out from proxmox-network-api)
+
+ -- Proxmox Support Team <support@proxmox.com>  Tue, 09 Jun 2026 16:27:09 +0200
diff --git a/proxmox-iproute2/debian/control b/proxmox-iproute2/debian/control
new file mode 100644
index 00000000..ea4f7752
--- /dev/null
+++ b/proxmox-iproute2/debian/control
@@ -0,0 +1,42 @@
+Source: rust-proxmox-iproute2
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.85) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
+ librust-proxmox-schema-5+api-types-dev (>= 5.1.1-~~) <!nocheck>,
+ librust-proxmox-schema-5+default-dev (>= 5.1.1-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-iproute2
+
+Package: librust-proxmox-iproute2-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~),
+ librust-proxmox-schema-5+api-types-dev (>= 5.1.1-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.1.1-~~),
+ librust-serde-1+default-dev,
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-iproute2+default-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0+default-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0.1-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-iproute2-0.1.0+default-dev (= ${binary:Version})
+Description: Rust crate "proxmox-iproute2" - Rust source code
+ Source code for Debianized Rust crate "proxmox-iproute2"
diff --git a/proxmox-iproute2/debian/debcargo.toml b/proxmox-iproute2/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-iproute2/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-iproute2/src/lib.rs b/proxmox-iproute2/src/lib.rs
new file mode 100644
index 00000000..9bafb228
--- /dev/null
+++ b/proxmox-iproute2/src/lib.rs
@@ -0,0 +1,368 @@
+use std::collections::HashMap;
+
+use anyhow::{Context, Error, bail};
+
+use proxmox_network_types::mac_address::MacAddress;
+use proxmox_schema::api_types::PHYSICAL_NIC_REGEX;
+
+/// Struct representing the info_slave_data field inside link_info, as returned by `ip -details -json link show`.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+pub struct SlaveData {
+    perm_hw_addr: Option<MacAddress>,
+}
+
+/// Struct representing the link_info field, as returned by `ip -details -json link show`.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+pub struct LinkInfo {
+    info_slave_data: Option<SlaveData>,
+    info_kind: Option<String>,
+}
+
+/// The fields specific to an interface of type `ether`.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+pub struct EtherLink {
+    address: MacAddress,
+}
+
+/// Catch all variant for all unknown link types.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+pub struct UnknownLink {
+    // required, since otherwise the tagged enum will fail to parse, due to the tag of [`Link`]
+    // being link_type.
+    link_type: String,
+}
+
+/// Enum abstracting the fields that are specific to a given link type.
+///
+/// The JSON returned by `ip -details -json link show` returns different fields for different link
+/// types. Depending on the type, fields with the same name can contain different types. This enum
+/// is used to handle the fields that vary between the different link types.
+///
+/// The last variant is a catch all variant, that should capture everything else, so we do not get
+/// deserialization errors in any case.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+#[serde(tag = "link_type", rename_all = "lowercase")]
+pub enum Link {
+    Ether(EtherLink),
+    #[serde(untagged)]
+    Unknown(UnknownLink),
+}
+
+/// An IpLink entry, as returned by `ip -details -json link show`.
+///
+/// For now this parses only the fields that are used throughout our stack, the fields of this
+/// struct are incomplete. To abstract fields that are specific to a link type, this struct uses
+/// [`Link`].
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+pub struct IpLink {
+    ifname: String,
+    #[serde(default)]
+    altnames: Vec<String>,
+    ifindex: i64,
+    #[serde(flatten)]
+    link_type: Link,
+    linkinfo: Option<LinkInfo>,
+    operstate: String,
+}
+
+impl IpLink {
+    /// The index of the interface.
+    pub fn index(&self) -> i64 {
+        self.ifindex
+    }
+
+    /// Whether this is a physical or virtual interface.
+    ///
+    /// For ethernet interfaces, this checks whether info_kind is set in link_info,
+    /// since virtual 'physical' interfaces (e.g. bridges) have link type ether as well.
+    ///
+    /// Otherwise, we fall back to [`PHYSICAL_NIC_REGEX`], which was the sole method for
+    /// determining whether an interface is physical or not before. This should cover other types of
+    /// non-ethernet physical interfaces (e.g. Infiniband), that have not been manually renamed.
+    pub fn is_physical(&self) -> bool {
+        if let Link::Ether(_) = self.link_type {
+            if let Some(linkinfo) = &self.linkinfo {
+                if linkinfo.info_kind.is_none() {
+                    return true;
+                }
+            } else {
+                return true;
+            }
+        }
+
+        PHYSICAL_NIC_REGEX.is_match(&self.ifname)
+    }
+
+    /// The name of the interface (ifname / IFLA_IFNAME).
+    pub fn name(&self) -> &str {
+        &self.ifname
+    }
+
+    /// Returns the MAC address of the physical device, even if the interface is enslaved.
+    ///
+    /// Some interfaces can change their MAC address if they are enslaved to bonds or bridges. This
+    /// method returns the permanent MAC address of a link, independent of whether they are
+    /// enslaved or not.
+    pub fn permanent_mac(&self) -> Option<MacAddress> {
+        if let Link::Ether(ether) = &self.link_type {
+            if let Some(link_info) = &self.linkinfo {
+                if let Some(info_slave_data) = &link_info.info_slave_data {
+                    return info_slave_data.perm_hw_addr;
+                }
+            }
+
+            return Some(ether.address);
+        }
+
+        None
+    }
+
+    /// Returns an iterator over the altnames of an interface.
+    pub fn altnames(&self) -> impl Iterator<Item = &String> {
+        self.altnames.iter()
+    }
+
+    /// Returns whether the interface is currently in an UP state.
+    pub fn active(&self) -> bool {
+        self.operstate == "UP"
+    }
+}
+
+/// A mapping of altnames to the interfaces' ifname.
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct AltnameMapping {
+    mapping: HashMap<String, String>,
+}
+
+impl std::ops::Deref for AltnameMapping {
+    type Target = HashMap<String, String>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.mapping
+    }
+}
+
+impl FromIterator<IpLink> for AltnameMapping {
+    fn from_iter<T: IntoIterator<Item = IpLink>>(iter: T) -> Self {
+        let mut mapping = HashMap::new();
+
+        for iface in iter.into_iter() {
+            for altname in iface.altnames {
+                mapping.insert(altname, iface.ifname.clone());
+            }
+        }
+
+        Self { mapping }
+    }
+}
+
+/// Returns a list of all network interfaces currently available on the host.
+///
+/// This parses the output of `ip -details -json link show` and returns a map of the ifname to the
+/// [`IpLink`] for that interface.
+pub fn get_network_interfaces() -> Result<HashMap<String, IpLink>, Error> {
+    let output = std::process::Command::new("ip")
+        .arg("-details")
+        .arg("-json")
+        .arg("link")
+        .arg("show")
+        .stdout(std::process::Stdio::piped())
+        .output()
+        .with_context(|| "could not obtain ip link output")?;
+
+    if !output.status.success() {
+        bail!("ip link returned non-zero exit code")
+    }
+
+    Ok(serde_json::from_slice::<Vec<IpLink>>(&output.stdout)
+        .with_context(|| "could not deserialize ip link output")?
+        .into_iter()
+        .map(|ip_link| (ip_link.ifname.clone(), ip_link))
+        .collect())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_deserialize_ipv6_to_ipv4_tunnel() {
+        let interface = r#"{
+  "ifindex": 7,
+  "link": null,
+  "ifname": "sit0",
+  "flags": [
+    "NOARP"
+  ],
+  "mtu": 1480,
+  "qdisc": "noop",
+  "operstate": "DOWN",
+  "linkmode": "DEFAULT",
+  "group": "default",
+  "txqlen": 1000,
+  "link_type": "sit",
+  "address": "0.0.0.0",
+  "broadcast": "0.0.0.0",
+  "promiscuity": 0,
+  "allmulti": 0,
+  "min_mtu": 1280,
+  "max_mtu": 65555,
+  "linkinfo": {
+    "info_kind": "sit",
+    "info_data": {
+      "proto": "ip6ip",
+      "remote": "any",
+      "local": "any",
+      "ttl": 64,
+      "pmtudisc": false,
+      "prefix": "2002::",
+      "prefixlen": 16
+    }
+  },
+  "inet6_addr_gen_mode": "eui64",
+  "num_tx_queues": 1,
+  "num_rx_queues": 1,
+  "gso_max_size": 65536,
+  "gso_max_segs": 65535,
+  "tso_max_size": 65536,
+  "tso_max_segs": 65535,
+  "gro_max_size": 65536,
+  "gso_ipv4_max_size": 65536,
+  "gro_ipv4_max_size": 65536
+}"#;
+
+        serde_json::from_str::<IpLink>(interface).unwrap();
+    }
+
+    #[test]
+    fn test_deserialize_ethernet_interface() {
+        let interface = r#"{
+    "ifindex": 2,
+    "ifname": "eth1",
+    "flags": [
+      "BROADCAST",
+      "MULTICAST",
+      "UP",
+      "LOWER_UP"
+    ],
+    "mtu": 1500,
+    "qdisc": "fq_codel",
+    "master": "vmbr0",
+    "operstate": "UP",
+    "linkmode": "DEFAULT",
+    "group": "default",
+    "txqlen": 1000,
+    "link_type": "ether",
+    "address": "bc:24:11:ca:ff:ee",
+    "broadcast": "ff:ff:ff:ff:ff:ff",
+    "promiscuity": 1,
+    "allmulti": 1,
+    "min_mtu": 68,
+    "max_mtu": 9194,
+    "linkinfo": {
+      "info_slave_kind": "bridge",
+      "info_slave_data": {
+        "state": "forwarding",
+        "priority": 32,
+        "cost": 5,
+        "hairpin": false,
+        "guard": false,
+        "root_block": false,
+        "fastleave": false,
+        "learning": true,
+        "flood": true,
+        "id": "0x8001",
+        "no": "0x1",
+        "designated_port": 32769,
+        "designated_cost": 0,
+        "bridge_id": "8000.bc:24:11:00:00:00",
+        "root_id": "8000.bc:24:11:00:00:00",
+        "hold_timer": 0.00,
+        "message_age_timer": 0.00,
+        "forward_delay_timer": 0.00,
+        "topology_change_ack": 0,
+        "config_pending": 0,
+        "proxy_arp": false,
+        "proxy_arp_wifi": false,
+        "multicast_router": 1,
+        "mcast_flood": true,
+        "bcast_flood": true,
+        "mcast_to_unicast": false,
+        "neigh_suppress": false,
+        "neigh_vlan_suppress": false,
+        "group_fwd_mask": "0",
+        "group_fwd_mask_str": "0x0",
+        "vlan_tunnel": false,
+        "isolated": false,
+        "locked": false,
+        "mab": false
+      }
+    },
+    "inet6_addr_gen_mode": "eui64",
+    "num_tx_queues": 1,
+    "num_rx_queues": 1,
+    "gso_max_size": 64000,
+    "gso_max_segs": 64,
+    "tso_max_size": 64000,
+    "tso_max_segs": 64,
+    "gro_max_size": 65536,
+    "gso_ipv4_max_size": 64000,
+    "gro_ipv4_max_size": 65536,
+    "parentbus": "pci",
+    "parentdev": "0000:01:00.0",
+    "altnames": [
+      "enxbc2411aabbcc"
+    ]
+}"#;
+
+        serde_json::from_str::<IpLink>(interface).unwrap();
+    }
+
+    #[test]
+    fn test_deserialize_tailscale_interface() {
+        let interface = r#"{
+    "ifindex": 3,
+    "ifname": "tailscale0",
+    "flags": [
+      "POINTOPOINT",
+      "MULTICAST",
+      "NOARP",
+      "UP",
+      "LOWER_UP"
+    ],
+    "mtu": 1280,
+    "qdisc": "fq_codel",
+    "operstate": "UNKNOWN",
+    "linkmode": "DEFAULT",
+    "group": "default",
+    "txqlen": 500,
+    "link_type": "none",
+    "promiscuity": 0,
+    "allmulti": 0,
+    "min_mtu": 68,
+    "max_mtu": 65535,
+    "linkinfo": {
+      "info_kind": "tun",
+      "info_data": {
+        "type": "tun",
+        "pi": false,
+        "vnet_hdr": true,
+        "multi_queue": false,
+        "persist": false
+      }
+    },
+    "inet6_addr_gen_mode": "random",
+    "num_tx_queues": 1,
+    "num_rx_queues": 1,
+    "gso_max_size": 65536,
+    "gso_max_segs": 65535,
+    "tso_max_size": 65536,
+    "tso_max_segs": 65535,
+    "gro_max_size": 65536,
+    "gso_ipv4_max_size": 65536,
+    "gro_ipv4_max_size": 65536
+}"#;
+
+        serde_json::from_str::<IpLink>(interface).unwrap();
+    }
+}
diff --git a/proxmox-network-api/Cargo.toml b/proxmox-network-api/Cargo.toml
index 7c79b171..02cdc190 100644
--- a/proxmox-network-api/Cargo.toml
+++ b/proxmox-network-api/Cargo.toml
@@ -24,6 +24,7 @@ libc = { workspace = true, optional = true }
 proxmox-sys = { workspace = true, optional = true }
 proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] }
 proxmox-config-digest = { workspace = true, optional = true }
+proxmox-iproute2.workspace = true
 proxmox-product-config = { workspace = true, optional = true }
 proxmox-network-types.workspace = true
 
diff --git a/proxmox-network-api/debian/control b/proxmox-network-api/debian/control
index b2af1e6a..07c46978 100644
--- a/proxmox-network-api/debian/control
+++ b/proxmox-network-api/debian/control
@@ -8,6 +8,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
  libstd-rust-dev <!nocheck>,
  librust-anyhow-1+default-dev <!nocheck>,
  librust-const-format-0.2+default-dev <!nocheck>,
+ librust-proxmox-iproute2-0.1+default-dev <!nocheck>,
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
  librust-proxmox-schema-5+api-macro-dev (>= 5.1.1-~~) <!nocheck>,
  librust-proxmox-schema-5+api-types-dev (>= 5.1.1-~~) <!nocheck>,
@@ -30,6 +31,7 @@ Depends:
  ${misc:Depends},
  librust-anyhow-1+default-dev,
  librust-const-format-0.2+default-dev,
+ librust-proxmox-iproute2-0.1+default-dev,
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~),
  librust-proxmox-schema-5+api-macro-dev (>= 5.1.1-~~),
  librust-proxmox-schema-5+api-types-dev (>= 5.1.1-~~),
diff --git a/proxmox-network-api/src/config/helper.rs b/proxmox-network-api/src/config/helper.rs
index a9ae35fc..4e4cf96d 100644
--- a/proxmox-network-api/src/config/helper.rs
+++ b/proxmox-network-api/src/config/helper.rs
@@ -3,16 +3,13 @@ use std::path::Path;
 use std::process::Command;
 use std::sync::LazyLock;
 
-use anyhow::{Context, Error, bail, format_err};
+use anyhow::{Error, bail, format_err};
 use const_format::concatcp;
 use regex::Regex;
 
-use proxmox_network_types::mac_address::MacAddress;
 use proxmox_schema::api_types::IPV4RE_STR;
 use proxmox_schema::api_types::IPV6RE_STR;
 
-use crate::config::PHYSICAL_NIC_REGEX;
-
 pub static IPV4_REVERSE_MASK: &[&str] = &[
     "0.0.0.0",
     "128.0.0.0",
@@ -119,182 +116,6 @@ pub(crate) fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, b
     }
 }
 
-/// Struct representing the info_slave_data field inside link_info, as returned by `ip -details -json link show`.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-pub struct SlaveData {
-    perm_hw_addr: Option<MacAddress>,
-}
-
-/// Struct representing the link_info field, as returned by `ip -details -json link show`.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-pub struct LinkInfo {
-    info_slave_data: Option<SlaveData>,
-    info_kind: Option<String>,
-}
-
-/// The fields specific to an interface of type `ether`.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-pub struct EtherLink {
-    address: MacAddress,
-}
-
-/// Catch all variant for all unknown link types.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-pub struct UnknownLink {
-    // required, since otherwise the tagged enum will fail to parse, due to the tag of [`Link`]
-    // being link_type.
-    link_type: String,
-}
-
-/// Enum abstracting the fields that are specific to a given link type.
-///
-/// The JSON returned by `ip -details -json link show` returns different fields for different link
-/// types. Depending on the type, fields with the same name can contain different types. This enum
-/// is used to handle the fields that vary between the different link types.
-///
-/// The last variant is a catch all variant, that should capture everything else, so we do not get
-/// deserialization errors in any case.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-#[serde(tag = "link_type", rename_all = "lowercase")]
-pub enum Link {
-    Ether(EtherLink),
-    #[serde(untagged)]
-    Unknown(UnknownLink),
-}
-
-/// An IpLink entry, as returned by `ip -details -json link show`.
-///
-/// For now this parses only the fields that are used throughout our stack, the fields of this
-/// struct are incomplete. To abstract fields that are specific to a link type, this struct uses
-/// [`Link`].
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
-pub struct IpLink {
-    ifname: String,
-    #[serde(default)]
-    altnames: Vec<String>,
-    ifindex: i64,
-    #[serde(flatten)]
-    link_type: Link,
-    linkinfo: Option<LinkInfo>,
-    operstate: String,
-}
-
-impl IpLink {
-    /// The index of the interface.
-    pub fn index(&self) -> i64 {
-        self.ifindex
-    }
-
-    /// Whether this is a physical or virtual interface.
-    ///
-    /// For ethernet interfaces, this checks whether info_kind is set in link_info,
-    /// since virtual 'physical' interfaces (e.g. bridges) have link type ether as well.
-    ///
-    /// Otherwise, we fall back to [`PHYSICAL_NIC_REGEX`], which was the sole method for
-    /// determining whether an interface is physical or not before. This should cover other types of
-    /// non-ethernet physical interfaces (e.g. Infiniband), that have not been manually renamed.
-    pub fn is_physical(&self) -> bool {
-        if let Link::Ether(_) = self.link_type {
-            if let Some(linkinfo) = &self.linkinfo {
-                if linkinfo.info_kind.is_none() {
-                    return true;
-                }
-            } else {
-                return true;
-            }
-        }
-
-        PHYSICAL_NIC_REGEX.is_match(&self.ifname)
-    }
-
-    /// The name of the interface (ifname / IFLA_IFNAME).
-    pub fn name(&self) -> &str {
-        &self.ifname
-    }
-
-    /// Returns the MAC address of the physical device, even if the interface is enslaved.
-    ///
-    /// Some interfaces can change their MAC address if they are enslaved to bonds or bridges. This
-    /// method returns the permanent MAC address of a link, independent of whether they are
-    /// enslaved or not.
-    pub fn permanent_mac(&self) -> Option<MacAddress> {
-        if let Link::Ether(ether) = &self.link_type {
-            if let Some(link_info) = &self.linkinfo {
-                if let Some(info_slave_data) = &link_info.info_slave_data {
-                    return info_slave_data.perm_hw_addr;
-                }
-            }
-
-            return Some(ether.address);
-        }
-
-        None
-    }
-
-    /// Returns an iterator over the altnames of an interface.
-    pub fn altnames(&self) -> impl Iterator<Item = &String> {
-        self.altnames.iter()
-    }
-
-    /// Returns whether the interface is currently in an UP state.
-    pub fn active(&self) -> bool {
-        self.operstate == "UP"
-    }
-}
-
-/// A mapping of altnames to the interfaces' ifname.
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct AltnameMapping {
-    mapping: HashMap<String, String>,
-}
-
-impl std::ops::Deref for AltnameMapping {
-    type Target = HashMap<String, String>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.mapping
-    }
-}
-
-impl FromIterator<IpLink> for AltnameMapping {
-    fn from_iter<T: IntoIterator<Item = IpLink>>(iter: T) -> Self {
-        let mut mapping = HashMap::new();
-
-        for iface in iter.into_iter() {
-            for altname in iface.altnames {
-                mapping.insert(altname, iface.ifname.clone());
-            }
-        }
-
-        Self { mapping }
-    }
-}
-
-/// Returns a list of all network interfaces currently available on the host.
-///
-/// This parses the output of `ip -details -json link show` and returns a map of the ifname to the
-/// [`IpLink`] for that interface.
-pub fn get_network_interfaces() -> Result<HashMap<String, IpLink>, Error> {
-    let output = std::process::Command::new("ip")
-        .arg("-details")
-        .arg("-json")
-        .arg("link")
-        .arg("show")
-        .stdout(std::process::Stdio::piped())
-        .output()
-        .with_context(|| "could not obtain ip link output")?;
-
-    if !output.status.success() {
-        bail!("ip link returned non-zero exit code")
-    }
-
-    Ok(serde_json::from_slice::<Vec<IpLink>>(&output.stdout)
-        .with_context(|| "could not deserialize ip link output")?
-        .into_iter()
-        .map(|ip_link| (ip_link.ifname.clone(), ip_link))
-        .collect())
-}
-
 pub(crate) fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error> {
     let output = Command::new("diff")
         .arg("-b")
@@ -329,189 +150,3 @@ pub fn network_reload() -> Result<(), Error> {
 
     Ok(())
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_deserialize_ipv6_to_ipv4_tunnel() {
-        let interface = r#"{
-  "ifindex": 7,
-  "link": null,
-  "ifname": "sit0",
-  "flags": [
-    "NOARP"
-  ],
-  "mtu": 1480,
-  "qdisc": "noop",
-  "operstate": "DOWN",
-  "linkmode": "DEFAULT",
-  "group": "default",
-  "txqlen": 1000,
-  "link_type": "sit",
-  "address": "0.0.0.0",
-  "broadcast": "0.0.0.0",
-  "promiscuity": 0,
-  "allmulti": 0,
-  "min_mtu": 1280,
-  "max_mtu": 65555,
-  "linkinfo": {
-    "info_kind": "sit",
-    "info_data": {
-      "proto": "ip6ip",
-      "remote": "any",
-      "local": "any",
-      "ttl": 64,
-      "pmtudisc": false,
-      "prefix": "2002::",
-      "prefixlen": 16
-    }
-  },
-  "inet6_addr_gen_mode": "eui64",
-  "num_tx_queues": 1,
-  "num_rx_queues": 1,
-  "gso_max_size": 65536,
-  "gso_max_segs": 65535,
-  "tso_max_size": 65536,
-  "tso_max_segs": 65535,
-  "gro_max_size": 65536,
-  "gso_ipv4_max_size": 65536,
-  "gro_ipv4_max_size": 65536
-}"#;
-
-        serde_json::from_str::<IpLink>(interface).unwrap();
-    }
-
-    #[test]
-    fn test_deserialize_ethernet_interface() {
-        let interface = r#"{
-    "ifindex": 2,
-    "ifname": "eth1",
-    "flags": [
-      "BROADCAST",
-      "MULTICAST",
-      "UP",
-      "LOWER_UP"
-    ],
-    "mtu": 1500,
-    "qdisc": "fq_codel",
-    "master": "vmbr0",
-    "operstate": "UP",
-    "linkmode": "DEFAULT",
-    "group": "default",
-    "txqlen": 1000,
-    "link_type": "ether",
-    "address": "bc:24:11:ca:ff:ee",
-    "broadcast": "ff:ff:ff:ff:ff:ff",
-    "promiscuity": 1,
-    "allmulti": 1,
-    "min_mtu": 68,
-    "max_mtu": 9194,
-    "linkinfo": {
-      "info_slave_kind": "bridge",
-      "info_slave_data": {
-        "state": "forwarding",
-        "priority": 32,
-        "cost": 5,
-        "hairpin": false,
-        "guard": false,
-        "root_block": false,
-        "fastleave": false,
-        "learning": true,
-        "flood": true,
-        "id": "0x8001",
-        "no": "0x1",
-        "designated_port": 32769,
-        "designated_cost": 0,
-        "bridge_id": "8000.bc:24:11:00:00:00",
-        "root_id": "8000.bc:24:11:00:00:00",
-        "hold_timer": 0.00,
-        "message_age_timer": 0.00,
-        "forward_delay_timer": 0.00,
-        "topology_change_ack": 0,
-        "config_pending": 0,
-        "proxy_arp": false,
-        "proxy_arp_wifi": false,
-        "multicast_router": 1,
-        "mcast_flood": true,
-        "bcast_flood": true,
-        "mcast_to_unicast": false,
-        "neigh_suppress": false,
-        "neigh_vlan_suppress": false,
-        "group_fwd_mask": "0",
-        "group_fwd_mask_str": "0x0",
-        "vlan_tunnel": false,
-        "isolated": false,
-        "locked": false,
-        "mab": false
-      }
-    },
-    "inet6_addr_gen_mode": "eui64",
-    "num_tx_queues": 1,
-    "num_rx_queues": 1,
-    "gso_max_size": 64000,
-    "gso_max_segs": 64,
-    "tso_max_size": 64000,
-    "tso_max_segs": 64,
-    "gro_max_size": 65536,
-    "gso_ipv4_max_size": 64000,
-    "gro_ipv4_max_size": 65536,
-    "parentbus": "pci",
-    "parentdev": "0000:01:00.0",
-    "altnames": [
-      "enxbc2411aabbcc"
-    ]
-}"#;
-
-        serde_json::from_str::<IpLink>(interface).unwrap();
-    }
-
-    #[test]
-    fn test_deserialize_tailscale_interface() {
-        let interface = r#"{
-    "ifindex": 3,
-    "ifname": "tailscale0",
-    "flags": [
-      "POINTOPOINT",
-      "MULTICAST",
-      "NOARP",
-      "UP",
-      "LOWER_UP"
-    ],
-    "mtu": 1280,
-    "qdisc": "fq_codel",
-    "operstate": "UNKNOWN",
-    "linkmode": "DEFAULT",
-    "group": "default",
-    "txqlen": 500,
-    "link_type": "none",
-    "promiscuity": 0,
-    "allmulti": 0,
-    "min_mtu": 68,
-    "max_mtu": 65535,
-    "linkinfo": {
-      "info_kind": "tun",
-      "info_data": {
-        "type": "tun",
-        "pi": false,
-        "vnet_hdr": true,
-        "multi_queue": false,
-        "persist": false
-      }
-    },
-    "inet6_addr_gen_mode": "random",
-    "num_tx_queues": 1,
-    "num_rx_queues": 1,
-    "gso_max_size": 65536,
-    "gso_max_segs": 65535,
-    "tso_max_size": 65536,
-    "tso_max_segs": 65535,
-    "gro_max_size": 65536,
-    "gso_ipv4_max_size": 65536,
-    "gro_ipv4_max_size": 65536
-}"#;
-
-        serde_json::from_str::<IpLink>(interface).unwrap();
-    }
-}
diff --git a/proxmox-network-api/src/config/mod.rs b/proxmox-network-api/src/config/mod.rs
index 09165bfb..1fdb5e8a 100644
--- a/proxmox-network-api/src/config/mod.rs
+++ b/proxmox-network-api/src/config/mod.rs
@@ -2,7 +2,7 @@ mod helper;
 mod lexer;
 mod parser;
 
-pub use helper::{AltnameMapping, IpLink, assert_ifupdown2_installed, network_reload, parse_cidr};
+pub use helper::{assert_ifupdown2_installed, network_reload, parse_cidr};
 
 use std::collections::{BTreeMap, HashMap, HashSet};
 use std::io::Write;
@@ -17,14 +17,12 @@ use super::{
 };
 
 use helper::compute_file_diff;
-pub use helper::get_network_interfaces;
 use parser::NetworkParser;
 
 use proxmox_config_digest::ConfigDigest;
 use proxmox_product_config::{ApiLockGuard, open_api_lockfile, replace_system_config};
+use proxmox_schema::api_types::PHYSICAL_NIC_REGEX;
 
-static PHYSICAL_NIC_REGEX: LazyLock<Regex> =
-    LazyLock::new(|| Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap());
 static VLAN_INTERFACE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
     Regex::new(r"^(?P<vlan_raw_device>\S+)\.(?P<vlan_id>\d+)|vlan(?P<vlan_id2>\d+)$").unwrap()
 });
@@ -587,7 +585,7 @@ pub fn config() -> Result<(NetworkConfig, ConfigDigest), Error> {
 
     let digest = ConfigDigest::from_slice(&content);
 
-    let existing_interfaces = get_network_interfaces()?;
+    let existing_interfaces = proxmox_iproute2::get_network_interfaces()?;
     let mut parser = NetworkParser::new(&content[..]);
     let data = parser.parse_interfaces(Some(&existing_interfaces))?;
 
diff --git a/proxmox-network-api/src/config/parser.rs b/proxmox-network-api/src/config/parser.rs
index 71c9ec0f..ff0bf33e 100644
--- a/proxmox-network-api/src/config/parser.rs
+++ b/proxmox-network-api/src/config/parser.rs
@@ -1,4 +1,4 @@
-use crate::{PHYSICAL_NIC_REGEX, VLAN_INTERFACE_REGEX};
+use crate::VLAN_INTERFACE_REGEX;
 
 use std::collections::{HashMap, HashSet};
 use std::io::BufRead;
@@ -14,7 +14,8 @@ use super::lexer::*;
 
 use super::LinuxBondMode;
 
-use proxmox_schema::api_types::IP_REGEX;
+use proxmox_iproute2::IpLink;
+use proxmox_schema::api_types::{IP_REGEX, PHYSICAL_NIC_REGEX};
 
 use super::{BondXmitHashPolicy, Interface, NetworkConfigMethod, NetworkInterfaceType};
 
diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
index d6a0608c..69e659d1 100644
--- a/proxmox-schema/src/api_types.rs
+++ b/proxmox-schema/src/api_types.rs
@@ -65,6 +65,9 @@ pub const DNS_ALIAS_NAME_STR: &str = concatcp!(r"(?:(?:", DNS_ALIAS_LABEL_STR ,
 #[rustfmt::skip]
 pub const PORT_REGEX_STR: &str = r"(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])";
 
+#[rustfmt::skip]
+pub const PHYSICAL_NIC_REGEX_STR: &str = r"(?:eth\d+|en[^:.]+|ib\d+)";
+
 const_regex! {
     /// IPv4 regular expression.
     pub IP_V4_REGEX = concatcp!(r"^", IPV4RE_STR, r"$");
@@ -127,6 +130,8 @@ const_regex! {
     /// 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 PHYSICAL_NIC_REGEX = concatcp!(r"^", PHYSICAL_NIC_REGEX_STR, r"$");
 }
 
 pub const SAFE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SAFE_ID_REGEX);
-- 
2.47.3





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

* [PATCH proxmox 02/13] iproute2: add missing getters
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
  2026-06-17 11:09 ` [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema Stefan Hanreich
@ 2026-06-17 11:09 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox 03/13] iproute2: add support for parsing interface flags Stefan Hanreich
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:09 UTC (permalink / raw)
  To: pve-devel

Not all parsed fields provided getters for their value. Make them
available at use sites by providing the missing getters.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-iproute2/src/lib.rs | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/proxmox-iproute2/src/lib.rs b/proxmox-iproute2/src/lib.rs
index 9bafb228..f00d3475 100644
--- a/proxmox-iproute2/src/lib.rs
+++ b/proxmox-iproute2/src/lib.rs
@@ -18,12 +18,28 @@ pub struct LinkInfo {
     info_kind: Option<String>,
 }
 
+impl LinkInfo {
+    pub fn info_slave_data(&self) -> Option<&SlaveData> {
+        self.info_slave_data.as_ref()
+    }
+
+    pub fn info_kind(&self) -> Option<&String> {
+        self.info_kind.as_ref()
+    }
+}
+
 /// The fields specific to an interface of type `ether`.
 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
 pub struct EtherLink {
     address: MacAddress,
 }
 
+impl EtherLink {
+    pub fn address(&self) -> MacAddress {
+        self.address
+    }
+}
+
 /// Catch all variant for all unknown link types.
 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
 pub struct UnknownLink {
@@ -117,6 +133,16 @@ impl IpLink {
         None
     }
 
+    /// The [`LinkInfo`] for this link.
+    pub fn linkinfo(&self) -> Option<&LinkInfo> {
+        self.linkinfo.as_ref()
+    }
+
+    /// The operating state of the interface.
+    pub fn operstate(&self) -> &str {
+        &self.operstate
+    }
+
     /// Returns an iterator over the altnames of an interface.
     pub fn altnames(&self) -> impl Iterator<Item = &String> {
         self.altnames.iter()
-- 
2.47.3





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

* [PATCH proxmox 03/13] iproute2: add support for parsing interface flags
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
  2026-06-17 11:09 ` [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema Stefan Hanreich
  2026-06-17 11:09 ` [PATCH proxmox 02/13] iproute2: add missing getters Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox 04/13] wireguard: derive additional traits for public key Stefan Hanreich
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Interface flags were not available in the parsed ip link output. The
flags are required for determining the status of some interfaces,
since WireGuard for example always sets the operating state to
UNKNOWN, even if the interface is up and working.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-iproute2/src/lib.rs | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/proxmox-iproute2/src/lib.rs b/proxmox-iproute2/src/lib.rs
index f00d3475..1a7efd7e 100644
--- a/proxmox-iproute2/src/lib.rs
+++ b/proxmox-iproute2/src/lib.rs
@@ -64,6 +64,21 @@ pub enum Link {
     Unknown(UnknownLink),
 }
 
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Deserialize)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum LinkFlag {
+    Pointopoint,
+    Noarp,
+    Up,
+    LowerUp,
+    Broadcast,
+    Multicast,
+    Loopback,
+    Promisc,
+    #[serde(untagged)]
+    Unknown(String),
+}
+
 /// An IpLink entry, as returned by `ip -details -json link show`.
 ///
 /// For now this parses only the fields that are used throughout our stack, the fields of this
@@ -79,6 +94,8 @@ pub struct IpLink {
     link_type: Link,
     linkinfo: Option<LinkInfo>,
     operstate: String,
+    #[serde(default)]
+    flags: Vec<LinkFlag>,
 }
 
 impl IpLink {
@@ -148,6 +165,11 @@ impl IpLink {
         self.altnames.iter()
     }
 
+    /// Returns the flags of this interface
+    pub fn flags(&self) -> impl Iterator<Item = &LinkFlag> {
+        self.flags.iter()
+    }
+
     /// Returns whether the interface is currently in an UP state.
     pub fn active(&self) -> bool {
         self.operstate == "UP"
-- 
2.47.3





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

* [PATCH proxmox 04/13] wireguard: derive additional traits for public key
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (2 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox 03/13] iproute2: add support for parsing interface flags Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-backup 05/13] metric_collection: switch to proxmox-iproute2 crate Stefan Hanreich
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-wireguard/src/lib.rs | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs
index 5c3ac674..d8338b1c 100644
--- a/proxmox-wireguard/src/lib.rs
+++ b/proxmox-wireguard/src/lib.rs
@@ -42,7 +42,7 @@ impl From<proxmox_ini::Error> for Error {
 }
 
 /// Public key of a WireGuard peer.
-#[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)]
+#[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Ord, PartialOrd)]
 #[serde(transparent)]
 pub struct PublicKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]);
 
@@ -59,6 +59,9 @@ impl UpdaterType for PublicKey {
     type Updater = Option<PublicKey>;
 }
 
+proxmox_serde::forward_from_str_to_deserialize!(PublicKey);
+proxmox_serde::forward_display_to_serialize!(PublicKey);
+
 /// Private key of a WireGuard peer.
 #[derive(Clone, Copy, Deserialize, Serialize, Hash)]
 #[serde(transparent)]
-- 
2.47.3





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

* [PATCH proxmox-backup 05/13] metric_collection: switch to proxmox-iproute2 crate
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (3 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox 04/13] wireguard: derive additional traits for public key Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-datacenter-manager 06/13] " Stefan Hanreich
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                          | 2 ++
 src/server/metric_collection/mod.rs | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index dc8e2730c..6ab9dec30 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -67,6 +67,7 @@ proxmox-docgen = "1"
 proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "api-types", "websocket" ] } # see below
 proxmox-human-byte = "1"
 proxmox-io = "1.0.1" # tools and client use "tokio" feature
+proxmox-iproute2 = "0.1.0"
 proxmox-lang = "1.1"
 proxmox-log = "1"
 proxmox-ldap = "1"
@@ -228,6 +229,7 @@ proxmox-docgen.workspace = true
 proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
 proxmox-human-byte.workspace = true
 proxmox-io.workspace = true
+proxmox-iproute2.workspace = true
 proxmox-lang.workspace = true
 proxmox-log.workspace = true
 proxmox-ldap.workspace = true
diff --git a/src/server/metric_collection/mod.rs b/src/server/metric_collection/mod.rs
index 18625b1a5..cfa560866 100644
--- a/src/server/metric_collection/mod.rs
+++ b/src/server/metric_collection/mod.rs
@@ -13,8 +13,8 @@ use tokio::join;
 use pbs_api_types::{
     DataStoreConfig, DatastoreBackendConfig, DatastoreBackendType, Operation, S3Statistics,
 };
+use proxmox_iproute2::{IpLink, get_network_interfaces};
 use proxmox_lang::try_block;
-use proxmox_network_api::{IpLink, get_network_interfaces};
 use proxmox_s3_client::SharedRequestCounters;
 use proxmox_sys::fs::FileSystemInformation;
 use proxmox_sys::linux::procfs::{Loadavg, ProcFsMemInfo, ProcFsNetDev, ProcFsStat};
-- 
2.47.3





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

* [PATCH proxmox-datacenter-manager 06/13] metric_collection: switch to proxmox-iproute2 crate
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (4 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-backup 05/13] metric_collection: switch to proxmox-iproute2 crate Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-firewall 07/13] firewall config: " Stefan Hanreich
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                                            | 1 +
 server/Cargo.toml                                     | 1 +
 server/src/metric_collection/local_collection_task.rs | 6 +++---
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index c047fd08..f0fbcfb8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,7 @@ proxmox-docgen = "1"
 proxmox-http = { version = "1.0.4", features = [ "client", "http-helpers", "websocket" ] } # see below
 proxmox-human-byte = "1"
 proxmox-io = "1.0.1" # tools and client use "tokio" feature
+proxmox-iproute2 = "0.1.0"
 proxmox-ldap = { version = "1.1", features = ["sync"] }
 proxmox-lang = "1.1"
 proxmox-log = "1"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 663f21cb..6d28b97c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -44,6 +44,7 @@ proxmox-disks.workspace = true
 proxmox-docgen.workspace = true
 proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
 proxmox-installer-types.workspace = true
+proxmox-iproute2.workspace = true
 proxmox-lang.workspace = true
 proxmox-ldap.workspace = true
 proxmox-log.workspace = true
diff --git a/server/src/metric_collection/local_collection_task.rs b/server/src/metric_collection/local_collection_task.rs
index 034b51a3..19c661da 100644
--- a/server/src/metric_collection/local_collection_task.rs
+++ b/server/src/metric_collection/local_collection_task.rs
@@ -6,8 +6,8 @@ use anyhow::{Context, Error};
 use tokio::{sync::mpsc::Sender, time::MissedTickBehavior};
 
 use proxmox_disks::Disks;
+use proxmox_iproute2::{IpLink, get_network_interfaces};
 use proxmox_log::{debug, error};
-use proxmox_network_api::IpLink;
 use proxmox_procfs::pressure::{PressureData, Resource};
 use proxmox_sys::fs;
 use proxmox_sys::linux::procfs;
@@ -170,8 +170,8 @@ fn collect_netdev_metrics() -> Result<NetDevStats, Error> {
         cache.replace({
             debug!("updating cached network devices");
 
-            let interfaces = proxmox_network_api::get_network_interfaces()
-                .context("failed to enumerate network devices")?;
+            let interfaces =
+                get_network_interfaces().context("failed to enumerate network devices")?;
 
             NetdevCacheEntry {
                 interfaces,
-- 
2.47.3





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

* [PATCH proxmox-firewall 07/13] firewall config: switch to proxmox-iproute2 crate
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (5 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-datacenter-manager 06/13] " Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-network-interface-pinning 08/13] network-interface-pinning: " Stefan Hanreich
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml                     | 2 +-
 proxmox-firewall/Cargo.toml    | 2 +-
 proxmox-firewall/src/config.rs | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 1dd2784..b44daae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,7 +29,7 @@ thiserror = "2"
 
 # proxmox dependencies
 proxmox-log = "1"
-proxmox-network-api = "1"
+proxmox-iproute2 = "0.1.0"
 proxmox-network-types = "1"
 proxmox-serde = "1"
 proxmox-sys = "1"
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 6ad7e79..edb497d 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -19,8 +19,8 @@ serde_json.workspace = true
 signal-hook.workspace = true
 
 proxmox-log.workspace = true
+proxmox-iproute2.workspace = true
 proxmox-network-types.workspace = true
-proxmox-network-api = { workspace = true, features = [ "impl" ] }
 proxmox-nftables = { workspace = true, features = [ "config-ext" ] }
 proxmox-ve-config.workspace = true
 
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 341e05a..57433a0 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -19,7 +19,7 @@ use proxmox_ve_config::guest::types::Vmid;
 use proxmox_ve_config::guest::{GuestEntry, GuestMap};
 use proxmox_ve_config::host::types::BridgeName;
 
-use proxmox_network_api::{AltnameMapping, get_network_interfaces};
+use proxmox_iproute2::{AltnameMapping, get_network_interfaces};
 use proxmox_nftables::NftClient;
 use proxmox_nftables::command::{CommandOutput, Commands, List, ListOutput};
 use proxmox_nftables::types::ListChain;
-- 
2.47.3





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

* [PATCH proxmox-network-interface-pinning 08/13] network-interface-pinning: switch to proxmox-iproute2 crate
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (6 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-firewall 07/13] firewall config: " Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-ve-rs 09/13] fabric: wireguard: add helper for findings peer based on endpoint Stefan Hanreich
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 Cargo.toml  |  1 +
 src/main.rs | 19 ++++++++-----------
 2 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 6e6f982..eddcba4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ walkdir = "2.5.0"
 
 proxmox-async = "0.5.0"
 proxmox-log = "1.0.0"
+proxmox-iproute2 = "0.1.0"
 proxmox-network-api = { version = "1.0.3", features = [ "impl" ] }
 proxmox-network-types = "1"
 proxmox-product-config = "1"
diff --git a/src/main.rs b/src/main.rs
index 3129432..004db8c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -51,10 +51,7 @@ impl InterfaceMapping {
     ///
     /// It uses [`ip_links`] to determine the MAC address of the interfaces that should be pinned,
     /// since we pin based on MAC addresses.
-    pub fn write(
-        &self,
-        ip_links: HashMap<String, proxmox_network_api::IpLink>,
-    ) -> Result<(), Error> {
+    pub fn write(&self, ip_links: HashMap<String, proxmox_iproute2::IpLink>) -> Result<(), Error> {
         if self.mapping.is_empty() {
             return Ok(());
         }
@@ -63,7 +60,7 @@ impl InterfaceMapping {
 
         std::fs::create_dir_all(SYSTEMD_LINK_FILE_PATH)?;
 
-        let mut sorted_links: Vec<&proxmox_network_api::IpLink> = ip_links.values().collect();
+        let mut sorted_links: Vec<&proxmox_iproute2::IpLink> = ip_links.values().collect();
         sorted_links.sort_by_key(|a| a.index());
 
         for ip_link in sorted_links {
@@ -323,10 +320,10 @@ impl Display for LinkFile {
 #[derive(Debug, Clone, Eq, PartialEq, Hash)]
 /// A wrapper struct for [`proxmox_network_api::IpLink`], that implements Ord by comparing
 /// ifindexes.
-struct IpLink(proxmox_network_api::IpLink);
+struct IpLink(proxmox_iproute2::IpLink);
 
 impl Deref for IpLink {
-    type Target = proxmox_network_api::IpLink;
+    type Target = proxmox_iproute2::IpLink;
 
     fn deref(&self) -> &Self::Target {
         &self.0
@@ -345,8 +342,8 @@ impl Ord for IpLink {
     }
 }
 
-impl From<proxmox_network_api::IpLink> for IpLink {
-    fn from(value: proxmox_network_api::IpLink) -> Self {
+impl From<proxmox_iproute2::IpLink> for IpLink {
+    fn from(value: proxmox_iproute2::IpLink) -> Self {
         Self(value)
     }
 }
@@ -355,7 +352,7 @@ impl From<proxmox_network_api::IpLink> for IpLink {
 ///
 /// It holds all information required for generating
 pub struct PinningTool {
-    ip_links: HashMap<String, proxmox_network_api::IpLink>,
+    ip_links: HashMap<String, proxmox_iproute2::IpLink>,
     pinned_interfaces: PinnedInterfaces,
     existing_names: HashSet<String>,
 }
@@ -395,7 +392,7 @@ impl PinningTool {
 
     /// Constructs a new instance of the pinning tool.
     pub fn new() -> Result<Self, Error> {
-        let ip_links = proxmox_network_api::get_network_interfaces()?;
+        let ip_links = proxmox_iproute2::get_network_interfaces()?;
         let pinned_interfaces: PinnedInterfaces = Self::read_link_files()?.into_iter().collect();
 
         let mut existing_names = HashSet::new();
-- 
2.47.3





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

* [PATCH proxmox-ve-rs 09/13] fabric: wireguard: add helper for findings peer based on endpoint
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (7 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-network-interface-pinning 08/13] network-interface-pinning: " Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH proxmox-perl-rs 10/13] sdn status: fabrics: add status reporting for wireguard Stefan Hanreich
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

This function will be used by the status reporting, which requires the
ability to match an entry from the dump output to the respective node
in the section config, in order to include the corresponding
node/interface in its informational output. This helps users matching
peers from the running WireGuard configuration to their respective
section config entry.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 115 +++++++++++++++++-
 .../section_config/protocol/wireguard.rs      |   8 ++
 2 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 22f19c7..d608266 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -7,6 +7,7 @@ use std::marker::PhantomData;
 use std::ops::Deref;
 
 use anyhow::Error;
+use proxmox_network_types::endpoint::ServiceEndpoint;
 use section_config::protocol::wireguard::WireGuardProperties;
 use serde::{Deserialize, Serialize};
 
@@ -34,8 +35,9 @@ use crate::sdn::fabric::section_config::protocol::ospf::{
     OspfNodePropertiesUpdater, OspfProperties, OspfPropertiesUpdater,
 };
 use crate::sdn::fabric::section_config::protocol::wireguard::{
-    WireGuardDeletableProperties, WireGuardNode, WireGuardNodeDeletableProperties,
-    WireGuardNodePeer, WireGuardNodeUpdater, WireGuardPropertiesUpdater,
+    WireGuardDeletableProperties, WireGuardInterfaceProperties, WireGuardNode,
+    WireGuardNodeDeletableProperties, WireGuardNodePeer, WireGuardNodeUpdater,
+    WireGuardPropertiesUpdater,
 };
 use crate::sdn::fabric::section_config::{FabricOrNode, Section};
 
@@ -215,6 +217,115 @@ impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
 impl_entry!(WireGuard, WireGuardProperties, WireGuardNode);
 impl_entry!(Bgp, BgpProperties, BgpNode);
 
+impl Entry<WireGuardProperties, WireGuardNode> {
+    /// Search for a node in the fabric based on its endpoint.
+    ///
+    /// Searches for the node in the fabric configuration that has the given endpoint on a specific
+    /// node. Mainly useful for mapping the `wg show` output to a node entry in the section config
+    /// via the specified endpoint.
+    pub fn find_node_and_interface_by_endpoint(
+        &self,
+        local_node_id: &NodeId,
+        endpoint: &ServiceEndpoint,
+    ) -> Result<Option<(&Node, Option<&WireGuardInterfaceProperties>)>, Error> {
+        let node = self.get_node(local_node_id)?;
+
+        let Node::WireGuard(wireguard_node) = node else {
+            anyhow::bail!("no wireguard node with id {local_node_id} found");
+        };
+
+        let WireGuardNode::Internal(internal_node) = wireguard_node.properties() else {
+            anyhow::bail!("wireguard node with id {local_node_id} is not an internal node");
+        };
+
+        for peer in internal_node.peers() {
+            if let Some(peer_endpoint) = peer.endpoint() {
+                if endpoint == peer_endpoint {
+                    let referenced_node = self.get_node(peer.node())?;
+
+                    return Ok(Some(match peer {
+                        WireGuardNodePeer::Internal(internal_peer) => {
+                            let referenced_wireguard_node =
+                                self.node_section(&internal_peer.node)?;
+
+                            let WireGuardNode::Internal(referenced_internal_node) =
+                                referenced_wireguard_node.properties()
+                            else {
+                                anyhow::bail!(
+                                    "referenced node {} is not a internal wireguard node",
+                                    internal_peer.node
+                                );
+                            };
+
+                            (
+                                referenced_node,
+                                Some(
+                                    referenced_internal_node
+                                        .interfaces()
+                                        .find(|interface| {
+                                            interface.name() == &internal_peer.node_iface
+                                        })
+                                        .ok_or_else(|| {
+                                            anyhow::anyhow!("referenced interface does not exist")
+                                        })?,
+                                ),
+                            )
+                        }
+                        WireGuardNodePeer::External(_) => (referenced_node, None),
+                    }));
+                }
+            } else {
+                let referenced_node = self.get_node(peer.node())?;
+
+                match peer {
+                    WireGuardNodePeer::Internal(internal_peer) => {
+                        let referenced_wireguard_node = self.node_section(&internal_peer.node)?;
+
+                        let WireGuardNode::Internal(referenced_internal_node) =
+                            referenced_wireguard_node.properties()
+                        else {
+                            anyhow::bail!(
+                                "referenced node {} is not a internal wireguard node",
+                                internal_peer.node
+                            );
+                        };
+
+                        let Some(ip_host) = &referenced_internal_node.endpoint else {
+                            continue;
+                        };
+
+                        for interface in internal_node.interfaces() {
+                            let node_endpoint =
+                                ServiceEndpoint::new(&ip_host.to_string(), interface.listen_port)?;
+
+                            if &node_endpoint == endpoint {
+                                return Ok(Some((referenced_node, Some(interface))));
+                            }
+                        }
+                    }
+                    WireGuardNodePeer::External(external_peer) => {
+                        let referenced_wireguard_node = self.node_section(&external_peer.node)?;
+
+                        let WireGuardNode::External(referenced_external_node) = referenced_wireguard_node.properties()
+                            else {
+                            anyhow::bail!(
+                                "referenced node {} is not an external wireguard node",
+                                external_peer.node
+                            );
+                        };
+
+                        if &referenced_external_node.endpoint == endpoint {
+                            return Ok(Some((referenced_node, None)));
+                        }
+                    }
+                }
+            }
+        }
+
+        return Ok(None);
+    }
+}
+
 /// All possible entries in a [`FabricConfig`].
 ///
 /// It utilizes the [`Entry`] struct to validate proper combinations of [`FabricSection`] and
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
index a2d8c6e..38cc8f0 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/wireguard.rs
@@ -488,6 +488,14 @@ impl WireGuardNodePeer {
         }
     }
 
+    /// Returns the endpoint override for this peer definition, if it exists.
+    pub fn endpoint(&self) -> Option<&ServiceEndpoint> {
+        match self {
+            WireGuardNodePeer::Internal(internal_peer) => internal_peer.endpoint.as_ref(),
+            WireGuardNodePeer::External(external_peer) => external_peer.endpoint.as_ref(),
+        }
+    }
+
     pub fn node_iface(&self) -> Option<&WireGuardInterfaceName> {
         match self {
             WireGuardNodePeer::Internal(internal_peer) => Some(&internal_peer.node_iface),
-- 
2.47.3





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

* [PATCH proxmox-perl-rs 10/13] sdn status: fabrics: add status reporting for wireguard
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (8 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-ve-rs 09/13] fabric: wireguard: add helper for findings peer based on endpoint Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH pve-network 11/13] api: fabric status: add schema for wireguard properties Stefan Hanreich
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Utilize the built-in `wg show` command to obtain the status of all
WireGuard interfaces that are configured on the node. Additionally,
utilize the status output from `ip link` to obtain additional
information about the state of the wireguard interfaces themselves.

In order to be able to match the interfaces / peers from the `wg show`
output to the entities in the fabrics configuration the endpoint is
used, since that is unique to the local host.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/Cargo.toml                  |   1 +
 pve-rs/src/bindings/sdn/fabrics.rs |  41 ++-
 pve-rs/src/sdn/status.rs           | 529 ++++++++++++++++++++++++++++-
 3 files changed, 562 insertions(+), 9 deletions(-)

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 5ae9082..8940b27 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -37,6 +37,7 @@ proxmox-config-digest = "1"
 proxmox-frr = { version = "0.5.1" }
 proxmox-http = { version = "1.0.2", features = ["client-sync", "client-trait"] }
 proxmox-http-error = "1"
+proxmox-iproute2 = "0.1.0"
 proxmox-log = "1"
 proxmox-network-types = "1.1.2"
 proxmox-notify = { version = "1", features = ["pve-context"] }
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index f96b6b1..e971327 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -14,6 +14,7 @@ pub mod pve_rs_sdn_fabrics {
 
     use anyhow::{Context, Error, format_err};
     use openssl::hash::{MessageDigest, hash};
+    use proxmox_iproute2::get_network_interfaces;
     use proxmox_ve_config::sdn::fabric::section_config::node::api::{Node, NodeUpdater};
     use serde::{Deserialize, Serialize};
 
@@ -953,7 +954,24 @@ pub mod pve_rs_sdn_fabrics {
                 )
                 .map(|v| v.into())
             }
-            FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())),
+            FabricEntry::WireGuard(fabric_entry) => {
+                let wg_dump = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "wg show all dump"])
+                        .output()?
+                        .stdout,
+                )?;
+
+                let node_name = proxmox_sys::nodename();
+                let current_node_id = NodeId::from_string(node_name.to_string())?;
+
+                status::wireguard::parse_wireguard_neighbors(
+                    &wg_dump,
+                    fabric_entry.node_section(&current_node_id)?,
+                    &fabric_entry,
+                )
+                .map(Into::into)
+            }
             FabricEntry::Bgp(_) => {
                 let bgp_neighbors_string = String::from_utf8(
                     Command::new("sh")
@@ -1031,7 +1049,26 @@ pub mod pve_rs_sdn_fabrics {
                 )
                 .map(|v| v.into())
             }
-            FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())),
+            FabricEntry::WireGuard(fabric_entry) => {
+                let wg_dump = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "wg show all dump"])
+                        .output()?
+                        .stdout,
+                )?;
+
+                let network_interfaces = get_network_interfaces()?;
+
+                let node_name = proxmox_sys::nodename();
+                let current_node_id = NodeId::from_string(node_name.to_string())?;
+
+                status::wireguard::parse_wireguard_interfaces(
+                    &wg_dump,
+                    fabric_entry.node_section(&current_node_id)?,
+                    &network_interfaces,
+                )
+                .map(Into::into)
+            }
             FabricEntry::Bgp(_) => {
                 let bgp_neighbors_string = String::from_utf8(
                     Command::new("sh")
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 7a1334d..38afab2 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -81,13 +81,265 @@ mod openfabric {
     }
 }
 
-mod wireguard {
+pub mod wireguard {
+    use std::{
+        collections::{HashMap, HashSet},
+        str::FromStr,
+    };
+
+    use anyhow::Context;
+    use proxmox_iproute2::{IpLink, LinkFlag};
+    use proxmox_ve_config::sdn::fabric::{
+        Entry,
+        section_config::{
+            node::NodeSection,
+            protocol::wireguard::{InternalWireGuardNode, WireGuardNode, WireGuardProperties},
+        },
+    };
+    use proxmox_wireguard::PublicKey;
     use serde::Serialize;
 
-    #[derive(Debug, Serialize)]
-    pub struct NeighborStatus;
-    #[derive(Debug, Serialize)]
-    pub struct InterfaceStatus;
+    #[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+    #[repr(transparent)]
+    pub struct PersistentKeepAliveStatus(
+        #[serde(skip_serializing_if = "Option::is_none")] Option<u16>,
+    );
+
+    impl FromStr for PersistentKeepAliveStatus {
+        type Err = anyhow::Error;
+
+        fn from_str(s: &str) -> Result<Self, Self::Err> {
+            if s == "off" {
+                return Ok(Self(None));
+            }
+
+            Ok(Self(Some(s.parse()?)))
+        }
+    }
+
+    #[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+    #[serde(rename_all = "kebab-case")]
+    pub struct NeighborStatus {
+        pub neighbor: String,
+        pub name: String,
+        pub interface: String,
+        pub public_key: PublicKey,
+        pub allowed_ips: String,
+        pub latest_handshake: u64,
+        pub bytes_rx: u64,
+        pub bytes_tx: u64,
+        pub persistent_keepalive: PersistentKeepAliveStatus,
+    }
+
+    #[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+    #[serde(rename_all = "kebab-case")]
+    pub struct InterfaceStatus {
+        pub name: String,
+        #[serde(rename = "type")]
+        pub ty: String,
+        pub state: String,
+        pub public_key: PublicKey,
+        pub listen_port: u16,
+    }
+
+    pub fn parse_wireguard_interfaces(
+        wg_dump_output: &str,
+        node_section: &NodeSection<WireGuardNode>,
+        network_interfaces: &HashMap<String, IpLink>,
+    ) -> Result<Vec<InterfaceStatus>, anyhow::Error> {
+        let WireGuardNode::Internal(wireguard_node) = node_section.properties() else {
+            anyhow::bail!("is not an internal node");
+        };
+
+        let mut interface_status = Vec::new();
+        let mut last_interface = None;
+
+        let interfaces: HashSet<&str> = wireguard_node
+            .interfaces()
+            .map(|interface| interface.name().as_ref())
+            .collect();
+
+        for line in wg_dump_output.lines() {
+            let mut parts = line.split_ascii_whitespace();
+
+            let interface = parts.next().ok_or_else(|| {
+                anyhow::anyhow!("could not read interface name from `wg dump` output")
+            })?;
+
+            if last_interface != Some(interface) && interfaces.contains(interface) {
+                // skip the private key
+                parts.next().ok_or_else(|| {
+                    anyhow::anyhow!("could not read private key from `wg dump` output")
+                })?;
+
+                let public_key = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read public key from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse public key from `wg dump` output.")?;
+
+                let listen_port = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read listen port from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse listen_port from `wg dump` output")?;
+
+                let state = if let Some(network_interface) = network_interfaces.get(interface) {
+                    let is_wireguard_interface = network_interface
+                        .linkinfo()
+                        .and_then(|link_info| link_info.info_kind())
+                        .map(|info_kind| info_kind == "wireguard")
+                        .unwrap_or_default();
+
+                    if !is_wireguard_interface {
+                        "error"
+                    } else if !network_interface.flags().any(|flag| *flag == LinkFlag::Up) {
+                        "down"
+                    } else {
+                        "up"
+                    }
+                } else {
+                    "error"
+                }
+                .to_string();
+
+                interface_status.push(InterfaceStatus {
+                    name: interface.to_string(),
+                    ty: "wireguard".to_string(),
+                    state,
+                    public_key,
+                    listen_port,
+                });
+
+                last_interface = Some(interface);
+            }
+        }
+
+        Ok(interface_status)
+    }
+
+    pub fn parse_wireguard_neighbors(
+        wg_dump_output: &str,
+        node_section: &NodeSection<WireGuardNode>,
+        fabric: &Entry<WireGuardProperties, WireGuardNode>,
+    ) -> Result<Vec<NeighborStatus>, anyhow::Error> {
+        let WireGuardNode::Internal(wireguard_node) = node_section.properties() else {
+            anyhow::bail!("is not an internal node");
+        };
+
+        let mut neighbors = Vec::new();
+        let mut last_interface = None;
+
+        let interfaces: HashSet<&str> = wireguard_node
+            .interfaces()
+            .map(|interface| interface.name().as_ref())
+            .collect();
+
+        for line in wg_dump_output.lines() {
+            let mut parts = line.split_ascii_whitespace();
+
+            let interface = parts.next().ok_or_else(|| {
+                anyhow::anyhow!("could not read interface name from `wg dump` output")
+            })?;
+
+            if last_interface == Some(interface) && interfaces.contains(interface) {
+                let public_key = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read public key from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse public key from `wg dump` output.")?;
+
+                // skip the preshared key
+                parts.next().ok_or_else(|| {
+                    anyhow::anyhow!("could not read private key from `wg dump` output")
+                })?;
+
+                let neighbor = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read neighbor from `wg dump` output")
+                    })?
+                    .to_string();
+
+                let allowed_ips = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read allowed ips from `wg dump` output")
+                    })?
+                    .to_string();
+
+                let latest_handshake = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read latest handshake from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse latest_handshake timestamp")?;
+
+                let bytes_rx = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read received bytes from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse received bytes")?;
+
+                let bytes_tx = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read transmitted bytes from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse transmitted bytes")?;
+
+                let persistent_keepalive = parts
+                    .next()
+                    .ok_or_else(|| {
+                        anyhow::anyhow!("could not read persistent_keepalive from `wg dump` output")
+                    })?
+                    .parse()
+                    .with_context(|| "could not parse persistent_keepalive interval")?;
+
+                let Some((node, node_interface)) = fabric.find_node_and_interface_by_endpoint(
+                    node_section.id().node_id(),
+                    &neighbor.parse()?,
+                )?
+                else {
+                    anyhow::bail!("can not find matching peer definition for endpoint {neighbor}");
+                };
+
+                let mut name = node.id().node_id().to_string();
+
+                if let Some(node_interface) = node_interface {
+                    name.push_str(" (");
+                    name.push_str(node_interface.name());
+                    name.push_str(")");
+                }
+
+                neighbors.push(NeighborStatus {
+                    neighbor,
+                    interface: interface.to_string(),
+                    public_key,
+                    name,
+                    allowed_ips,
+                    latest_handshake,
+                    bytes_tx,
+                    bytes_rx,
+                    persistent_keepalive,
+                });
+            } else {
+                last_interface = Some(interface);
+            }
+        }
+
+        Ok(neighbors)
+    }
 }
 
 mod bgp {
@@ -129,6 +381,11 @@ impl From<Vec<ospf::NeighborStatus>> for NeighborStatus {
         NeighborStatus::Ospf(value)
     }
 }
+impl From<Vec<wireguard::NeighborStatus>> for NeighborStatus {
+    fn from(value: Vec<wireguard::NeighborStatus>) -> Self {
+        NeighborStatus::WireGuard(value)
+    }
+}
 impl From<Vec<bgp::NeighborStatus>> for NeighborStatus {
     fn from(value: Vec<bgp::NeighborStatus>) -> Self {
         NeighborStatus::Bgp(value)
@@ -155,6 +412,11 @@ impl From<Vec<ospf::InterfaceStatus>> for InterfaceStatus {
         InterfaceStatus::Ospf(value)
     }
 }
+impl From<Vec<wireguard::InterfaceStatus>> for InterfaceStatus {
+    fn from(value: Vec<wireguard::InterfaceStatus>) -> Self {
+        InterfaceStatus::WireGuard(value)
+    }
+}
 impl From<Vec<bgp::InterfaceStatus>> for InterfaceStatus {
     fn from(value: Vec<bgp::InterfaceStatus>) -> Self {
         InterfaceStatus::Bgp(value)
@@ -687,9 +949,15 @@ pub fn get_l2vpn_routes(routes: de::evpn::Routes) -> Result<L2VPNRoutes, anyhow:
 
 #[cfg(test)]
 mod tests {
+    use std::str::FromStr;
+
     use super::*;
-    use proxmox_section_config::typed::SectionConfigData;
-    use proxmox_ve_config::sdn::fabric::FabricConfig;
+    use anyhow::Error;
+    use proxmox_iproute2::IpLink;
+    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::sdn::fabric::{
+        FabricConfig, FabricEntry, section_config::protocol::wireguard::WireGuardNode,
+    };
 
     fn sample_two_fabric_config() -> Valid<FabricConfig> {
         let raw_config = r#"{
@@ -2778,4 +3046,251 @@ mod tests {
             assert_eq!(reference, output);
         }
     }
+
+    #[test]
+    fn parse_wireguard_interfaces_neighbors() -> Result<(), Error> {
+        let wg_dump_output = r#"wg1	wF2D1my5Cj962/MOS2UXLvCddm3ozmCSSuBQ1Ey6v3I=	Um+Z4Ymq3r24txH2itVVtC86ra1PIrFs9AeMebESdWM=	51821	off
+wg1	aHnKVQ4yTtTdfwsk+r4Z12FrRlUnXXksjRcV7x41+G0=	(none)	172.31.1.10:51820	(none)	0	0	0	off
+wg0	GDHQ8ivFvwz53U0bSmQ7uTD2bJ8GpcFCCxGeU1G9m2U=	AQkUKUJXOxR/QH6fjZLx8GO1ZnnD8PueRYMJNOCzc2M=	51820	off
+wg0	+PA/xNCZ3G+Wy1DeqF251Us5mcZhBTGc9CT3AaJ89HI=	(none)	172.31.1.2:51820	198.51.100.2/32,203.0.113.0/28	0	0	0	off
+wg0	8ybxvVfiWqpBG160nMgH14acR3z31ZeceW7ita0zonA=	(none)	172.31.1.4:51820	198.51.100.4/32	0	0	0	off"#;
+
+        let ip_link_output = r#"[
+{
+  "ifindex": 25,
+  "ifname": "wg0",
+  "flags": [
+    "POINTOPOINT",
+    "NOARP",
+    "UP",
+    "LOWER_UP"
+  ],
+  "mtu": 1500,
+  "qdisc": "noqueue",
+  "operstate": "UNKNOWN",
+  "linkmode": "DEFAULT",
+  "group": "default",
+  "txqlen": 1000,
+  "link_type": "none",
+  "promiscuity": 0,
+  "allmulti": 0,
+  "min_mtu": 0,
+  "max_mtu": 2147483552,
+  "linkinfo": {
+    "info_kind": "wireguard"
+  },
+  "inet6_addr_gen_mode": "none",
+  "num_tx_queues": 1,
+  "num_rx_queues": 1,
+  "gso_max_size": 65536,
+  "gso_max_segs": 65535,
+  "tso_max_size": 524280,
+  "tso_max_segs": 65535,
+  "gro_max_size": 65536,
+  "gso_ipv4_max_size": 65536,
+  "gro_ipv4_max_size": 65536
+},
+{
+  "ifindex": 24,
+  "ifname": "wg1",
+  "flags": [
+    "POINTOPOINT",
+    "NOARP"
+  ],
+  "mtu": 1500,
+  "qdisc": "noqueue",
+  "operstate": "DOWN",
+  "linkmode": "DEFAULT",
+  "group": "default",
+  "txqlen": 1000,
+  "link_type": "none",
+  "promiscuity": 0,
+  "allmulti": 0,
+  "min_mtu": 0,
+  "max_mtu": 2147483552,
+  "linkinfo": {
+    "info_kind": "wireguard"
+  },
+  "inet6_addr_gen_mode": "none",
+  "num_tx_queues": 1,
+  "num_rx_queues": 1,
+  "gso_max_size": 65536,
+  "gso_max_segs": 65535,
+  "tso_max_size": 524280,
+  "tso_max_segs": 65535,
+  "gro_max_size": 65536,
+  "gso_ipv4_max_size": 65536,
+  "gro_ipv4_max_size": 65536
+}
+]"#;
+
+        let network_interfaces: HashMap<String, IpLink> =
+            serde_json::from_str::<Vec<IpLink>>(&ip_link_output)?
+                .into_iter()
+                .map(|ip_link| (ip_link.name().to_string(), ip_link))
+                .collect();
+
+        let raw_fabric_config = r#"wireguard_fabric: test
+
+wireguard_node: test_chronomancer
+        endpoint 172.31.1.4
+        interfaces name=wg1,listen_port=51821,public_key=+RAKruThY8Qx/oLKRoqYZ4DZJwd9/BO3lVVAR6INNTo=
+        interfaces name=wg0,listen_port=51820,public_key=8ybxvVfiWqpBG160nMgH14acR3z31ZeceW7ita0zonA=,ip=198.51.100.4/24
+        peers type=internal,node=elementalist,node_iface=wg0,iface=wg0
+        peers type=internal,node=occultist,node_iface=wg0,iface=wg0
+        peers type=external,node=stormweaver,iface=wg1
+        role internal
+
+wireguard_node: test_elementalist
+        endpoint 172.31.1.1
+        interfaces name=wg1,listen_port=51821,public_key=Um+Z4Ymq3r24txH2itVVtC86ra1PIrFs9AeMebESdWM=
+        interfaces name=wg0,listen_port=51820,public_key=AQkUKUJXOxR/QH6fjZLx8GO1ZnnD8PueRYMJNOCzc2M=,ip=198.51.100.1/24
+        peers type=internal,node=occultist,node_iface=wg0,iface=wg0
+        peers type=internal,node=chronomancer,node_iface=wg0,iface=wg0
+        peers type=external,node=stormweaver,iface=wg1
+        role internal
+
+wireguard_node: test_occultist
+        allowed_ips 203.0.113.0/28
+        endpoint 172.31.1.2
+        interfaces name=wg1,listen_port=51821,public_key=UBvDcsMICJLpy/+aRpJXFbDfU4eZrBeWnHimjzla/SI=
+        interfaces name=wg0,listen_port=51820,public_key=+PA/xNCZ3G+Wy1DeqF251Us5mcZhBTGc9CT3AaJ89HI=,ip=198.51.100.2/24
+        peers type=external,node=stormweaver,iface=wg1
+        peers type=internal,node=elementalist,node_iface=wg0,iface=wg0
+        peers type=internal,node=chronomancer,node_iface=wg0,iface=wg0
+        role internal
+
+wireguard_node: test_stormweaver
+        endpoint 172.31.1.10:51820
+        public_key aHnKVQ4yTtTdfwsk+r4Z12FrRlUnXXksjRcV7x41+G0=
+        role external"#;
+
+        let parsed_config = Section::parse_section_config("fabrics.cfg", raw_fabric_config)?;
+
+        let fabric_config = FabricConfig::from_section_config(parsed_config)
+            .expect("is a valid fabric configuration");
+
+        let fabric_entry = fabric_config
+            .get_fabric(&FabricId::from_str("test").expect("valid fabric id"))
+            .expect("fabric exists");
+
+        let ConfigNode::WireGuard(wireguard_node) = fabric_entry
+            .get_node(&NodeId::from_str("elementalist").expect("is a valid node id"))
+            .expect("node exists in fabric config")
+        else {
+            anyhow::bail!("is not a wireguard node");
+        };
+
+        let FabricEntry::WireGuard(wireguard_entry) = fabric_entry else {
+            anyhow::bail!("is not a wireguard fabric");
+        };
+
+        let reference = vec![
+            wireguard::InterfaceStatus {
+                name: "wg1".to_string(),
+                ty: "wireguard".to_string(),
+                state: "down".to_string(),
+                public_key: "Um+Z4Ymq3r24txH2itVVtC86ra1PIrFs9AeMebESdWM="
+                    .parse()
+                    .expect("valid public key"),
+                listen_port: 51821,
+            },
+            wireguard::InterfaceStatus {
+                name: "wg0".to_string(),
+                ty: "wireguard".to_string(),
+                state: "up".to_string(),
+                public_key: "AQkUKUJXOxR/QH6fjZLx8GO1ZnnD8PueRYMJNOCzc2M="
+                    .parse()
+                    .expect("valid public key"),
+                listen_port: 51820,
+            },
+        ];
+
+        assert_eq!(
+            reference,
+            wireguard::parse_wireguard_interfaces(
+                wg_dump_output,
+                &wireguard_node,
+                &network_interfaces
+            )
+            .expect("can parse wireguard output")
+        );
+
+        let reference = vec![
+            wireguard::InterfaceStatus {
+                name: "wg1".to_string(),
+                ty: "wireguard".to_string(),
+                state: "error".to_string(),
+                public_key: "Um+Z4Ymq3r24txH2itVVtC86ra1PIrFs9AeMebESdWM="
+                    .parse()
+                    .expect("valid public key"),
+                listen_port: 51821,
+            },
+            wireguard::InterfaceStatus {
+                name: "wg0".to_string(),
+                ty: "wireguard".to_string(),
+                state: "error".to_string(),
+                public_key: "AQkUKUJXOxR/QH6fjZLx8GO1ZnnD8PueRYMJNOCzc2M="
+                    .parse()
+                    .expect("valid public key"),
+                listen_port: 51820,
+            },
+        ];
+
+        assert_eq!(
+            reference,
+            wireguard::parse_wireguard_interfaces(wg_dump_output, &wireguard_node, &HashMap::new())
+                .expect("can parse wireguard output")
+        );
+
+        let reference = vec![
+            wireguard::NeighborStatus {
+                neighbor: "172.31.1.10:51820".to_string(),
+                name: "stormweaver".to_string(),
+                interface: "wg1".to_string(),
+                public_key: "aHnKVQ4yTtTdfwsk+r4Z12FrRlUnXXksjRcV7x41+G0="
+                    .parse()
+                    .expect("valid public key"),
+                allowed_ips: "(none)".to_string(),
+                latest_handshake: 0,
+                bytes_rx: 0,
+                bytes_tx: 0,
+                persistent_keepalive: "off".parse().expect("valid persistent keepalive value"),
+            },
+            wireguard::NeighborStatus {
+                neighbor: "172.31.1.2:51820".to_string(),
+                name: "occultist (wg0)".to_string(),
+                interface: "wg0".to_string(),
+                public_key: "+PA/xNCZ3G+Wy1DeqF251Us5mcZhBTGc9CT3AaJ89HI="
+                    .parse()
+                    .expect("valid public key"),
+                allowed_ips: "198.51.100.2/32,203.0.113.0/28".to_string(),
+                latest_handshake: 0,
+                bytes_rx: 0,
+                bytes_tx: 0,
+                persistent_keepalive: "off".parse().expect("valid persistent keepalive value"),
+            },
+            wireguard::NeighborStatus {
+                neighbor: "172.31.1.4:51820".to_string(),
+                name: "chronomancer (wg0)".to_string(),
+                interface: "wg0".to_string(),
+                public_key: "8ybxvVfiWqpBG160nMgH14acR3z31ZeceW7ita0zonA="
+                    .parse()
+                    .expect("valid public key"),
+                allowed_ips: "198.51.100.4/32".to_string(),
+                latest_handshake: 0,
+                bytes_rx: 0,
+                bytes_tx: 0,
+                persistent_keepalive: "off".parse().expect("valid persistent keepalive value"),
+            },
+        ];
+
+        assert_eq!(
+            reference,
+            wireguard::parse_wireguard_neighbors(wg_dump_output, &wireguard_node, wireguard_entry)
+                .expect("can parse wireguard output")
+        );
+
+        Ok(())
+    }
 }
-- 
2.47.3





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

* [PATCH pve-network 11/13] api: fabric status: add schema for wireguard properties
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (9 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH proxmox-perl-rs 10/13] sdn status: fabrics: add status reporting for wireguard Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH pve-manager 12/13] ui: fabric content: add wireguard protocol Stefan Hanreich
  2026-06-17 11:10 ` [PATCH pve-docs 13/13] sdn: add documentation for wireguard status reporting Stefan Hanreich
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

The WireGuard fabric returns lots of addtional options in status
reporting, add the properties to the return schema. Additionally,
improve the description of the existing properties, so the format is
more clear.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 71 +++++++++++++++++++++++-
 1 file changed, 68 insertions(+), 3 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
index 0202b81e..28aa2030 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
@@ -115,17 +115,71 @@ __PACKAGE__->register_method({
             type => "object",
             properties => {
                 neighbor => {
-                    description => "The IP or hostname of the neighbor.",
+                    description => "The IP or hostname of the neighbor (including the port for WireGuard).",
+                    format_description => '(<IP>|<Host>)[:<port>]',
                     type => 'string',
                 },
                 status => {
-                    description => "The status of the neighbor, as returned by FRR.",
+                    description => "The status of the neighbor, as returned by FRR (OSPF, Openfabric, BGP only)",
                     type => 'string',
+                    optional => 1,
                 },
                 uptime => {
                     description =>
-                        "The uptime of this neighbor, as returned by FRR (e.g. 8h24m12s).",
+                        "The uptime of this neighbor, as returned by FRR, e.g. 8h24m12s (OSPF, Openfabric, BGP only)",
                     type => 'string',
+                    optional => 1,
+                },
+                name => {
+                    description =>
+                        "The name for this neighbor in the WireGuard fabric. (WireGuard only)",
+                    format_description => '(<IP>|<Host>)[ (<interface>)]',
+                    type => 'string',
+                    optional => 1,
+                },
+                interface => {
+                    description =>
+                        "The name of the interface that uses this neighbor as peer (WireGuard only)",
+                    type => 'string',
+                    optional => 1,
+                },
+                'public-key' => {
+                    description =>
+                        "The public key used for this neighbor (WireGuard only)",
+                    format_description => 'Base64-encoded Curve 25519 public key.',
+                    type => 'string',
+                    optional => 1,
+                },
+                'allowed-ips' => {
+                    description =>
+                        "The allowed IPs for this neighbor (WireGuard only)",
+                    type => 'string',
+                    format_description => 'Comma-separated list of CIDRs.',
+                    optional => 1,
+                },
+                'latest-handshake' => {
+                    description =>
+                        "UNIX Timestamp of the last successful handshake (WireGuard only)",
+                    type => 'number',
+                    optional => 1,
+                },
+                'bytes-rx' => {
+                    description =>
+                        "The amount of bytes received from this neighbor (WireGuard only)",
+                    type => 'number',
+                    optional => 1,
+                },
+                'bytes-tx' => {
+                    description =>
+                        "The amount of bytes transmitted to this neighbor (WireGuard only)",
+                    type => 'number',
+                    optional => 1,
+                },
+                'persistent-keepalive' => {
+                    description =>
+                        "The currently used persistent keepalive interval for this neighbor (WireGuard only)",
+                    type => 'number',
+                    optional => 1,
                 },
             },
         },
@@ -173,6 +227,17 @@ __PACKAGE__->register_method({
                     description => "The current state of the interface.",
                     type => 'string',
                 },
+                'public-key' => {
+                    description => "The public key of the interface (WireGuard only).",
+                    format_description => 'Base64-encoded Curve 25519 public key.',
+                    type => 'string',
+                    optional => 1,
+                },
+                'listen-port' => {
+                    description => "The listen port of the interface (WireGuard only).",
+                    type => 'number',
+                    optional => 1,
+                },
             },
         },
     },
-- 
2.47.3





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

* [PATCH pve-manager 12/13] ui: fabric content: add wireguard protocol
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (10 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH pve-network 11/13] api: fabric status: add schema for wireguard properties Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  2026-06-17 11:10 ` [PATCH pve-docs 13/13] sdn: add documentation for wireguard status reporting Stefan Hanreich
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

WireGuard returns a lot of additional options compared to the other,
FRR-based, fabric types. Hide the routes panel completely and change
the neighbor / interface panel to add the additional columns if the
fabric has the type wireguard.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/FabricsContentView.js | 173 +++++++++++++++++++------
 www/manager6/sdn/NetworkBrowser.js     |  40 +++---
 2 files changed, 154 insertions(+), 59 deletions(-)

diff --git a/www/manager6/sdn/FabricsContentView.js b/www/manager6/sdn/FabricsContentView.js
index 47e8bce7f..ebed8eea5 100644
--- a/www/manager6/sdn/FabricsContentView.js
+++ b/www/manager6/sdn/FabricsContentView.js
@@ -28,50 +28,143 @@ Ext.define('PVE.sdn.FabricNeighborsContentView', {
     extend: 'Ext.grid.GridPanel',
     alias: 'widget.pveSDNFabricNeighborsContentView',
 
-    columns: [
-        {
-            header: gettext('Neighbor'),
-            sortable: true,
-            dataIndex: 'neighbor',
-            flex: 1,
-        },
-        {
-            header: gettext('Status'),
-            sortable: true,
-            dataIndex: 'status',
-            flex: 0.5,
-        },
-        {
-            header: gettext('Uptime'),
-            sortable: true,
-            dataIndex: 'uptime',
-            flex: 0.5,
-        },
-    ],
+    protocol: null,
+
+    initComponent: function() {
+        let me = this;
+
+        me.columns = [
+            {
+                header: gettext('Neighbor'),
+                sortable: true,
+                dataIndex: 'neighbor',
+                flex: 1,
+            },
+        ];
+
+        if (me.protocol === 'wireguard') {
+            me.columns.unshift({
+                header: gettext('Name'),
+                sortable: true,
+                dataIndex: 'name',
+                flex: 1,
+            });
+
+            me.columns = me.columns.concat([
+                {
+                    header: gettext('Interface'),
+                    sortable: true,
+                    dataIndex: 'interface',
+                    flex: 0.5,
+                },
+                {
+                    header: gettext('Public Key'),
+                    sortable: true,
+                    dataIndex: 'public-key',
+                    flex: 1,
+                },
+                {
+                    header: gettext('Allowed IPs'),
+                    sortable: true,
+                    dataIndex: 'allowed-ips',
+                    flex: 1,
+                },
+                {
+                    header: gettext('Latest Handshake'),
+                    sortable: true,
+                    dataIndex: 'latest-handshake',
+                    flex: 1,
+                    renderer: function(value) {
+                        if (!value) {
+                            return "-"
+                        }
+
+                        return Proxmox.Utils.render_timestamp(value);
+                    }
+                },
+                {
+                    header: gettext('Bytes transmitted'),
+                    sortable: true,
+                    dataIndex: 'bytes-tx',
+                    flex: 0.5,
+                    renderer: Proxmox.Utils.render_size,
+                },
+                {
+                    header: gettext('Bytes received'),
+                    sortable: true,
+                    dataIndex: 'bytes-rx',
+                    flex: 0.5,
+                    renderer: Proxmox.Utils.render_size,
+                },
+            ]);
+        } else {
+            me.columns = me.columns.concat([
+                {
+                    header: gettext('Status'),
+                    sortable: true,
+                    dataIndex: 'status',
+                    flex: 0.5,
+                },
+                {
+                    header: gettext('Uptime'),
+                    sortable: true,
+                    dataIndex: 'uptime',
+                    flex: 0.5,
+                },
+            ]);
+        }
+
+        me.callParent();
+    }
 });
 
 Ext.define('PVE.sdn.FabricInterfacesContentView', {
     extend: 'Ext.grid.GridPanel',
     alias: 'widget.pveSDNFabricInterfacesContentView',
 
-    columns: [
-        {
-            header: gettext('Name'),
-            sortable: true,
-            dataIndex: 'name',
-            flex: 1,
-        },
-        {
-            header: gettext('Type'),
-            sortable: true,
-            dataIndex: 'type',
-            flex: 1,
-        },
-        {
-            header: gettext('State'),
-            sortable: true,
-            dataIndex: 'state',
-            flex: 1,
-        },
-    ],
+    protocol: null,
+
+    initComponent: function() {
+        let me = this;
+
+        me.columns = [
+            {
+                header: gettext('Name'),
+                sortable: true,
+                dataIndex: 'name',
+                flex: 1,
+            },
+            {
+                header: gettext('Type'),
+                sortable: true,
+                dataIndex: 'type',
+                flex: 1,
+            },
+            {
+                header: gettext('State'),
+                sortable: true,
+                dataIndex: 'state',
+                flex: 1,
+            },
+        ];
+
+        if (me.protocol === 'wireguard') {
+            me.columns = me.columns.concat([
+                {
+                    header: gettext('Public Key'),
+                    sortable: true,
+                    dataIndex: 'public-key',
+                    flex: 1,
+                },
+                {
+                    header: gettext('Listen Port'),
+                    sortable: true,
+                    dataIndex: 'listen-port',
+                    flex: 1,
+                },
+            ]);
+        }
+
+        me.callParent();
+    }
 });
diff --git a/www/manager6/sdn/NetworkBrowser.js b/www/manager6/sdn/NetworkBrowser.js
index f94b27d35..edb00fb38 100644
--- a/www/manager6/sdn/NetworkBrowser.js
+++ b/www/manager6/sdn/NetworkBrowser.js
@@ -26,27 +26,29 @@ Ext.define('PVE.network.Browser', {
         if (networkType === 'fabric') {
             me.onlineHelp = 'pvesdn_config_fabrics';
 
-            me.items.push({
-                nodename: node,
-                fabricId: name,
-                protocol: me.pveSelNode.data.protocol,
-                xtype: 'pveSDNFabricRoutesContentView',
-                title: gettext('Routes'),
-                iconCls: 'fa fa-exchange',
-                itemId: 'routes',
-                width: '100%',
-                store: {
-                    proxy: {
-                        type: 'proxmox',
-                        url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/routes`,
-                        reader: {
-                            type: 'json',
-                            rootProperty: 'data',
+            if (me.pveSelNode.data.protocol !== 'wireguard') {
+                me.items.push({
+                    nodename: node,
+                    fabricId: name,
+                    protocol: me.pveSelNode.data.protocol,
+                    xtype: 'pveSDNFabricRoutesContentView',
+                    title: gettext('Routes'),
+                    iconCls: 'fa fa-exchange',
+                    itemId: 'routes',
+                    width: '100%',
+                    store: {
+                        proxy: {
+                            type: 'proxmox',
+                            url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/routes`,
+                            reader: {
+                                type: 'json',
+                                rootProperty: 'data',
+                            },
                         },
+                        autoLoad: true,
                     },
-                    autoLoad: true,
-                },
-            });
+                });
+            }
 
             me.items.push({
                 nodename: node,
-- 
2.47.3





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

* [PATCH pve-docs 13/13] sdn: add documentation for wireguard status reporting
  2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
                   ` (11 preceding siblings ...)
  2026-06-17 11:10 ` [PATCH pve-manager 12/13] ui: fabric content: add wireguard protocol Stefan Hanreich
@ 2026-06-17 11:10 ` Stefan Hanreich
  12 siblings, 0 replies; 14+ messages in thread
From: Stefan Hanreich @ 2026-06-17 11:10 UTC (permalink / raw)
  To: pve-devel

Explains the content of the returned fields and how to interpret them,
particularly how the status of an interface is determined.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-gui.adoc |  1 +
 pvesdn.adoc  | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/pve-gui.adoc b/pve-gui.adoc
index 3a99eaf..dce5e26 100644
--- a/pve-gui.adoc
+++ b/pve-gui.adoc
@@ -149,6 +149,7 @@ Line Height :: Specify the absolute height of a line.
 
 
 
+[[gui_resource_tree]]
 Resource Tree
 ~~~~~~~~~~~~~
 
diff --git a/pvesdn.adoc b/pvesdn.adoc
index a09a443..edcb539 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -957,6 +957,70 @@ Skip Route Generation:: The fabric will autogenerate routes in the kernel
 routing table for all allowed IPs of a peer. By setting this option, no routes
 will be inserted into the kernel routing table.
 
+
+[[pvesdn_fabric_status]]
+Status Reporting
+~~~~~~~~~~~~~~~~
+
+Fabrics report their current status for informational and debugging purposes. It
+can be accessed by clicking on the respective entry for the fabric in the
+xref:gui_resource_tree[Resource Tree]. Depending on the protocol, different
+metrics are reported.
+
+
+[[pvesdn_fabric_status_wireguard]]
+WireGuard
+^^^^^^^^^
+
+.Interfaces
+
+Provides information about the WireGuard interfaces of a specific node. The
+following properties are available:
+
+Name:: The name of the network interface on the host.
+
+Type:: Interfaces are always of type 'wireguard'.
+
+State:: The SDN stack performs several checks to determine the state of the
+interface. Possible states are: 'up', 'down', 'error'. An interface is up if the
+kernel interface exists and has the `UP` flag set. Otherwise, if the interface
+exists, but does not have the `UP` flag set, then it is considered 'down'. In
+all other cases (e.g. interface does not exist on the node or has the wrong
+type) the 'error' status is returned.
+
+Public Key:: The public key that can be used for communicating with the
+WireGuard instance on this interface.
+
+Listen Port:: The port that can be used to reach this WireGuard instance.
+
+.Neighbors
+
+Provides information about the peers of all WireGuard interfaces on a specific
+node. The following properties are available:
+
+Name:: The name of the node in the fabric configuration. Additionally contains
+the name of the referenced interface in parentheses, if the node is in an
+internal node.
+
+Neighbor:: The endpoint used to reach this peer (e.g. 192.0.2.0:51820).
+
+Interface:: The interface that has this neighbor configured.
+
+Public Key:: The public key of the neighbor.
+
+Allowed IPs:: A comma separated list of CIDRs. Those indicate the destination
+IPs that are allowed when sending traffic to this neighbor.
+
+Latest Handshake:: When the latest handshake with this neighbor occurred.
+Handshakes are exchanged every 180 seconds, *if* traffic is sent via this
+WireGuard interface. If no traffic is sent, then also no handshake is exchanged.
+
+Bytes transmitted:: The amount of traffic that has been sent to this neighbor.
+
+Bytes received:: The amount of traffic that has been received from this
+neighbor.
+
+
 [[pvesdn_config_route_filtering]]
 Prefix Lists and Route Maps
 ---------------------------
-- 
2.47.3





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

end of thread, other threads:[~2026-06-17 11:12 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-17 11:09 [PATCH docs/manager/network/proxmox{,-backup,-datacenter-manager,-firewall,-network-interface-pinning,-ve-rs,-perl-rs} 00/13] Status reporting for wireguard fabrics Stefan Hanreich
2026-06-17 11:09 ` [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema Stefan Hanreich
2026-06-17 11:09 ` [PATCH proxmox 02/13] iproute2: add missing getters Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox 03/13] iproute2: add support for parsing interface flags Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox 04/13] wireguard: derive additional traits for public key Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-backup 05/13] metric_collection: switch to proxmox-iproute2 crate Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-datacenter-manager 06/13] " Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-firewall 07/13] firewall config: " Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-network-interface-pinning 08/13] network-interface-pinning: " Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-ve-rs 09/13] fabric: wireguard: add helper for findings peer based on endpoint Stefan Hanreich
2026-06-17 11:10 ` [PATCH proxmox-perl-rs 10/13] sdn status: fabrics: add status reporting for wireguard Stefan Hanreich
2026-06-17 11:10 ` [PATCH pve-network 11/13] api: fabric status: add schema for wireguard properties Stefan Hanreich
2026-06-17 11:10 ` [PATCH pve-manager 12/13] ui: fabric content: add wireguard protocol Stefan Hanreich
2026-06-17 11:10 ` [PATCH pve-docs 13/13] sdn: add documentation for wireguard status reporting Stefan Hanreich

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