From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-perl-rs 10/13] sdn status: fabrics: add status reporting for wireguard
Date: Wed, 17 Jun 2026 13:10:07 +0200 [thread overview]
Message-ID: <20260617111012.312710-11-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260617111012.312710-1-s.hanreich@proxmox.com>
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(¤t_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(¤t_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
next prev parent reply other threads:[~2026-06-17 11:12 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Stefan Hanreich [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260617111012.312710-11-s.hanreich@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.