From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 16C101FF14F for ; Wed, 17 Jun 2026 13:11:15 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1D374309CC; Wed, 17 Jun 2026 13:10:52 +0200 (CEST) From: Stefan Hanreich 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 Message-ID: <20260617111012.312710-2-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260617111012.312710-1-s.hanreich@proxmox.com> References: <20260617111012.312710-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1781694568422 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.596 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 7OCS5WPOSHQ6P4BTEPBI7OCAYWRM4QYF X-Message-ID-Hash: 7OCS5WPOSHQ6P4BTEPBI7OCAYWRM4QYF X-MailFrom: s.hanreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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 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 , + rustc:native (>= 1.85) , + libstd-rust-dev , + 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 +Maintainer: Proxmox Support Team +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 " + +[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, +} + +/// 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, + info_kind: Option, +} + +/// 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, + ifindex: i64, + #[serde(flatten)] + link_type: Link, + linkinfo: Option, + 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 { + 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 { + 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, +} + +impl std::ops::Deref for AltnameMapping { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.mapping + } +} + +impl FromIterator for AltnameMapping { + fn from_iter>(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, 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::>(&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::(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::(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::(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 , libstd-rust-dev , 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-~~) , @@ -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, 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, -} - -/// 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, - info_kind: Option, -} - -/// 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, - ifindex: i64, - #[serde(flatten)] - link_type: Link, - linkinfo: Option, - 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 { - 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 { - 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, -} - -impl std::ops::Deref for AltnameMapping { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.mapping - } -} - -impl FromIterator for AltnameMapping { - fn from_iter>(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, 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::>(&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 { 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::(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::(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::(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 = - LazyLock::new(|| Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap()); static VLAN_INTERFACE_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^(?P\S+)\.(?P\d+)|vlan(?P\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