public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox 01/13] iproute2: schema: move iproute2 helpers to new create / schema
Date: Wed, 17 Jun 2026 13:09:58 +0200	[thread overview]
Message-ID: <20260617111012.312710-2-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260617111012.312710-1-s.hanreich@proxmox.com>

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





  reply	other threads:[~2026-06-17 11:11 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 ` Stefan Hanreich [this message]
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

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-2-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 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