From 6e968e6a2dca891e607d391b3cfb69610c6dcc0b Mon Sep 17 00:00:00 2001 From: Yonatan Linik Date: Fri, 25 Jul 2025 17:35:36 +0300 Subject: [PATCH 1/2] ip-link: Adds support for common details Now supporting: promiscuity min_mtu max_mtu inet6_addr_gen_mode num_tx_queues num_rx_queues gso_max_size gso_max_segs Also adds skeleton for specific details --- src/ip/link/cli.rs | 4 +- src/ip/link/show.rs | 213 +++++++++++++++++++++++++++++++++++++++++++- src/ip/main.rs | 7 ++ 3 files changed, 220 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..5740f27 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_route::link::{ + AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkLayerType, LinkMessage, + }, + packet_utils::nla::Nla, +}; use serde::Serialize; use super::flags::link_flags_to_string; @@ -11,6 +16,172 @@ use iproute_rs::{ CanDisplay, CanOutput, CliColor, CliError, mac_to_string, write_with_color, }; +#[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, + 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, + #[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 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 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, + _ => { + // println!("Remains {:?}", nl_attr); + } + } + } + + Self { + promiscuity, + min_mtu, + max_mtu, + inet6_addr_gen_mode, + num_tx_queues, + num_rx_queues, + gso_max_size, + gso_max_segs, + link_type_details, + } + } +} + +impl std::fmt::Display for CliLinkInfoDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + " promiscuity {} minmtu {} maxmtu {} addrgenmode {} numtxqueues {} numrxqueues {} gso_max_size {} gso_max_segs {} {}", + self.promiscuity, + 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.link_type_details + )?; + Ok(()) + } +} + #[derive(Serialize, Default)] pub(crate) struct CliLinkInfo { ifindex: u32, @@ -32,6 +203,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 +241,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 +259,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 +277,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 +287,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 +296,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 +329,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 76ec769ccefd7a0962a0b9dc5da1b84c07663d57 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 25 Jul 2025 18:38:01 +0300 Subject: [PATCH 2/2] ip-link: Support type details for lo Loopback interfaces don't seem to have specific properties unique to them. These changes are added as a base example for other interface types. --- src/ip/link/show.rs | 39 +++++++++++++++++++++------------------ src/ip/link/tests/link.rs | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index 5740f27..ba32c27 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -3,11 +3,8 @@ use std::collections::HashMap; use futures_util::stream::TryStreamExt; -use rtnetlink::{ - packet_route::link::{ - AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkLayerType, LinkMessage, - }, - packet_utils::nla::Nla, +use rtnetlink::packet_route::link::{ + AfSpecInet6, AfSpecUnspec, LinkAttribute, LinkLayerType, LinkMessage, }; use serde::Serialize; @@ -18,12 +15,14 @@ use iproute_rs::{ #[derive(Serialize)] #[serde(untagged)] -enum CliLinkTypeDetails {} +enum CliLinkTypeDetails { + Loopback, +} impl CliLinkTypeDetails { - fn new(link_type: LinkLayerType, nl_attrs: &[LinkAttribute]) -> Self { + fn new(link_type: LinkLayerType, _nl_attrs: &[LinkAttribute]) -> Self { match link_type { - LinkLayerType::Loopback => todo!(), + LinkLayerType::Loopback => CliLinkTypeDetails::Loopback, LinkLayerType::Ether => todo!(), LinkLayerType::Netrom => todo!(), LinkLayerType::Eether => todo!(), @@ -95,7 +94,11 @@ impl CliLinkTypeDetails { } impl std::fmt::Display for CliLinkTypeDetails { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CliLinkTypeDetails::Loopback => (), + } + Ok(()) } } @@ -258,20 +261,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(); @@ -296,10 +297,12 @@ 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_with_type( + nl_msg.header.link_layer_type, + &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..00b2940 100644 --- a/src/ip/link/tests/link.rs +++ b/src/ip/link/tests/link.rs @@ -13,6 +13,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", "lo"]); + + let our_output = exec_cmd(&[cli_path.as_str(), "-d", "link", "show", "lo"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +} + #[test] fn test_link_show_json() { let cli_path = get_ip_cli_path(); @@ -23,3 +34,15 @@ 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", "lo"]); + + let our_output = + exec_cmd(&[cli_path.as_str(), "-d", "-j", "link", "show", "lo"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +}