From a48107078f66ffa7d2b0401d3fc5bcf7e0492064 Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Fri, 25 Jul 2025 17:35:36 +0300 Subject: [PATCH 1/5] ip-link: Adds support for common details Now supporting: promiscuity allmulti min_mtu max_mtu inet6_addr_gen_mode num_tx_queues num_rx_queues gso_max_size gso_max_segs tso_max_size tso_max_segs gro_max_size Also adds skeleton for specific details --- src/ip/link/cli.rs | 4 +- src/ip/link/show.rs | 258 +++++++++++++++++++++++++++++++++++++++++++- src/ip/main.rs | 7 ++ 3 files changed, 265 insertions(+), 4 deletions(-) diff --git a/src/ip/link/cli.rs b/src/ip/link/cli.rs index a24325b..8b604ec 100644 --- a/src/ip/link/cli.rs +++ b/src/ip/link/cli.rs @@ -53,9 +53,9 @@ impl LinkCommand { .unwrap_or_default() .map(String::as_str) .collect(); - handle_show(&opts).await + handle_show(&opts, matches.get_flag("DETAILS")).await } else { - handle_show(&[]).await + handle_show(&[], matches.get_flag("DETAILS")).await } } } diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index d2434dc..457863f 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -3,7 +3,12 @@ use std::collections::HashMap; use futures_util::stream::TryStreamExt; -use rtnetlink::packet_route::link::{LinkAttribute, LinkMessage}; +use rtnetlink::{ + packet_core::Nla as _, + packet_route::link::{ + AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkLayerType, LinkMessage, + }, +}; use serde::Serialize; use super::flags::link_flags_to_string; @@ -11,6 +16,217 @@ use iproute_rs::{ CanDisplay, CanOutput, CliColor, CliError, mac_to_string, write_with_color, }; +// Use constants until support is added to netlink-packet-route +const IFLA_GRO_MAX_SIZE: u16 = 58; +const IFLA_TSO_MAX_SIZE: u16 = 59; +const IFLA_TSO_MAX_SEGS: u16 = 60; +const IFLA_ALLMULTI: u16 = 61; + +#[derive(Serialize)] +#[serde(untagged)] +enum CliLinkTypeDetails {} + +impl CliLinkTypeDetails { + fn new(link_type: LinkLayerType, nl_attrs: &[LinkAttribute]) -> Self { + match link_type { + LinkLayerType::Loopback => todo!(), + LinkLayerType::Ether => todo!(), + LinkLayerType::Netrom => todo!(), + LinkLayerType::Eether => todo!(), + LinkLayerType::Ax25 => todo!(), + LinkLayerType::Pronet => todo!(), + LinkLayerType::Chaos => todo!(), + LinkLayerType::Ieee802 => todo!(), + LinkLayerType::Arcnet => todo!(), + LinkLayerType::Appletlk => todo!(), + LinkLayerType::Dlci => todo!(), + LinkLayerType::Atm => todo!(), + LinkLayerType::Metricom => todo!(), + LinkLayerType::Ieee1394 => todo!(), + LinkLayerType::Eui64 => todo!(), + LinkLayerType::Infiniband => todo!(), + LinkLayerType::Slip => todo!(), + LinkLayerType::Cslip => todo!(), + LinkLayerType::Slip6 => todo!(), + LinkLayerType::Cslip6 => todo!(), + LinkLayerType::Rsrvd => todo!(), + LinkLayerType::Adapt => todo!(), + LinkLayerType::Rose => todo!(), + LinkLayerType::X25 => todo!(), + LinkLayerType::Hwx25 => todo!(), + LinkLayerType::Can => todo!(), + LinkLayerType::Ppp => todo!(), + LinkLayerType::Hdlc => todo!(), + LinkLayerType::Lapb => todo!(), + LinkLayerType::Ddcmp => todo!(), + LinkLayerType::Rawhdlc => todo!(), + LinkLayerType::Rawip => todo!(), + LinkLayerType::Tunnel => todo!(), + LinkLayerType::Tunnel6 => todo!(), + LinkLayerType::Frad => todo!(), + LinkLayerType::Skip => todo!(), + LinkLayerType::Localtlk => todo!(), + LinkLayerType::Fddi => todo!(), + LinkLayerType::Bif => todo!(), + LinkLayerType::Sit => todo!(), + LinkLayerType::Ipddp => todo!(), + LinkLayerType::Ipgre => todo!(), + LinkLayerType::Pimreg => todo!(), + LinkLayerType::Hippi => todo!(), + LinkLayerType::Ash => todo!(), + LinkLayerType::Econet => todo!(), + LinkLayerType::Irda => todo!(), + LinkLayerType::Fcpp => todo!(), + LinkLayerType::Fcal => todo!(), + LinkLayerType::Fcpl => todo!(), + LinkLayerType::Fcfabric => todo!(), + LinkLayerType::Ieee802Tr => todo!(), + LinkLayerType::Ieee80211 => todo!(), + LinkLayerType::Ieee80211Prism => todo!(), + LinkLayerType::Ieee80211Radiotap => todo!(), + LinkLayerType::Ieee802154 => todo!(), + LinkLayerType::Ieee802154Monitor => todo!(), + LinkLayerType::Phonet => todo!(), + LinkLayerType::PhonetPipe => todo!(), + LinkLayerType::Caif => todo!(), + LinkLayerType::Ip6gre => todo!(), + LinkLayerType::Netlink => todo!(), + LinkLayerType::Sixlowpan => todo!(), + LinkLayerType::Vsockmon => todo!(), + LinkLayerType::Void => todo!(), + LinkLayerType::None => todo!(), + _ => todo!(), + } + } +} + +impl std::fmt::Display for CliLinkTypeDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +#[derive(Serialize)] +pub(crate) struct CliLinkInfoDetails { + promiscuity: u32, + allmulti: u32, + min_mtu: u32, + max_mtu: u32, + #[serde(skip_serializing_if = "String::is_empty")] + inet6_addr_gen_mode: String, + num_tx_queues: u32, + num_rx_queues: u32, + gso_max_size: u32, + gso_max_segs: u32, + tso_max_size: u32, + tso_max_segs: u32, + gro_max_size: u32, + #[serde(flatten)] + link_type_details: CliLinkTypeDetails, +} + +impl CliLinkInfoDetails { + fn new_with_type( + link_type: LinkLayerType, + nl_attrs: &[LinkAttribute], + ) -> Self { + let link_type_details = CliLinkTypeDetails::new(link_type, nl_attrs); + + let mut promiscuity = 0; + let mut allmulti = 0; + let mut min_mtu = 0; + let mut max_mtu = 0; + let mut num_tx_queues = 0; + let mut num_rx_queues = 0; + let mut gso_max_size = 0; + let mut gso_max_segs = 0; + let mut tso_max_size = 0; + let mut tso_max_segs = 0; + let mut gro_max_size = 0; + let mut inet6_addr_gen_mode = String::new(); + + for nl_attr in nl_attrs { + match nl_attr { + LinkAttribute::Promiscuity(p) => promiscuity = *p, + LinkAttribute::MinMtu(m) => min_mtu = *m, + LinkAttribute::MaxMtu(m) => max_mtu = *m, + LinkAttribute::AfSpecUnspec(a) => { + inet6_addr_gen_mode = get_addr_gen_mode(a) + } + LinkAttribute::NumTxQueues(n) => num_tx_queues = *n, + LinkAttribute::NumRxQueues(n) => num_rx_queues = *n, + LinkAttribute::GsoMaxSize(g) => gso_max_size = *g, + LinkAttribute::GsoMaxSegs(g) => gso_max_segs = *g, + LinkAttribute::Other(default_nla) => match default_nla.kind() { + IFLA_GRO_MAX_SIZE => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + gro_max_size = u32::from_ne_bytes(val); + } + IFLA_TSO_MAX_SIZE => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + tso_max_size = u32::from_ne_bytes(val); + } + IFLA_TSO_MAX_SEGS => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + tso_max_segs = u32::from_ne_bytes(val); + } + IFLA_ALLMULTI => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + allmulti = u32::from_ne_bytes(val); + } + _ => { /* println!("Remains {:?}", default_nla); */ } + }, + _ => { + // println!("Remains {:?}", nl_attr); + } + } + } + + Self { + promiscuity, + allmulti, + min_mtu, + max_mtu, + inet6_addr_gen_mode, + num_tx_queues, + num_rx_queues, + gso_max_size, + gso_max_segs, + link_type_details, + tso_max_size, + tso_max_segs, + gro_max_size, + } + } +} + +impl std::fmt::Display for CliLinkInfoDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + " promiscuity {} allmulti {} minmtu {} maxmtu {} addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} tso_max_size {} tso_max_segs {} gro_max_size {} {}", + self.promiscuity, + self.allmulti, + self.min_mtu, + self.max_mtu, + self.inet6_addr_gen_mode, + self.num_tx_queues, + self.num_rx_queues, + self.gso_max_size, + self.gso_max_segs, + self.tso_max_size, + self.tso_max_segs, + self.gro_max_size, + self.link_type_details + )?; + Ok(()) + } +} + #[derive(Serialize, Default)] pub(crate) struct CliLinkInfo { ifindex: u32, @@ -32,6 +248,9 @@ pub(crate) struct CliLinkInfo { address: String, #[serde(skip_serializing_if = "String::is_empty")] broadcast: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(flatten)] + details: Option, } impl std::fmt::Display for CliLinkInfo { @@ -67,6 +286,10 @@ impl std::fmt::Display for CliLinkInfo { write!(f, " brd ")?; write_with_color!(f, CliColor::Mac, "{}", self.broadcast)?; } + + if let Some(details) = &self.details { + write!(f, "{details}",)?; + } Ok(()) } } @@ -81,6 +304,7 @@ impl CanOutput for CliLinkInfo {} pub(crate) async fn handle_show( _opts: &[&str], + include_details: bool, ) -> Result, CliError> { let (connection, handle, _) = rtnetlink::new_connection()?; @@ -98,7 +322,7 @@ pub(crate) async fn handle_show( let mut ifaces: Vec = Vec::new(); while let Some(nl_msg) = links.try_next().await? { - ifaces.push(parse_nl_msg_to_iface(nl_msg)?); + ifaces.push(parse_nl_msg_to_iface(nl_msg, include_details)?); } resolve_controller_name(&mut ifaces); @@ -108,6 +332,7 @@ pub(crate) async fn handle_show( pub(crate) fn parse_nl_msg_to_iface( nl_msg: LinkMessage, + include_details: bool, ) -> Result { let mut ret = CliLinkInfo { ifindex: nl_msg.header.index, @@ -116,6 +341,11 @@ pub(crate) fn parse_nl_msg_to_iface( ..Default::default() }; + ret.details = include_details.then_some(CliLinkInfoDetails::new_with_type( + nl_msg.header.link_layer_type, + &nl_msg.attributes, + )); + for nl_attr in nl_msg.attributes { match nl_attr { LinkAttribute::IfName(name) => ret.ifname = name, @@ -144,9 +374,33 @@ pub(crate) fn parse_nl_msg_to_iface( } } } + Ok(ret) } +fn get_addr_gen_mode(af_spec_unspec: &[AfSpecUnspec]) -> String { + af_spec_unspec + .iter() + .filter_map(|s| { + let AfSpecUnspec::Inet6(v) = s else { + return None; + }; + v.iter() + .filter_map(|i| { + if let AfSpecInet6::AddrGenMode(mode) = i { + Some(mode) + } else { + None + } + }) + .next() + }) + .next() + .copied() + .unwrap_or_default() + .to_string() +} + fn resolve_ip_link_group_name(id: u32) -> String { // TODO: Read `/usr/share/iproute2/group` and `/etc/iproute2/group` match id { diff --git a/src/ip/main.rs b/src/ip/main.rs index bf38625..cc35755 100644 --- a/src/ip/main.rs +++ b/src/ip/main.rs @@ -47,6 +47,13 @@ async fn main() -> Result<(), CliError> { .action(clap::ArgAction::SetTrue) .global(true), ) + .arg( + clap::Arg::new("DETAILS") + .short('d') + .help("Interface details") + .action(clap::ArgAction::SetTrue) + .global(true), + ) .subcommand_required(true) .subcommand(LinkCommand::gen_command()); From 688498c74ef1522704980e84e1b622205f17e301 Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Sat, 26 Jul 2025 17:26:47 +0300 Subject: [PATCH 2/5] ip-link: Support link-kind details Also adds support for parentbus and parentdev fields. --- src/ip/link/show.rs | 210 +++++++++++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 83 deletions(-) diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index 457863f..7909a9e 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -1,8 +1,12 @@ // SPDX-License-Identifier: MIT use std::collections::HashMap; +use std::ffi::CStr; use futures_util::stream::TryStreamExt; +use rtnetlink::packet_core::DefaultNla; +use rtnetlink::packet_route::link::InfoData; +use rtnetlink::packet_route::link::LinkInfo; use rtnetlink::{ packet_core::Nla as _, packet_route::link::{ @@ -17,91 +21,84 @@ use iproute_rs::{ }; // Use constants until support is added to netlink-packet-route +const IFLA_PARENT_DEV_NAME: u16 = 56; +const IFLA_PARENT_DEV_BUS_NAME: u16 = 57; const IFLA_GRO_MAX_SIZE: u16 = 58; const IFLA_TSO_MAX_SIZE: u16 = 59; const IFLA_TSO_MAX_SEGS: u16 = 60; const IFLA_ALLMULTI: u16 = 61; +fn default_nla_to_string(default_nla: &DefaultNla) -> String { + let val_len = default_nla.value_len(); + let mut val = vec![0u8; val_len]; + default_nla.emit_value(&mut val); + CStr::from_bytes_with_nul(&val) + .expect("String nla to be nul-terminated and not contain interior nuls") + .to_str() + .expect("To be valid UTF-8") + .to_string() +} + #[derive(Serialize)] #[serde(untagged)] -enum CliLinkTypeDetails {} - -impl CliLinkTypeDetails { - fn new(link_type: LinkLayerType, nl_attrs: &[LinkAttribute]) -> Self { - match link_type { - LinkLayerType::Loopback => todo!(), - LinkLayerType::Ether => todo!(), - LinkLayerType::Netrom => todo!(), - LinkLayerType::Eether => todo!(), - LinkLayerType::Ax25 => todo!(), - LinkLayerType::Pronet => todo!(), - LinkLayerType::Chaos => todo!(), - LinkLayerType::Ieee802 => todo!(), - LinkLayerType::Arcnet => todo!(), - LinkLayerType::Appletlk => todo!(), - LinkLayerType::Dlci => todo!(), - LinkLayerType::Atm => todo!(), - LinkLayerType::Metricom => todo!(), - LinkLayerType::Ieee1394 => todo!(), - LinkLayerType::Eui64 => todo!(), - LinkLayerType::Infiniband => todo!(), - LinkLayerType::Slip => todo!(), - LinkLayerType::Cslip => todo!(), - LinkLayerType::Slip6 => todo!(), - LinkLayerType::Cslip6 => todo!(), - LinkLayerType::Rsrvd => todo!(), - LinkLayerType::Adapt => todo!(), - LinkLayerType::Rose => todo!(), - LinkLayerType::X25 => todo!(), - LinkLayerType::Hwx25 => todo!(), - LinkLayerType::Can => todo!(), - LinkLayerType::Ppp => todo!(), - LinkLayerType::Hdlc => todo!(), - LinkLayerType::Lapb => todo!(), - LinkLayerType::Ddcmp => todo!(), - LinkLayerType::Rawhdlc => todo!(), - LinkLayerType::Rawip => todo!(), - LinkLayerType::Tunnel => todo!(), - LinkLayerType::Tunnel6 => todo!(), - LinkLayerType::Frad => todo!(), - LinkLayerType::Skip => todo!(), - LinkLayerType::Localtlk => todo!(), - LinkLayerType::Fddi => todo!(), - LinkLayerType::Bif => todo!(), - LinkLayerType::Sit => todo!(), - LinkLayerType::Ipddp => todo!(), - LinkLayerType::Ipgre => todo!(), - LinkLayerType::Pimreg => todo!(), - LinkLayerType::Hippi => todo!(), - LinkLayerType::Ash => todo!(), - LinkLayerType::Econet => todo!(), - LinkLayerType::Irda => todo!(), - LinkLayerType::Fcpp => todo!(), - LinkLayerType::Fcal => todo!(), - LinkLayerType::Fcpl => todo!(), - LinkLayerType::Fcfabric => todo!(), - LinkLayerType::Ieee802Tr => todo!(), - LinkLayerType::Ieee80211 => todo!(), - LinkLayerType::Ieee80211Prism => todo!(), - LinkLayerType::Ieee80211Radiotap => todo!(), - LinkLayerType::Ieee802154 => todo!(), - LinkLayerType::Ieee802154Monitor => todo!(), - LinkLayerType::Phonet => todo!(), - LinkLayerType::PhonetPipe => todo!(), - LinkLayerType::Caif => todo!(), - LinkLayerType::Ip6gre => todo!(), - LinkLayerType::Netlink => todo!(), - LinkLayerType::Sixlowpan => todo!(), - LinkLayerType::Vsockmon => todo!(), - LinkLayerType::Void => todo!(), - LinkLayerType::None => todo!(), +enum CliLinkInfoData {} + +impl CliLinkInfoData { + fn new(info_data: &InfoData) -> Self { + match info_data { + InfoData::Bridge(_info_bridge) => todo!(), + InfoData::Tun(_info_tun) => todo!(), + InfoData::Vlan(_info_vlan) => todo!(), + InfoData::Veth(_info_veth) => todo!(), + InfoData::Vxlan(_info_vxlan) => todo!(), + InfoData::Bond(_info_bond) => todo!(), + InfoData::IpVlan(_info_ip_vlan) => todo!(), + InfoData::IpVtap(_info_ip_vtap) => todo!(), + InfoData::MacVlan(_info_mac_vlan) => todo!(), + InfoData::MacVtap(_info_mac_vtap) => todo!(), + InfoData::GreTap(_info_gre_tap) => todo!(), + InfoData::GreTap6(_info_gre_tap6) => todo!(), + InfoData::SitTun(_info_sit_tun) => todo!(), + InfoData::GreTun(_info_gre_tun) => todo!(), + InfoData::GreTun6(_info_gre_tun6) => todo!(), + InfoData::Vti(_info_vti) => todo!(), + InfoData::Vrf(_info_vrf) => todo!(), + InfoData::Gtp(_info_gtp) => todo!(), + InfoData::Ipoib(_info_ipoib) => todo!(), + InfoData::Xfrm(_info_xfrm) => todo!(), + InfoData::MacSec(_info_mac_sec) => todo!(), + InfoData::Hsr(_info_hsr) => todo!(), + InfoData::Geneve(_info_geneve) => todo!(), + InfoData::Other(_items) => todo!(), _ => todo!(), } } } -impl std::fmt::Display for CliLinkTypeDetails { +impl std::fmt::Display for CliLinkInfoData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + _ => todo!(), + } + + Ok(()) + } +} + +#[derive(Serialize)] +pub(crate) struct CliLinkInfoKindNData { + info_kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + info_data: Option, +} + +impl std::fmt::Display for CliLinkInfoKindNData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\n ")?; + write!(f, "{} ", self.info_kind)?; + if let Some(data) = &self.info_data { + write!(f, "{data} ")?; + } Ok(()) } } @@ -112,6 +109,8 @@ pub(crate) struct CliLinkInfoDetails { allmulti: u32, min_mtu: u32, max_mtu: u32, + #[serde(skip_serializing_if = "Option::is_none")] + linkinfo: Option, #[serde(skip_serializing_if = "String::is_empty")] inet6_addr_gen_mode: String, num_tx_queues: u32, @@ -121,8 +120,10 @@ pub(crate) struct CliLinkInfoDetails { tso_max_size: u32, tso_max_segs: u32, gro_max_size: u32, - #[serde(flatten)] - link_type_details: CliLinkTypeDetails, + #[serde(skip_serializing_if = "String::is_empty")] + parentbus: String, + #[serde(skip_serializing_if = "String::is_empty")] + parentdev: String, } impl CliLinkInfoDetails { @@ -130,8 +131,7 @@ impl CliLinkInfoDetails { link_type: LinkLayerType, nl_attrs: &[LinkAttribute], ) -> Self { - let link_type_details = CliLinkTypeDetails::new(link_type, nl_attrs); - + let mut linkinfo = None; let mut promiscuity = 0; let mut allmulti = 0; let mut min_mtu = 0; @@ -144,6 +144,8 @@ impl CliLinkInfoDetails { let mut tso_max_segs = 0; let mut gro_max_size = 0; let mut inet6_addr_gen_mode = String::new(); + let mut parentbus = String::new(); + let mut parentdev = String::new(); for nl_attr in nl_attrs { match nl_attr { @@ -158,6 +160,12 @@ impl CliLinkInfoDetails { LinkAttribute::GsoMaxSize(g) => gso_max_size = *g, LinkAttribute::GsoMaxSegs(g) => gso_max_segs = *g, LinkAttribute::Other(default_nla) => match default_nla.kind() { + IFLA_PARENT_DEV_BUS_NAME => { + parentbus = default_nla_to_string(default_nla); + } + IFLA_PARENT_DEV_NAME => { + parentdev = default_nla_to_string(default_nla); + } IFLA_GRO_MAX_SIZE => { let mut val = [0u8; 4]; default_nla.emit_value(&mut val); @@ -180,6 +188,27 @@ impl CliLinkInfoDetails { } _ => { /* println!("Remains {:?}", default_nla); */ } }, + LinkAttribute::LinkInfo(info) => { + // println!("LinkInfo: {:?}", info); + let mut info_kind = String::new(); + let mut info_data = Option::None; + for nla in info { + match nla { + LinkInfo::Kind(t) => { + info_kind = t.to_string(); + } + LinkInfo::Data(data) => { + info_data = Some(CliLinkInfoData::new(data)); + } + _ => (), + } + } + + linkinfo = Some(CliLinkInfoKindNData { + info_kind, + info_data, + }); + } _ => { // println!("Remains {:?}", nl_attr); } @@ -191,15 +220,17 @@ impl CliLinkInfoDetails { allmulti, min_mtu, max_mtu, + linkinfo, inet6_addr_gen_mode, num_tx_queues, num_rx_queues, gso_max_size, gso_max_segs, - link_type_details, tso_max_size, tso_max_segs, gro_max_size, + parentbus, + parentdev, } } } @@ -208,11 +239,17 @@ impl std::fmt::Display for CliLinkInfoDetails { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - " promiscuity {} allmulti {} minmtu {} maxmtu {} addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} tso_max_size {} tso_max_segs {} gro_max_size {} {}", - self.promiscuity, - self.allmulti, - self.min_mtu, - self.max_mtu, + " promiscuity {} allmulti {} minmtu {} maxmtu {} ", + self.promiscuity, self.allmulti, self.min_mtu, self.max_mtu, + )?; + + if let Some(linkinfo) = &self.linkinfo { + write!(f, "{linkinfo}")?; + } + + write!( + f, + "addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} tso_max_size {} tso_max_segs {} gro_max_size {} ", self.inet6_addr_gen_mode, self.num_tx_queues, self.num_rx_queues, @@ -221,8 +258,15 @@ impl std::fmt::Display for CliLinkInfoDetails { self.tso_max_size, self.tso_max_segs, self.gro_max_size, - self.link_type_details )?; + + if !self.parentbus.is_empty() { + write!(f, "parentbus {} ", self.parentbus)?; + } + if !self.parentdev.is_empty() { + write!(f, "parentdev {} ", self.parentdev)?; + } + Ok(()) } } From 1977619b55503bacff5cfca8ad9b3b55b85f4900 Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Fri, 25 Jul 2025 18:38:01 +0300 Subject: [PATCH 3/5] ip-link: Support type details for vlans These changes are added as a base example for other interface types. --- Cargo.toml | 3 +- src/ip/link/show.rs | 83 +++++++++++++++++++++++++++++++-------- src/ip/link/tests/link.rs | 38 ++++++++++++++++++ 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 698d3c6..ef527c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,11 @@ path = "src/ip/main.rs" clap = { version = "4.5.40", features = ["cargo"] } futures-util = "0.3.31" rtnetlink = { git = "https://github.com/rust-netlink/rtnetlink", branch = "use_git" } -serde = {version = "1.0", default-features = false, features = ["derive"]} +serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = "1.0.140" serde_yaml = "0.9.34" tokio = { version = "1.30", features = ["rt", "net", "time", "macros"] } [dev-dependencies] pretty_assertions = "1.4.1" +ctor = "0.5.0" diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index 7909a9e..6cbb1bd 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -10,7 +10,7 @@ use rtnetlink::packet_route::link::LinkInfo; use rtnetlink::{ packet_core::Nla as _, packet_route::link::{ - AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkLayerType, LinkMessage, + AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkMessage, }, }; use serde::Serialize; @@ -28,6 +28,11 @@ const IFLA_TSO_MAX_SIZE: u16 = 59; const IFLA_TSO_MAX_SEGS: u16 = 60; const IFLA_ALLMULTI: u16 = 61; +const VLAN_FLAG_REORDER_HDR: u32 = 0x1; +const VLAN_FLAG_GVRP: u32 = 0x2; +const VLAN_FLAG_LOOSE_BINDING: u32 = 0x4; +const VLAN_FLAG_MVRP: u32 = 0x8; + fn default_nla_to_string(default_nla: &DefaultNla) -> String { let val_len = default_nla.value_len(); let mut val = vec![0u8; val_len]; @@ -41,14 +46,55 @@ fn default_nla_to_string(default_nla: &DefaultNla) -> String { #[derive(Serialize)] #[serde(untagged)] -enum CliLinkInfoData {} +enum CliLinkInfoData { + Vlan { + protocol: String, + id: u16, + flags: Vec, + }, +} impl CliLinkInfoData { fn new(info_data: &InfoData) -> Self { match info_data { InfoData::Bridge(_info_bridge) => todo!(), InfoData::Tun(_info_tun) => todo!(), - InfoData::Vlan(_info_vlan) => todo!(), + InfoData::Vlan(info_vlan) => { + use rtnetlink::packet_route::link::InfoVlan; + let mut id = 0; + let mut flags = Vec::new(); + let mut protocol = String::new(); + + for nla in info_vlan { + match nla { + InfoVlan::Id(v) => id = *v, + InfoVlan::Flags((flags_val, _)) => { + if flags_val & VLAN_FLAG_REORDER_HDR != 0 { + flags.push("REORDER_HDR".to_string()); + } + if flags_val & VLAN_FLAG_GVRP != 0 { + flags.push("GVRP".to_string()); + } + if flags_val & VLAN_FLAG_LOOSE_BINDING != 0 { + flags.push("LOOSE_BINDING".to_string()); + } + if flags_val & VLAN_FLAG_MVRP != 0 { + flags.push("MVRP".to_string()); + } + } + InfoVlan::Protocol(v) => { + protocol = v.to_string().to_uppercase(); + } + _ => (), + } + } + + Self::Vlan { + id, + flags, + protocol, + } + } InfoData::Veth(_info_veth) => todo!(), InfoData::Vxlan(_info_vxlan) => todo!(), InfoData::Bond(_info_bond) => todo!(), @@ -78,7 +124,17 @@ impl CliLinkInfoData { impl std::fmt::Display for CliLinkInfoData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - _ => todo!(), + CliLinkInfoData::Vlan { + id, + flags, + protocol, + } => { + write!(f, "protocol {} ", protocol)?; + write!(f, "id {} ", id)?; + if !flags.is_empty() { + write!(f, "<{}>", flags.as_slice().join(","))?; + } + } } Ok(()) @@ -127,10 +183,7 @@ pub(crate) struct CliLinkInfoDetails { } impl CliLinkInfoDetails { - fn new_with_type( - link_type: LinkLayerType, - nl_attrs: &[LinkAttribute], - ) -> Self { + fn new(nl_attrs: &[LinkAttribute]) -> Self { let mut linkinfo = None; let mut promiscuity = 0; let mut allmulti = 0; @@ -347,20 +400,18 @@ impl CanDisplay for CliLinkInfo { impl CanOutput for CliLinkInfo {} pub(crate) async fn handle_show( - _opts: &[&str], + opts: &[&str], include_details: bool, ) -> Result, CliError> { let (connection, handle, _) = rtnetlink::new_connection()?; tokio::spawn(connection); - let link_get_handle = handle.link().get(); + let mut link_get_handle = handle.link().get(); - /* - if let Some(iface_name) = filter.iface_name.as_ref() { + if let Some(iface_name) = opts.first() { link_get_handle = link_get_handle.match_name(iface_name.to_string()); } - */ let mut links = link_get_handle.execute(); let mut ifaces: Vec = Vec::new(); @@ -385,10 +436,8 @@ pub(crate) fn parse_nl_msg_to_iface( ..Default::default() }; - ret.details = include_details.then_some(CliLinkInfoDetails::new_with_type( - nl_msg.header.link_layer_type, - &nl_msg.attributes, - )); + ret.details = + include_details.then(|| CliLinkInfoDetails::new(&nl_msg.attributes)); for nl_attr in nl_msg.attributes { match nl_attr { diff --git a/src/ip/link/tests/link.rs b/src/ip/link/tests/link.rs index 142244f..3657467 100644 --- a/src/ip/link/tests/link.rs +++ b/src/ip/link/tests/link.rs @@ -2,6 +2,22 @@ use crate::tests::{exec_cmd, get_ip_cli_path}; +#[cfg(test)] +#[ctor::ctor] +fn setup() { + println!("setup for interfaces for tests"); + + // Add vlan over dummy interface + exec_cmd(&["ip", "link", "add", "link", "dummy0", "type", "dummy"]); + exec_cmd(&[ + "ip", "link", "add", "link", "dummy0", "name", "dummy0.1", "type", + "vlan", "id", "1", + ]); + + exec_cmd(&["ip", "link", "set", "dummy0", "up"]); + exec_cmd(&["ip", "link", "set", "dummy0.1", "up"]); +} + #[test] fn test_link_show() { let cli_path = get_ip_cli_path(); @@ -13,6 +29,17 @@ fn test_link_show() { pretty_assertions::assert_eq!(expected_output, our_output); } +#[test] +fn test_link_detailed_show() { + let cli_path = get_ip_cli_path(); + + let expected_output = exec_cmd(&["ip", "-d", "link", "show"]); + + let our_output = exec_cmd(&[cli_path.as_str(), "-d", "link", "show"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +} + #[test] fn test_link_show_json() { let cli_path = get_ip_cli_path(); @@ -23,3 +50,14 @@ fn test_link_show_json() { pretty_assertions::assert_eq!(expected_output, our_output); } + +#[test] +fn test_link_detailed_show_json() { + let cli_path = get_ip_cli_path(); + + let expected_output = exec_cmd(&["ip", "-d", "-j", "link", "show"]); + + let our_output = exec_cmd(&[cli_path.as_str(), "-d", "-j", "link", "show"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +} From df791ebd1a3edef5b1836712717ce200687f21f5 Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Sat, 26 Jul 2025 17:26:47 +0300 Subject: [PATCH 4/5] ip-link: Support showing link-netns and down-link --- src/ip/link/show.rs | 137 +++++++++++++++++++++++++++++++++++++++++--- src/mac.rs | 11 +++- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index 6cbb1bd..cf58418 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::ffi::CStr; +use std::os::fd::AsRawFd; +use futures_util::stream::StreamExt; use futures_util::stream::TryStreamExt; use rtnetlink::packet_core::DefaultNla; use rtnetlink::packet_route::link::InfoData; @@ -327,6 +329,10 @@ impl std::fmt::Display for CliLinkInfoDetails { #[derive(Serialize, Default)] pub(crate) struct CliLinkInfo { ifindex: u32, + #[serde(skip_serializing_if = "Option::is_none")] + link: Option, + #[serde(skip_serializing_if = "Option::is_none")] + link_index: Option, ifname: String, flags: Vec, mtu: u32, @@ -345,6 +351,10 @@ pub(crate) struct CliLinkInfo { address: String, #[serde(skip_serializing_if = "String::is_empty")] broadcast: String, + #[serde(skip)] + link_netns: String, + #[serde(skip_serializing_if = "Option::is_none")] + link_netnsid: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] details: Option, @@ -353,7 +363,20 @@ pub(crate) struct CliLinkInfo { impl std::fmt::Display for CliLinkInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: ", self.ifindex)?; - write_with_color!(f, CliColor::IfaceName, "{}: ", self.ifname)?; + let link = if self.link_index.is_some() || self.link.is_some() { + let display_name = if let Some(link_name) = &self.link { + link_name + } else if let Some(link_index) = self.link_index { + &format!("if{link_index}") + } else { + "NONE" + }; + format!("@{display_name}") + } else { + String::new() + }; + + write_with_color!(f, CliColor::IfaceName, "{}{link}: ", self.ifname)?; write!( f, "<{}> mtu {} qdisc {}", @@ -384,6 +407,12 @@ impl std::fmt::Display for CliLinkInfo { write_with_color!(f, CliColor::Mac, "{}", self.broadcast)?; } + if !self.link_netns.is_empty() { + write!(f, " link-netns {}", self.link_netns)?; + } else if let Some(netns_id) = self.link_netnsid { + write!(f, " link-netnsid {netns_id}")?; + } + if let Some(details) = &self.details { write!(f, "{details}",)?; } @@ -417,15 +446,16 @@ pub(crate) async fn handle_show( let mut ifaces: Vec = Vec::new(); while let Some(nl_msg) = links.try_next().await? { - ifaces.push(parse_nl_msg_to_iface(nl_msg, include_details)?); + ifaces.push(parse_nl_msg_to_iface(nl_msg, include_details).await?); } - resolve_controller_name(&mut ifaces); + resolve_controller_and_link_names(&mut ifaces); + resolve_netns_names(&mut ifaces).await?; Ok(ifaces) } -pub(crate) fn parse_nl_msg_to_iface( +pub(crate) async fn parse_nl_msg_to_iface( nl_msg: LinkMessage, include_details: bool, ) -> Result { @@ -462,6 +492,8 @@ pub(crate) fn parse_nl_msg_to_iface( } LinkAttribute::Mode(v) => ret.linkmode = v.to_string(), LinkAttribute::Controller(d) => ret.controller_ifindex = Some(d), + LinkAttribute::Link(i) => ret.link_index = Some(i), + LinkAttribute::LinkNetNsId(i) => ret.link_netnsid = Some(i), _ => { // println!("Remains {:?}", nl_attr); } @@ -471,6 +503,43 @@ pub(crate) fn parse_nl_msg_to_iface( Ok(ret) } +/// Try to resolve a netns id to its name using rtnetlink. +/// If not found, returns the id as a string. +async fn get_netns_id_from_fd( + handle: &mut rtnetlink::Handle, + fd: u32, +) -> Option { + let mut nsid_msg = rtnetlink::packet_route::nsid::NsidMessage::default(); + nsid_msg + .attributes + .push(rtnetlink::packet_route::nsid::NsidAttribute::Fd(fd)); + let mut nsid_req = rtnetlink::packet_core::NetlinkMessage::new( + rtnetlink::packet_core::NetlinkHeader::default(), + rtnetlink::packet_core::NetlinkPayload::InnerMessage( + rtnetlink::packet_route::RouteNetlinkMessage::GetNsId(nsid_msg), + ), + ); + nsid_req.header.flags = rtnetlink::packet_core::NLM_F_REQUEST; + + let mut netns = handle.request(nsid_req.clone()).unwrap(); + + if let Some(msg) = netns.next().await { + let rtnetlink::packet_core::NetlinkPayload::InnerMessage( + rtnetlink::packet_route::RouteNetlinkMessage::NewNsId(payload), + ) = msg.payload + else { + return None; + }; + for attr in payload.attributes { + if let rtnetlink::packet_route::nsid::NsidAttribute::Id(id) = attr { + return Some(id); + } + } + } + + None +} + fn get_addr_gen_mode(af_spec_unspec: &[AfSpecUnspec]) -> String { af_spec_unspec .iter() @@ -489,9 +558,8 @@ fn get_addr_gen_mode(af_spec_unspec: &[AfSpecUnspec]) -> String { .next() }) .next() - .copied() + .map(|i| i.to_string()) .unwrap_or_default() - .to_string() } fn resolve_ip_link_group_name(id: u32) -> String { @@ -502,7 +570,47 @@ fn resolve_ip_link_group_name(id: u32) -> String { } } -fn resolve_controller_name(links: &mut [CliLinkInfo]) { +async fn resolve_netns_names( + links: &mut [CliLinkInfo], +) -> Result<(), CliError> { + let (conn, mut handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(conn); + + // Read netns names from /run/netns + let netnses = std::fs::read_dir("/run/netns"); + if let Err(e) = &netnses + && e.kind() == std::io::ErrorKind::NotFound + { + // No /run/netns, nothing to resolve + return Ok(()); + } + let netnses = netnses?; + + let mut id_to_name: HashMap = HashMap::new(); + for netns in netnses { + let netns = netns?; + let name = netns.file_name().into_string().unwrap_or_default(); + let file = std::fs::File::open(netns.path())?; + + if let Some(id) = + get_netns_id_from_fd(&mut handle, file.as_raw_fd() as u32).await + { + id_to_name.insert(id, name); + } + } + + for link in links.iter_mut() { + if let Some(link_netns_id) = link.link_netnsid + && let Some(name) = id_to_name.get(&link_netns_id) + { + link.link_netns = name.to_string(); + } + } + + Ok(()) +} + +fn resolve_controller_and_link_names(links: &mut [CliLinkInfo]) { let index_2_name: HashMap = links .iter() .map(|l| (l.ifindex, l.ifname.to_string())) @@ -514,5 +622,20 @@ fn resolve_controller_name(links: &mut [CliLinkInfo]) { { link.controller = Some(name.to_string()); } + if let Some(link_ifindex) = link.link_index { + if link_ifindex == 0 { + continue; + } + + // Only set link name if the link is from the current netns + if let Some(name) = index_2_name.get(&link_ifindex) + && link.link_netnsid.is_none() + { + link.link = Some(name.to_string()); + // Clear link_index if we have a name + // We want to serialize one or the other + link.link_index = None; + } + } } } diff --git a/src/mac.rs b/src/mac.rs index 5a3119f..3cf26a2 100644 --- a/src/mac.rs +++ b/src/mac.rs @@ -3,11 +3,18 @@ use std::fmt::Write; pub fn mac_to_string(data: &[u8]) -> String { + let as_ip = data.len() == 4; + let sep = if as_ip { '.' } else { ':' }; let mut rt = String::new(); for (i, m) in data.iter().enumerate().take(data.len()) { - write!(rt, "{m:02x}").ok(); + if as_ip { + write!(rt, "{m}").ok(); + } else { + write!(rt, "{m:02x}").ok(); + } + if i != data.len() - 1 { - rt.push(':'); + rt.push(sep); } } rt From 7c31278f4d141535aa6d190f6cef751ea8e15288 Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Thu, 2 Oct 2025 22:13:07 +0300 Subject: [PATCH 5/5] ip-link: Reorganize link details structs between files --- src/ip/link/link_details.rs | 197 +++++++++++++++++++++ src/ip/link/link_info.rs | 145 ++++++++++++++++ src/ip/link/mod.rs | 2 + src/ip/link/show.rs | 337 +----------------------------------- 4 files changed, 346 insertions(+), 335 deletions(-) create mode 100644 src/ip/link/link_details.rs create mode 100644 src/ip/link/link_info.rs diff --git a/src/ip/link/link_details.rs b/src/ip/link/link_details.rs new file mode 100644 index 0000000..19872cc --- /dev/null +++ b/src/ip/link/link_details.rs @@ -0,0 +1,197 @@ +use std::ffi::CStr; + +use rtnetlink::packet_core::DefaultNla; +use rtnetlink::{ + packet_core::Nla as _, + packet_route::link::{AfSpecInet6, AfSpecUnspec, LinkAttribute}, +}; +use serde::Serialize; + +use crate::link::link_info::CliLinkInfoKindNData; + +// Use constants until support is added to netlink-packet-route +const IFLA_PARENT_DEV_NAME: u16 = 56; +const IFLA_PARENT_DEV_BUS_NAME: u16 = 57; +const IFLA_GRO_MAX_SIZE: u16 = 58; +const IFLA_TSO_MAX_SIZE: u16 = 59; +const IFLA_TSO_MAX_SEGS: u16 = 60; +const IFLA_ALLMULTI: u16 = 61; + +fn get_addr_gen_mode(af_spec_unspec: &[AfSpecUnspec]) -> String { + af_spec_unspec + .iter() + .filter_map(|s| { + let AfSpecUnspec::Inet6(v) = s else { + return None; + }; + v.iter() + .filter_map(|i| { + if let AfSpecInet6::AddrGenMode(mode) = i { + Some(mode) + } else { + None + } + }) + .next() + }) + .next() + .map(|i| i.to_string()) + .unwrap_or_default() +} +fn default_nla_to_string(default_nla: &DefaultNla) -> String { + let val_len = default_nla.value_len(); + let mut val = vec![0u8; val_len]; + default_nla.emit_value(&mut val); + CStr::from_bytes_with_nul(&val) + .expect("String nla to be nul-terminated and not contain interior nuls") + .to_str() + .expect("To be valid UTF-8") + .to_string() +} + +#[derive(Serialize)] +pub(crate) struct CliLinkInfoDetails { + promiscuity: u32, + allmulti: u32, + min_mtu: u32, + max_mtu: u32, + #[serde(skip_serializing_if = "Option::is_none")] + linkinfo: Option, + #[serde(skip_serializing_if = "String::is_empty")] + inet6_addr_gen_mode: String, + num_tx_queues: u32, + num_rx_queues: u32, + gso_max_size: u32, + gso_max_segs: u32, + tso_max_size: u32, + tso_max_segs: u32, + gro_max_size: u32, + #[serde(skip_serializing_if = "String::is_empty")] + parentbus: String, + #[serde(skip_serializing_if = "String::is_empty")] + parentdev: String, +} + +impl CliLinkInfoDetails { + pub fn new(nl_attrs: &[LinkAttribute]) -> Self { + let mut linkinfo = None; + let mut promiscuity = 0; + let mut allmulti = 0; + let mut min_mtu = 0; + let mut max_mtu = 0; + let mut num_tx_queues = 0; + let mut num_rx_queues = 0; + let mut gso_max_size = 0; + let mut gso_max_segs = 0; + let mut tso_max_size = 0; + let mut tso_max_segs = 0; + let mut gro_max_size = 0; + let mut inet6_addr_gen_mode = String::new(); + let mut parentbus = String::new(); + let mut parentdev = String::new(); + + for nl_attr in nl_attrs { + match nl_attr { + LinkAttribute::Promiscuity(p) => promiscuity = *p, + LinkAttribute::MinMtu(m) => min_mtu = *m, + LinkAttribute::MaxMtu(m) => max_mtu = *m, + LinkAttribute::AfSpecUnspec(a) => { + inet6_addr_gen_mode = get_addr_gen_mode(a) + } + LinkAttribute::NumTxQueues(n) => num_tx_queues = *n, + LinkAttribute::NumRxQueues(n) => num_rx_queues = *n, + LinkAttribute::GsoMaxSize(g) => gso_max_size = *g, + LinkAttribute::GsoMaxSegs(g) => gso_max_segs = *g, + LinkAttribute::Other(default_nla) => match default_nla.kind() { + IFLA_PARENT_DEV_BUS_NAME => { + parentbus = default_nla_to_string(default_nla); + } + IFLA_PARENT_DEV_NAME => { + parentdev = default_nla_to_string(default_nla); + } + IFLA_GRO_MAX_SIZE => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + gro_max_size = u32::from_ne_bytes(val); + } + IFLA_TSO_MAX_SIZE => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + tso_max_size = u32::from_ne_bytes(val); + } + IFLA_TSO_MAX_SEGS => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + tso_max_segs = u32::from_ne_bytes(val); + } + IFLA_ALLMULTI => { + let mut val = [0u8; 4]; + default_nla.emit_value(&mut val); + allmulti = u32::from_ne_bytes(val); + } + _ => { /* println!("Remains {:?}", default_nla); */ } + }, + LinkAttribute::LinkInfo(info) => { + linkinfo = CliLinkInfoKindNData::new(info); + } + _ => { + // println!("Remains {:?}", nl_attr); + } + } + } + + Self { + promiscuity, + allmulti, + min_mtu, + max_mtu, + linkinfo, + inet6_addr_gen_mode, + num_tx_queues, + num_rx_queues, + gso_max_size, + gso_max_segs, + tso_max_size, + tso_max_segs, + gro_max_size, + parentbus, + parentdev, + } + } +} + +impl std::fmt::Display for CliLinkInfoDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + " promiscuity {} allmulti {} minmtu {} maxmtu {} ", + self.promiscuity, self.allmulti, self.min_mtu, self.max_mtu, + )?; + + if let Some(linkinfo) = &self.linkinfo { + write!(f, "{linkinfo}")?; + } + + write!( + f, + "addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} tso_max_size {} tso_max_segs {} gro_max_size {} ", + self.inet6_addr_gen_mode, + self.num_tx_queues, + self.num_rx_queues, + self.gso_max_size, + self.gso_max_segs, + self.tso_max_size, + self.tso_max_segs, + self.gro_max_size, + )?; + + if !self.parentbus.is_empty() { + write!(f, "parentbus {} ", self.parentbus)?; + } + if !self.parentdev.is_empty() { + write!(f, "parentdev {} ", self.parentdev)?; + } + + Ok(()) + } +} diff --git a/src/ip/link/link_info.rs b/src/ip/link/link_info.rs new file mode 100644 index 0000000..f6f2865 --- /dev/null +++ b/src/ip/link/link_info.rs @@ -0,0 +1,145 @@ +use rtnetlink::packet_route::link::{InfoData, LinkInfo}; +use serde::Serialize; + +const VLAN_FLAG_REORDER_HDR: u32 = 0x1; +const VLAN_FLAG_GVRP: u32 = 0x2; +const VLAN_FLAG_LOOSE_BINDING: u32 = 0x4; +const VLAN_FLAG_MVRP: u32 = 0x8; + +#[derive(Serialize)] +#[serde(untagged)] +enum CliLinkInfoData { + Vlan { + protocol: String, + id: u16, + flags: Vec, + }, +} + +impl CliLinkInfoData { + fn new(info_data: &InfoData) -> Self { + match info_data { + InfoData::Bridge(_info_bridge) => todo!(), + InfoData::Tun(_info_tun) => todo!(), + InfoData::Vlan(info_vlan) => { + use rtnetlink::packet_route::link::InfoVlan; + let mut id = 0; + let mut flags = Vec::new(); + let mut protocol = String::new(); + + for nla in info_vlan { + match nla { + InfoVlan::Id(v) => id = *v, + InfoVlan::Flags((flags_val, _)) => { + if flags_val & VLAN_FLAG_REORDER_HDR != 0 { + flags.push("REORDER_HDR".to_string()); + } + if flags_val & VLAN_FLAG_GVRP != 0 { + flags.push("GVRP".to_string()); + } + if flags_val & VLAN_FLAG_LOOSE_BINDING != 0 { + flags.push("LOOSE_BINDING".to_string()); + } + if flags_val & VLAN_FLAG_MVRP != 0 { + flags.push("MVRP".to_string()); + } + } + InfoVlan::Protocol(v) => { + protocol = v.to_string().to_uppercase(); + } + _ => (), + } + } + + Self::Vlan { + id, + flags, + protocol, + } + } + InfoData::Veth(_info_veth) => todo!(), + InfoData::Vxlan(_info_vxlan) => todo!(), + InfoData::Bond(_info_bond) => todo!(), + InfoData::IpVlan(_info_ip_vlan) => todo!(), + InfoData::IpVtap(_info_ip_vtap) => todo!(), + InfoData::MacVlan(_info_mac_vlan) => todo!(), + InfoData::MacVtap(_info_mac_vtap) => todo!(), + InfoData::GreTap(_info_gre_tap) => todo!(), + InfoData::GreTap6(_info_gre_tap6) => todo!(), + InfoData::SitTun(_info_sit_tun) => todo!(), + InfoData::GreTun(_info_gre_tun) => todo!(), + InfoData::GreTun6(_info_gre_tun6) => todo!(), + InfoData::Vti(_info_vti) => todo!(), + InfoData::Vrf(_info_vrf) => todo!(), + InfoData::Gtp(_info_gtp) => todo!(), + InfoData::Ipoib(_info_ipoib) => todo!(), + InfoData::Xfrm(_info_xfrm) => todo!(), + InfoData::MacSec(_info_mac_sec) => todo!(), + InfoData::Hsr(_info_hsr) => todo!(), + InfoData::Geneve(_info_geneve) => todo!(), + InfoData::Other(_items) => todo!(), + _ => todo!(), + } + } +} + +impl std::fmt::Display for CliLinkInfoData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CliLinkInfoData::Vlan { + id, + flags, + protocol, + } => { + write!(f, "protocol {} ", protocol)?; + write!(f, "id {} ", id)?; + if !flags.is_empty() { + write!(f, "<{}>", flags.as_slice().join(","))?; + } + } + } + + Ok(()) + } +} + +#[derive(Serialize)] +pub(crate) struct CliLinkInfoKindNData { + info_kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + info_data: Option, +} + +impl std::fmt::Display for CliLinkInfoKindNData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\n ")?; + write!(f, "{} ", self.info_kind)?; + if let Some(data) = &self.info_data { + write!(f, "{data} ")?; + } + Ok(()) + } +} + +impl CliLinkInfoKindNData { + pub fn new(link_info: &[LinkInfo]) -> Option { + let mut info_kind = String::new(); + let mut info_data = Option::None; + for nla in link_info { + match nla { + LinkInfo::Kind(t) => { + info_kind = t.to_string(); + } + LinkInfo::Data(data) => { + info_data = Some(CliLinkInfoData::new(data)); + } + _ => (), + } + } + + Some(CliLinkInfoKindNData { + info_kind, + info_data, + }) + } +} diff --git a/src/ip/link/mod.rs b/src/ip/link/mod.rs index 0f1af48..08ac7e5 100644 --- a/src/ip/link/mod.rs +++ b/src/ip/link/mod.rs @@ -2,6 +2,8 @@ mod cli; mod flags; +mod link_details; +mod link_info; mod show; #[cfg(test)] diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index cf58418..74b8fb4 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -1,20 +1,11 @@ // SPDX-License-Identifier: MIT use std::collections::HashMap; -use std::ffi::CStr; use std::os::fd::AsRawFd; use futures_util::stream::StreamExt; use futures_util::stream::TryStreamExt; -use rtnetlink::packet_core::DefaultNla; -use rtnetlink::packet_route::link::InfoData; -use rtnetlink::packet_route::link::LinkInfo; -use rtnetlink::{ - packet_core::Nla as _, - packet_route::link::{ - AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkMessage, - }, -}; +use rtnetlink::packet_route::link::{LinkAttribute, LinkMessage}; use serde::Serialize; use super::flags::link_flags_to_string; @@ -22,309 +13,7 @@ use iproute_rs::{ CanDisplay, CanOutput, CliColor, CliError, mac_to_string, write_with_color, }; -// Use constants until support is added to netlink-packet-route -const IFLA_PARENT_DEV_NAME: u16 = 56; -const IFLA_PARENT_DEV_BUS_NAME: u16 = 57; -const IFLA_GRO_MAX_SIZE: u16 = 58; -const IFLA_TSO_MAX_SIZE: u16 = 59; -const IFLA_TSO_MAX_SEGS: u16 = 60; -const IFLA_ALLMULTI: u16 = 61; - -const VLAN_FLAG_REORDER_HDR: u32 = 0x1; -const VLAN_FLAG_GVRP: u32 = 0x2; -const VLAN_FLAG_LOOSE_BINDING: u32 = 0x4; -const VLAN_FLAG_MVRP: u32 = 0x8; - -fn default_nla_to_string(default_nla: &DefaultNla) -> String { - let val_len = default_nla.value_len(); - let mut val = vec![0u8; val_len]; - default_nla.emit_value(&mut val); - CStr::from_bytes_with_nul(&val) - .expect("String nla to be nul-terminated and not contain interior nuls") - .to_str() - .expect("To be valid UTF-8") - .to_string() -} - -#[derive(Serialize)] -#[serde(untagged)] -enum CliLinkInfoData { - Vlan { - protocol: String, - id: u16, - flags: Vec, - }, -} - -impl CliLinkInfoData { - fn new(info_data: &InfoData) -> Self { - match info_data { - InfoData::Bridge(_info_bridge) => todo!(), - InfoData::Tun(_info_tun) => todo!(), - InfoData::Vlan(info_vlan) => { - use rtnetlink::packet_route::link::InfoVlan; - let mut id = 0; - let mut flags = Vec::new(); - let mut protocol = String::new(); - - for nla in info_vlan { - match nla { - InfoVlan::Id(v) => id = *v, - InfoVlan::Flags((flags_val, _)) => { - if flags_val & VLAN_FLAG_REORDER_HDR != 0 { - flags.push("REORDER_HDR".to_string()); - } - if flags_val & VLAN_FLAG_GVRP != 0 { - flags.push("GVRP".to_string()); - } - if flags_val & VLAN_FLAG_LOOSE_BINDING != 0 { - flags.push("LOOSE_BINDING".to_string()); - } - if flags_val & VLAN_FLAG_MVRP != 0 { - flags.push("MVRP".to_string()); - } - } - InfoVlan::Protocol(v) => { - protocol = v.to_string().to_uppercase(); - } - _ => (), - } - } - - Self::Vlan { - id, - flags, - protocol, - } - } - InfoData::Veth(_info_veth) => todo!(), - InfoData::Vxlan(_info_vxlan) => todo!(), - InfoData::Bond(_info_bond) => todo!(), - InfoData::IpVlan(_info_ip_vlan) => todo!(), - InfoData::IpVtap(_info_ip_vtap) => todo!(), - InfoData::MacVlan(_info_mac_vlan) => todo!(), - InfoData::MacVtap(_info_mac_vtap) => todo!(), - InfoData::GreTap(_info_gre_tap) => todo!(), - InfoData::GreTap6(_info_gre_tap6) => todo!(), - InfoData::SitTun(_info_sit_tun) => todo!(), - InfoData::GreTun(_info_gre_tun) => todo!(), - InfoData::GreTun6(_info_gre_tun6) => todo!(), - InfoData::Vti(_info_vti) => todo!(), - InfoData::Vrf(_info_vrf) => todo!(), - InfoData::Gtp(_info_gtp) => todo!(), - InfoData::Ipoib(_info_ipoib) => todo!(), - InfoData::Xfrm(_info_xfrm) => todo!(), - InfoData::MacSec(_info_mac_sec) => todo!(), - InfoData::Hsr(_info_hsr) => todo!(), - InfoData::Geneve(_info_geneve) => todo!(), - InfoData::Other(_items) => todo!(), - _ => todo!(), - } - } -} - -impl std::fmt::Display for CliLinkInfoData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CliLinkInfoData::Vlan { - id, - flags, - protocol, - } => { - write!(f, "protocol {} ", protocol)?; - write!(f, "id {} ", id)?; - if !flags.is_empty() { - write!(f, "<{}>", flags.as_slice().join(","))?; - } - } - } - - Ok(()) - } -} - -#[derive(Serialize)] -pub(crate) struct CliLinkInfoKindNData { - info_kind: String, - #[serde(skip_serializing_if = "Option::is_none")] - info_data: Option, -} - -impl std::fmt::Display for CliLinkInfoKindNData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "\n ")?; - write!(f, "{} ", self.info_kind)?; - if let Some(data) = &self.info_data { - write!(f, "{data} ")?; - } - Ok(()) - } -} - -#[derive(Serialize)] -pub(crate) struct CliLinkInfoDetails { - promiscuity: u32, - allmulti: u32, - min_mtu: u32, - max_mtu: u32, - #[serde(skip_serializing_if = "Option::is_none")] - linkinfo: Option, - #[serde(skip_serializing_if = "String::is_empty")] - inet6_addr_gen_mode: String, - num_tx_queues: u32, - num_rx_queues: u32, - gso_max_size: u32, - gso_max_segs: u32, - tso_max_size: u32, - tso_max_segs: u32, - gro_max_size: u32, - #[serde(skip_serializing_if = "String::is_empty")] - parentbus: String, - #[serde(skip_serializing_if = "String::is_empty")] - parentdev: String, -} - -impl CliLinkInfoDetails { - fn new(nl_attrs: &[LinkAttribute]) -> Self { - let mut linkinfo = None; - let mut promiscuity = 0; - let mut allmulti = 0; - let mut min_mtu = 0; - let mut max_mtu = 0; - let mut num_tx_queues = 0; - let mut num_rx_queues = 0; - let mut gso_max_size = 0; - let mut gso_max_segs = 0; - let mut tso_max_size = 0; - let mut tso_max_segs = 0; - let mut gro_max_size = 0; - let mut inet6_addr_gen_mode = String::new(); - let mut parentbus = String::new(); - let mut parentdev = String::new(); - - for nl_attr in nl_attrs { - match nl_attr { - LinkAttribute::Promiscuity(p) => promiscuity = *p, - LinkAttribute::MinMtu(m) => min_mtu = *m, - LinkAttribute::MaxMtu(m) => max_mtu = *m, - LinkAttribute::AfSpecUnspec(a) => { - inet6_addr_gen_mode = get_addr_gen_mode(a) - } - LinkAttribute::NumTxQueues(n) => num_tx_queues = *n, - LinkAttribute::NumRxQueues(n) => num_rx_queues = *n, - LinkAttribute::GsoMaxSize(g) => gso_max_size = *g, - LinkAttribute::GsoMaxSegs(g) => gso_max_segs = *g, - LinkAttribute::Other(default_nla) => match default_nla.kind() { - IFLA_PARENT_DEV_BUS_NAME => { - parentbus = default_nla_to_string(default_nla); - } - IFLA_PARENT_DEV_NAME => { - parentdev = default_nla_to_string(default_nla); - } - IFLA_GRO_MAX_SIZE => { - let mut val = [0u8; 4]; - default_nla.emit_value(&mut val); - gro_max_size = u32::from_ne_bytes(val); - } - IFLA_TSO_MAX_SIZE => { - let mut val = [0u8; 4]; - default_nla.emit_value(&mut val); - tso_max_size = u32::from_ne_bytes(val); - } - IFLA_TSO_MAX_SEGS => { - let mut val = [0u8; 4]; - default_nla.emit_value(&mut val); - tso_max_segs = u32::from_ne_bytes(val); - } - IFLA_ALLMULTI => { - let mut val = [0u8; 4]; - default_nla.emit_value(&mut val); - allmulti = u32::from_ne_bytes(val); - } - _ => { /* println!("Remains {:?}", default_nla); */ } - }, - LinkAttribute::LinkInfo(info) => { - // println!("LinkInfo: {:?}", info); - let mut info_kind = String::new(); - let mut info_data = Option::None; - for nla in info { - match nla { - LinkInfo::Kind(t) => { - info_kind = t.to_string(); - } - LinkInfo::Data(data) => { - info_data = Some(CliLinkInfoData::new(data)); - } - _ => (), - } - } - - linkinfo = Some(CliLinkInfoKindNData { - info_kind, - info_data, - }); - } - _ => { - // println!("Remains {:?}", nl_attr); - } - } - } - - Self { - promiscuity, - allmulti, - min_mtu, - max_mtu, - linkinfo, - inet6_addr_gen_mode, - num_tx_queues, - num_rx_queues, - gso_max_size, - gso_max_segs, - tso_max_size, - tso_max_segs, - gro_max_size, - parentbus, - parentdev, - } - } -} - -impl std::fmt::Display for CliLinkInfoDetails { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - " promiscuity {} allmulti {} minmtu {} maxmtu {} ", - self.promiscuity, self.allmulti, self.min_mtu, self.max_mtu, - )?; - - if let Some(linkinfo) = &self.linkinfo { - write!(f, "{linkinfo}")?; - } - - write!( - f, - "addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} tso_max_size {} tso_max_segs {} gro_max_size {} ", - self.inet6_addr_gen_mode, - self.num_tx_queues, - self.num_rx_queues, - self.gso_max_size, - self.gso_max_segs, - self.tso_max_size, - self.tso_max_segs, - self.gro_max_size, - )?; - - if !self.parentbus.is_empty() { - write!(f, "parentbus {} ", self.parentbus)?; - } - if !self.parentdev.is_empty() { - write!(f, "parentdev {} ", self.parentdev)?; - } - - Ok(()) - } -} +use crate::link::link_details::CliLinkInfoDetails; #[derive(Serialize, Default)] pub(crate) struct CliLinkInfo { @@ -540,28 +229,6 @@ async fn get_netns_id_from_fd( None } -fn get_addr_gen_mode(af_spec_unspec: &[AfSpecUnspec]) -> String { - af_spec_unspec - .iter() - .filter_map(|s| { - let AfSpecUnspec::Inet6(v) = s else { - return None; - }; - v.iter() - .filter_map(|i| { - if let AfSpecInet6::AddrGenMode(mode) = i { - Some(mode) - } else { - None - } - }) - .next() - }) - .next() - .map(|i| i.to_string()) - .unwrap_or_default() -} - fn resolve_ip_link_group_name(id: u32) -> String { // TODO: Read `/usr/share/iproute2/group` and `/etc/iproute2/group` match id {