Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions src/ip/link/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
197 changes: 197 additions & 0 deletions src/ip/link/link_details.rs
Original file line number Diff line number Diff line change
@@ -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<CliLinkInfoKindNData>,
#[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(())
}
}
145 changes: 145 additions & 0 deletions src/ip/link/link_info.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
},
}

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<CliLinkInfoData>,
}

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<Self> {
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,
})
}
}
2 changes: 2 additions & 0 deletions src/ip/link/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod cli;
mod flags;
mod link_details;
mod link_info;
mod show;

#[cfg(test)]
Expand Down
Loading
Loading