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
next prev parent 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