diff --git a/Cargo.toml b/Cargo.toml index 5c58087..bffa625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ netlink-proto = { default-features = false, version = "0.11.2" } netlink-sys = { version = "0.8.4" } thiserror = "1.0.29" tokio = { version = "1.0.1", features = ["rt"], optional = true} +num_enum = "0.6.1" [dev-dependencies] tokio = { version = "1.11.0", features = ["macros", "rt", "rt-multi-thread"] } diff --git a/examples/dump_channel.rs b/examples/dump_channel.rs new file mode 100644 index 0000000..5dd9c72 --- /dev/null +++ b/examples/dump_channel.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +use futures::stream::TryStreamExt; + +// Once we find a way to load netsimdev kernel module in CI, we can convert this +// to a test +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(get_channel(None)); +} + +async fn get_channel(iface_name: Option<&str>) { + let (connection, mut handle, _) = ethtool::new_connection().unwrap(); + tokio::spawn(connection); + + let mut channel_handle = handle.channel().get(iface_name).execute().await; + + let mut msgs = Vec::new(); + while let Some(msg) = channel_handle.try_next().await.unwrap() { + msgs.push(msg); + } + assert!(!msgs.is_empty()); + for msg in msgs { + println!("{msg:?}"); + } +} diff --git a/src/channel/attr.rs b/src/channel/attr.rs new file mode 100644 index 0000000..d5d738e --- /dev/null +++ b/src/channel/attr.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT + +use anyhow::Context; +use byteorder::{ByteOrder, NativeEndian}; +use netlink_packet_utils::{ + nla::{DefaultNla, Nla, NlaBuffer, NlasIterator, NLA_F_NESTED}, + parsers::parse_u32, + DecodeError, Emitable, Parseable, +}; + +use crate::{EthtoolAttr, EthtoolHeader}; + +const ETHTOOL_A_CHANNELS_HEADER: u16 = 1; +const ETHTOOL_A_CHANNELS_RX_MAX: u16 = 2; +const ETHTOOL_A_CHANNELS_TX_MAX: u16 = 3; +const ETHTOOL_A_CHANNELS_OTHER_MAX: u16 = 4; +const ETHTOOL_A_CHANNELS_COMBINED_MAX: u16 = 5; +const ETHTOOL_A_CHANNELS_RX_COUNT: u16 = 6; +const ETHTOOL_A_CHANNELS_TX_COUNT: u16 = 7; +const ETHTOOL_A_CHANNELS_OTHER_COUNT: u16 = 8; +const ETHTOOL_A_CHANNELS_COMBINED_COUNT: u16 = 9; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum EthtoolChannelAttr { + Header(Vec), + RxMax(u32), + TxMax(u32), + OtherMax(u32), + CombinedMax(u32), + RxCount(u32), + TxCount(u32), + OtherCount(u32), + CombinedCount(u32), + Other(DefaultNla), +} + +impl Nla for EthtoolChannelAttr { + fn value_len(&self) -> usize { + match self { + Self::Header(hdrs) => hdrs.as_slice().buffer_len(), + Self::RxMax(_) + | Self::TxMax(_) + | Self::OtherMax(_) + | Self::CombinedMax(_) + | Self::RxCount(_) + | Self::TxCount(_) + | Self::OtherCount(_) + | Self::CombinedCount(_) => 4, + Self::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Header(_) => ETHTOOL_A_CHANNELS_HEADER | NLA_F_NESTED, + Self::RxMax(_) => ETHTOOL_A_CHANNELS_RX_MAX, + Self::TxMax(_) => ETHTOOL_A_CHANNELS_TX_MAX, + Self::OtherMax(_) => ETHTOOL_A_CHANNELS_OTHER_MAX, + Self::CombinedMax(_) => ETHTOOL_A_CHANNELS_COMBINED_MAX, + Self::RxCount(_) => ETHTOOL_A_CHANNELS_RX_COUNT, + Self::TxCount(_) => ETHTOOL_A_CHANNELS_TX_COUNT, + Self::OtherCount(_) => ETHTOOL_A_CHANNELS_OTHER_COUNT, + Self::CombinedCount(_) => ETHTOOL_A_CHANNELS_COMBINED_COUNT, + Self::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Header(ref nlas) => nlas.as_slice().emit(buffer), + Self::RxMax(d) + | Self::TxMax(d) + | Self::OtherMax(d) + | Self::CombinedMax(d) + | Self::RxCount(d) + | Self::TxCount(d) + | Self::OtherCount(d) + | Self::CombinedCount(d) => NativeEndian::write_u32(buffer, *d), + Self::Other(ref attr) => attr.emit(buffer), + } + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for EthtoolChannelAttr +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let payload = buf.value(); + Ok(match buf.kind() { + ETHTOOL_A_CHANNELS_HEADER => { + let mut nlas = Vec::new(); + let error_msg = "failed to parse ring header attributes"; + for nla in NlasIterator::new(payload) { + let nla = &nla.context(error_msg)?; + let parsed = + EthtoolHeader::parse(nla).context(error_msg)?; + nlas.push(parsed); + } + Self::Header(nlas) + } + ETHTOOL_A_CHANNELS_RX_MAX => Self::RxMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_MAX value")?, + ), + + ETHTOOL_A_CHANNELS_TX_MAX => Self::TxMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_TX_MAX value")?, + ), + + ETHTOOL_A_CHANNELS_OTHER_MAX => Self::OtherMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_OTHER_MAX value")?, + ), + + ETHTOOL_A_CHANNELS_COMBINED_MAX => Self::CombinedMax( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_COMBINED_MAX value")?, + ), + + ETHTOOL_A_CHANNELS_RX_COUNT => Self::RxCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_RX_COUNT value")?, + ), + + ETHTOOL_A_CHANNELS_TX_COUNT => Self::TxCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_TX_COUNT value")?, + ), + + ETHTOOL_A_CHANNELS_OTHER_COUNT => Self::OtherCount( + parse_u32(payload) + .context("Invalid ETHTOOL_A_CHANNELS_OTHER_COUNT value")?, + ), + + ETHTOOL_A_CHANNELS_COMBINED_COUNT => { + Self::CombinedCount(parse_u32(payload).context( + "Invalid ETHTOOL_A_CHANNELS_COMBINED_COUNT value", + )?) + } + + kind => Self::Other( + DefaultNla::parse(buf) + .context(format!("invalid ethtool ring NLA kind {kind}"))?, + ), + }) + } +} + +pub(crate) fn parse_channel_nlas( + buffer: &[u8], +) -> Result, DecodeError> { + let mut nlas = Vec::new(); + for nla in NlasIterator::new(buffer) { + let error_msg = + format!("Failed to parse ethtool ring message attribute {nla:?}"); + let nla = &nla.context(error_msg.clone())?; + let parsed = EthtoolChannelAttr::parse(nla).context(error_msg)?; + nlas.push(EthtoolAttr::Channel(parsed)); + } + Ok(nlas) +} diff --git a/src/channel/get.rs b/src/channel/get.rs new file mode 100644 index 0000000..2ebe54f --- /dev/null +++ b/src/channel/get.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +use futures::TryStream; +use netlink_packet_generic::GenlMessage; + +use crate::{ethtool_execute, EthtoolError, EthtoolHandle, EthtoolMessage}; + +pub struct EthtoolChannelGetRequest { + handle: EthtoolHandle, + iface_name: Option, +} + +impl EthtoolChannelGetRequest { + pub(crate) fn new(handle: EthtoolHandle, iface_name: Option<&str>) -> Self { + EthtoolChannelGetRequest { + handle, + iface_name: iface_name.map(|i| i.to_string()), + } + } + + pub async fn execute( + self, + ) -> impl TryStream, Error = EthtoolError> + { + let EthtoolChannelGetRequest { + mut handle, + iface_name, + } = self; + + let ethtool_msg = + EthtoolMessage::new_channel_get(iface_name.as_deref()); + ethtool_execute(&mut handle, iface_name.is_none(), ethtool_msg).await + } +} diff --git a/src/channel/handle.rs b/src/channel/handle.rs new file mode 100644 index 0000000..8763981 --- /dev/null +++ b/src/channel/handle.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +use crate::{EthtoolChannelGetRequest, EthtoolHandle}; + +pub struct EthtoolChannelHandle(EthtoolHandle); + +impl EthtoolChannelHandle { + pub fn new(handle: EthtoolHandle) -> Self { + EthtoolChannelHandle(handle) + } + + /// Retrieve the ethtool channels of a interface (equivalent to `ethtool -l + /// eth1`) + pub fn get( + &mut self, + iface_name: Option<&str>, + ) -> EthtoolChannelGetRequest { + EthtoolChannelGetRequest::new(self.0.clone(), iface_name) + } +} diff --git a/src/channel/mod.rs b/src/channel/mod.rs new file mode 100644 index 0000000..9578ac4 --- /dev/null +++ b/src/channel/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +mod attr; +mod get; +mod handle; + +pub(crate) use attr::parse_channel_nlas; + +pub use attr::EthtoolChannelAttr; +pub use get::EthtoolChannelGetRequest; +pub use handle::EthtoolChannelHandle; diff --git a/src/handle.rs b/src/handle.rs index 8b26024..799c39d 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -9,9 +9,9 @@ use netlink_packet_generic::GenlMessage; use netlink_packet_utils::DecodeError; use crate::{ - try_ethtool, EthtoolCoalesceHandle, EthtoolError, EthtoolFeatureHandle, - EthtoolLinkModeHandle, EthtoolMessage, EthtoolPauseHandle, - EthtoolRingHandle, EthtoolTsInfoHandle, + try_ethtool, EthtoolChannelHandle, EthtoolCoalesceHandle, EthtoolError, + EthtoolFeatureHandle, EthtoolLinkModeHandle, EthtoolMessage, + EthtoolPauseHandle, EthtoolRingHandle, EthtoolTsInfoHandle, }; #[derive(Clone, Debug)] @@ -40,6 +40,10 @@ impl EthtoolHandle { EthtoolRingHandle::new(self.clone()) } + pub fn channel(&mut self) -> EthtoolChannelHandle { + EthtoolChannelHandle::new(self.clone()) + } + pub fn coalesce(&mut self) -> EthtoolCoalesceHandle { EthtoolCoalesceHandle::new(self.clone()) } diff --git a/src/lib.rs b/src/lib.rs index b953c80..2df72e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT mod bitset_util; +mod channel; mod coalesce; mod connection; mod error; @@ -14,6 +15,9 @@ mod pause; mod ring; mod tsinfo; +pub use channel::{ + EthtoolChannelAttr, EthtoolChannelGetRequest, EthtoolChannelHandle, +}; pub use coalesce::{ EthtoolCoalesceAttr, EthtoolCoalesceGetRequest, EthtoolCoalesceHandle, }; diff --git a/src/message.rs b/src/message.rs index b76f3ad..7b8eb2c 100644 --- a/src/message.rs +++ b/src/message.rs @@ -4,8 +4,10 @@ use netlink_packet_generic::{GenlFamily, GenlHeader}; use netlink_packet_utils::{ nla::Nla, DecodeError, Emitable, ParseableParametrized, }; +use num_enum::{FromPrimitive, IntoPrimitive}; use crate::{ + channel::{parse_channel_nlas, EthtoolChannelAttr}, coalesce::{parse_coalesce_nlas, EthtoolCoalesceAttr}, feature::{parse_feature_nlas, EthtoolFeatureAttr}, link_mode::{parse_link_mode_nlas, EthtoolLinkModeAttr}, @@ -15,52 +17,44 @@ use crate::{ EthtoolHeader, }; -const ETHTOOL_MSG_PAUSE_GET: u8 = 21; -const ETHTOOL_MSG_PAUSE_GET_REPLY: u8 = 22; -const ETHTOOL_MSG_FEATURES_GET: u8 = 11; -const ETHTOOL_MSG_FEATURES_GET_REPLY: u8 = 11; -const ETHTOOL_MSG_LINKMODES_GET: u8 = 4; -const ETHTOOL_MSG_LINKMODES_GET_REPLY: u8 = 4; -const ETHTOOL_MSG_RINGS_GET: u8 = 15; -const ETHTOOL_MSG_RINGS_GET_REPLY: u8 = 16; -const ETHTOOL_MSG_COALESCE_GET: u8 = 19; -const ETHTOOL_MSG_COALESCE_GET_REPLY: u8 = 20; -const ETHTOOL_MSG_TSINFO_GET: u8 = 25; -const ETHTOOL_MSG_TSINFO_GET_REPLY: u8 = 26; +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, IntoPrimitive, FromPrimitive)] +pub enum EthtoolRequest { + LinkModeGet = 4, + LinkModeSet = 5, + FeatureGet = 11, + FeatureSet = 12, + RingGet = 15, + RingSet = 16, + ChannelGet = 17, + ChannelSet = 18, + CoalesceGet = 19, + CoalesceSet = 20, + PauseGet = 21, + PauseSet = 22, + TsInfoGet = 25, + #[num_enum(catch_all)] + UnSupport(u8), +} -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum EthtoolCmd { - PauseGet, - PauseGetReply, - FeatureGet, - FeatureGetReply, - LinkModeGet, - LinkModeGetReply, - RingGet, - RingGetReply, - CoalesceGet, - CoalesceGetReply, - TsInfoGet, - TsInfoGetReply, +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, IntoPrimitive, FromPrimitive)] +pub enum EthtoolReply { + LinkModeGetReply = 4, + FeatureGetReply = 11, + RingGetReply = 16, + ChannelGetReply = 18, + CoalesceGetReply = 20, + PauseGetReply = 22, + TsInfoGetReply = 26, + #[num_enum(catch_all)] + UnSupport(u8), } -impl From for u8 { - fn from(cmd: EthtoolCmd) -> Self { - match cmd { - EthtoolCmd::PauseGet => ETHTOOL_MSG_PAUSE_GET, - EthtoolCmd::PauseGetReply => ETHTOOL_MSG_PAUSE_GET_REPLY, - EthtoolCmd::FeatureGet => ETHTOOL_MSG_FEATURES_GET, - EthtoolCmd::FeatureGetReply => ETHTOOL_MSG_FEATURES_GET_REPLY, - EthtoolCmd::LinkModeGet => ETHTOOL_MSG_LINKMODES_GET, - EthtoolCmd::LinkModeGetReply => ETHTOOL_MSG_LINKMODES_GET_REPLY, - EthtoolCmd::RingGet => ETHTOOL_MSG_RINGS_GET, - EthtoolCmd::RingGetReply => ETHTOOL_MSG_RINGS_GET_REPLY, - EthtoolCmd::CoalesceGet => ETHTOOL_MSG_COALESCE_GET, - EthtoolCmd::CoalesceGetReply => ETHTOOL_MSG_COALESCE_GET_REPLY, - EthtoolCmd::TsInfoGet => ETHTOOL_MSG_TSINFO_GET, - EthtoolCmd::TsInfoGetReply => ETHTOOL_MSG_TSINFO_GET_REPLY, - } - } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum EthtoolCmd { + EthtoolRequest(EthtoolRequest), + EthtoolReply(EthtoolReply), } #[derive(Debug, PartialEq, Eq, Clone)] @@ -69,10 +63,23 @@ pub enum EthtoolAttr { Feature(EthtoolFeatureAttr), LinkMode(EthtoolLinkModeAttr), Ring(EthtoolRingAttr), + Channel(EthtoolChannelAttr), Coalesce(EthtoolCoalesceAttr), TsInfo(EthtoolTsInfoAttr), } +impl From for EthtoolCmd { + fn from(cmd: EthtoolReply) -> Self { + EthtoolCmd::EthtoolReply(cmd) + } +} + +impl From for EthtoolCmd { + fn from(cmd: EthtoolRequest) -> Self { + EthtoolCmd::EthtoolRequest(cmd) + } +} + impl Nla for EthtoolAttr { fn value_len(&self) -> usize { match self { @@ -80,6 +87,7 @@ impl Nla for EthtoolAttr { Self::Feature(attr) => attr.value_len(), Self::LinkMode(attr) => attr.value_len(), Self::Ring(attr) => attr.value_len(), + Self::Channel(attr) => attr.value_len(), Self::Coalesce(attr) => attr.value_len(), Self::TsInfo(attr) => attr.value_len(), } @@ -91,6 +99,7 @@ impl Nla for EthtoolAttr { Self::Feature(attr) => attr.kind(), Self::LinkMode(attr) => attr.kind(), Self::Ring(attr) => attr.kind(), + Self::Channel(attr) => attr.kind(), Self::Coalesce(attr) => attr.kind(), Self::TsInfo(attr) => attr.kind(), } @@ -102,6 +111,7 @@ impl Nla for EthtoolAttr { Self::Feature(attr) => attr.emit_value(buffer), Self::LinkMode(attr) => attr.emit_value(buffer), Self::Ring(attr) => attr.emit_value(buffer), + Self::Channel(attr) => attr.emit_value(buffer), Self::Coalesce(attr) => attr.emit_value(buffer), Self::TsInfo(attr) => attr.emit_value(buffer), } @@ -124,7 +134,10 @@ impl GenlFamily for EthtoolMessage { } fn command(&self) -> u8 { - self.cmd.into() + match self.cmd { + EthtoolCmd::EthtoolRequest(req) => req.into(), + EthtoolCmd::EthtoolReply(reply) => reply.into(), + } } } @@ -139,7 +152,7 @@ impl EthtoolMessage { None => vec![EthtoolAttr::Pause(EthtoolPauseAttr::Header(vec![]))], }; EthtoolMessage { - cmd: EthtoolCmd::PauseGet, + cmd: EthtoolRequest::PauseGet.into(), nlas, } } @@ -156,7 +169,7 @@ impl EthtoolMessage { } }; EthtoolMessage { - cmd: EthtoolCmd::FeatureGet, + cmd: EthtoolRequest::FeatureGet.into(), nlas, } } @@ -173,7 +186,7 @@ impl EthtoolMessage { } }; EthtoolMessage { - cmd: EthtoolCmd::LinkModeGet, + cmd: EthtoolRequest::LinkModeGet.into(), nlas, } } @@ -186,7 +199,24 @@ impl EthtoolMessage { None => vec![EthtoolAttr::Ring(EthtoolRingAttr::Header(vec![]))], }; EthtoolMessage { - cmd: EthtoolCmd::RingGet, + cmd: EthtoolRequest::RingGet.into(), + nlas, + } + } + + pub fn new_channel_get(iface_name: Option<&str>) -> Self { + let nlas = match iface_name { + Some(s) => { + vec![EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![ + EthtoolHeader::DevName(s.to_string()), + ]))] + } + None => { + vec![EthtoolAttr::Channel(EthtoolChannelAttr::Header(vec![]))] + } + }; + EthtoolMessage { + cmd: EthtoolRequest::ChannelGet.into(), nlas, } } @@ -203,7 +233,7 @@ impl EthtoolMessage { } }; EthtoolMessage { - cmd: EthtoolCmd::CoalesceGet, + cmd: EthtoolRequest::CoalesceGet.into(), nlas, } } @@ -220,7 +250,7 @@ impl EthtoolMessage { } }; EthtoolMessage { - cmd: EthtoolCmd::TsInfoGet, + cmd: EthtoolRequest::TsInfoGet.into(), nlas, } } @@ -241,32 +271,36 @@ impl ParseableParametrized<[u8], GenlHeader> for EthtoolMessage { buffer: &[u8], header: GenlHeader, ) -> Result { - Ok(match header.cmd { - ETHTOOL_MSG_PAUSE_GET_REPLY => Self { - cmd: EthtoolCmd::PauseGetReply, + Ok(match EthtoolReply::from(header.cmd) { + EthtoolReply::PauseGetReply => Self { + cmd: EthtoolReply::PauseGetReply.into(), nlas: parse_pause_nlas(buffer)?, }, - ETHTOOL_MSG_FEATURES_GET_REPLY => Self { - cmd: EthtoolCmd::FeatureGetReply, + EthtoolReply::FeatureGetReply => Self { + cmd: EthtoolReply::FeatureGetReply.into(), nlas: parse_feature_nlas(buffer)?, }, - ETHTOOL_MSG_LINKMODES_GET_REPLY => Self { - cmd: EthtoolCmd::LinkModeGetReply, + EthtoolReply::LinkModeGetReply => Self { + cmd: EthtoolReply::LinkModeGetReply.into(), nlas: parse_link_mode_nlas(buffer)?, }, - ETHTOOL_MSG_RINGS_GET_REPLY => Self { - cmd: EthtoolCmd::RingGetReply, + EthtoolReply::RingGetReply => Self { + cmd: EthtoolReply::RingGetReply.into(), nlas: parse_ring_nlas(buffer)?, }, - ETHTOOL_MSG_COALESCE_GET_REPLY => Self { - cmd: EthtoolCmd::CoalesceGetReply, + EthtoolReply::ChannelGetReply => Self { + cmd: EthtoolReply::ChannelGetReply.into(), + nlas: parse_channel_nlas(buffer)?, + }, + EthtoolReply::CoalesceGetReply => Self { + cmd: EthtoolReply::CoalesceGetReply.into(), nlas: parse_coalesce_nlas(buffer)?, }, - ETHTOOL_MSG_TSINFO_GET_REPLY => Self { - cmd: EthtoolCmd::TsInfoGetReply, + EthtoolReply::TsInfoGetReply => Self { + cmd: EthtoolReply::TsInfoGetReply.into(), nlas: parse_tsinfo_nlas(buffer)?, }, - cmd => { + EthtoolReply::UnSupport(cmd) => { return Err(DecodeError::from(format!( "Unsupported ethtool reply command: {cmd}" )))