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