diff --git a/examples/dump_link_state.rs b/examples/dump_link_state.rs new file mode 100644 index 0000000..1b38b38 --- /dev/null +++ b/examples/dump_link_state.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +use futures_util::stream::StreamExt; + +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + let iface_name = std::env::args().nth(1); + rt.block_on(get_link_state(iface_name.as_deref())); +} + +async fn get_link_state(iface_name: Option<&str>) { + let (connection, mut handle, _) = ethtool::new_connection().unwrap(); + tokio::spawn(connection); + + let mut link_state_handle = handle + .link_state() + .get(iface_name, Some(0)) + .execute() + .await + .unwrap(); + + let mut msgs = Vec::new(); + while let Some(Ok(msg)) = link_state_handle.next().await { + msgs.push(msg); + } + assert!(!msgs.is_empty()); + for msg in msgs { + println!("{:?}", msg); + } +} diff --git a/src/handle.rs b/src/handle.rs index 95267f9..8c5c6b3 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -11,8 +11,8 @@ use netlink_packet_generic::GenlMessage; use crate::{ try_ethtool, EthtoolChannelHandle, EthtoolCoalesceHandle, EthtoolError, EthtoolFeatureHandle, EthtoolFecHandle, EthtoolLinkModeHandle, - EthtoolMessage, EthtoolModuleEEPROMHandle, EthtoolPauseHandle, - EthtoolRingHandle, EthtoolTsInfoHandle, + EthtoolLinkStateHandle, EthtoolMessage, EthtoolModuleEEPROMHandle, + EthtoolPauseHandle, EthtoolRingHandle, EthtoolTsInfoHandle, }; #[derive(Clone, Debug)] @@ -61,6 +61,10 @@ impl EthtoolHandle { EthtoolModuleEEPROMHandle::new(self.clone()) } + pub fn link_state(&mut self) -> EthtoolLinkStateHandle { + EthtoolLinkStateHandle::new(self.clone()) + } + pub async fn request( &mut self, message: NetlinkMessage>, diff --git a/src/lib.rs b/src/lib.rs index bd03001..321077f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod fec; mod handle; mod header; mod link_mode; +mod link_state; mod macros; mod message; mod pause; @@ -46,6 +47,9 @@ pub use link_mode::{ EthtoolLinkModeAttr, EthtoolLinkModeDuplex, EthtoolLinkModeGetRequest, EthtoolLinkModeHandle, }; +pub use link_state::{ + EthtoolLinkStateAttr, EthtoolLinkStateGetRequest, EthtoolLinkStateHandle, +}; pub use message::{EthtoolAttr, EthtoolCmd, EthtoolMessage}; pub use pause::{ EthtoolPauseAttr, EthtoolPauseGetRequest, EthtoolPauseHandle, diff --git a/src/link_state/attr.rs b/src/link_state/attr.rs new file mode 100644 index 0000000..af35a7b --- /dev/null +++ b/src/link_state/attr.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_core::{ + parse_u32, parse_u8, DecodeError, DefaultNla, Emitable, ErrorContext, Nla, + NlaBuffer, NlasIterator, Parseable, NLA_F_NESTED, +}; + +use crate::{EthtoolAttr, EthtoolHeader}; + +const ETHTOOL_A_LINKSTATE_HEADER: u16 = 1; +const ETHTOOL_A_LINKSTATE_LINK: u16 = 2; +const ETHTOOL_A_LINKSTATE_SQI: u16 = 3; +const ETHTOOL_A_LINKSTATE_SQI_MAX: u16 = 4; +const ETHTOOL_A_LINKSTATE_EXT_STATE: u16 = 5; +const ETHTOOL_A_LINKSTATE_EXT_SUBSTATE: u16 = 6; +const ETHTOOL_A_LINKSTATE_EXT_DOWN_CNT: u16 = 7; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum EthtoolLinkStateAttr { + Header(Vec), + Link(bool), + Sqi(u32), + SqiMax(u32), + ExtState(u8), + ExtSubstate(u8), + ExtDownCnt(u32), + Other(DefaultNla), +} + +impl Nla for EthtoolLinkStateAttr { + fn value_len(&self) -> usize { + match self { + Self::Header(hdrs) => hdrs.as_slice().buffer_len(), + Self::Link(_) | Self::ExtState(_) | Self::ExtSubstate(_) => 1, + Self::Sqi(_) | Self::SqiMax(_) | Self::ExtDownCnt(_) => 4, + Self::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Header(_) => ETHTOOL_A_LINKSTATE_HEADER | NLA_F_NESTED, + Self::Link(_) => ETHTOOL_A_LINKSTATE_LINK, + Self::Sqi(_) => ETHTOOL_A_LINKSTATE_SQI, + Self::SqiMax(_) => ETHTOOL_A_LINKSTATE_SQI_MAX, + Self::ExtState(_) => ETHTOOL_A_LINKSTATE_EXT_STATE, + Self::ExtSubstate(_) => ETHTOOL_A_LINKSTATE_EXT_SUBSTATE, + Self::ExtDownCnt(_) => ETHTOOL_A_LINKSTATE_EXT_DOWN_CNT, + Self::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + match self { + Self::Header(ref nlas) => nlas.as_slice().emit(buffer), + Self::Other(ref attr) => attr.emit(buffer), + _ => todo!("Does not support changing link state"), + } + } +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for EthtoolLinkStateAttr +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let payload = buf.value(); + Ok(match buf.kind() { + ETHTOOL_A_LINKSTATE_HEADER => { + let mut nlas = Vec::new(); + let error_msg = "failed to parse link state 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_LINKSTATE_LINK => Self::Link( + parse_u8(payload) + .context("invalid ETHTOOL_A_LINKSTATE_LINK value")? + == 1, + ), + ETHTOOL_A_LINKSTATE_SQI => Self::Sqi( + parse_u32(payload) + .context("invalid ETHTOOL_A_LINKSTATE_SQI value")?, + ), + ETHTOOL_A_LINKSTATE_SQI_MAX => Self::SqiMax( + parse_u32(payload) + .context("invalid ETHTOOL_A_LINKSTATE_SQI_MAX value")?, + ), + ETHTOOL_A_LINKSTATE_EXT_STATE => Self::ExtState( + parse_u8(payload) + .context("invalid ETHTOOL_A_LINKSTATE_EXT_STATE value")?, + ), + ETHTOOL_A_LINKSTATE_EXT_SUBSTATE => { + Self::ExtSubstate(parse_u8(payload).context( + "invalid ETHTOOL_A_LINKSTATE_EXT_SUBSTATE value", + )?) + } + ETHTOOL_A_LINKSTATE_EXT_DOWN_CNT => { + Self::ExtDownCnt(parse_u32(payload).context( + "invalid ETHTOOL_A_LINKSTATE_EXT_DOWN_CNT value", + )?) + } + _ => Self::Other( + DefaultNla::parse(buf).context("invalid NLA (unknown kind)")?, + ), + }) + } +} + +pub(crate) fn parse_link_state_nlas( + buffer: &[u8], +) -> Result, DecodeError> { + let mut nlas = Vec::new(); + for nla in NlasIterator::new(buffer) { + let error_msg = format!( + "Failed to parse ethtool link state message attribute {nla:?}" + ); + let nla = &nla.context(error_msg.clone())?; + let parsed = EthtoolLinkStateAttr::parse(nla).context(error_msg)?; + nlas.push(EthtoolAttr::LinkState(parsed)); + } + Ok(nlas) +} diff --git a/src/link_state/get.rs b/src/link_state/get.rs new file mode 100644 index 0000000..b31eff1 --- /dev/null +++ b/src/link_state/get.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +use futures_util::Stream; +use netlink_packet_generic::GenlMessage; + +use crate::{ethtool_execute, EthtoolError, EthtoolHandle, EthtoolMessage}; + +pub struct EthtoolLinkStateGetRequest { + handle: EthtoolHandle, + iface_name: Option, + flags: Option, +} + +impl EthtoolLinkStateGetRequest { + pub(crate) fn new( + handle: EthtoolHandle, + iface_name: Option<&str>, + flags: Option, + ) -> Self { + EthtoolLinkStateGetRequest { + handle, + iface_name: iface_name.map(|i| i.to_string()), + flags, + } + } + + pub async fn execute( + self, + ) -> Result< + impl Stream, EthtoolError>>, + EthtoolError, + > { + let EthtoolLinkStateGetRequest { + mut handle, + iface_name, + flags, + } = self; + + let ethtool_msg = + EthtoolMessage::new_link_state_get(iface_name.as_deref(), flags); + ethtool_execute(&mut handle, iface_name.is_none(), ethtool_msg).await + } +} diff --git a/src/link_state/handle.rs b/src/link_state/handle.rs new file mode 100644 index 0000000..0360a08 --- /dev/null +++ b/src/link_state/handle.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +use crate::{EthtoolHandle, EthtoolLinkStateGetRequest}; + +pub struct EthtoolLinkStateHandle(EthtoolHandle); + +impl EthtoolLinkStateHandle { + pub fn new(handle: EthtoolHandle) -> Self { + EthtoolLinkStateHandle(handle) + } + + /// Retrieve the current link state of an interface + pub fn get( + &mut self, + iface_name: Option<&str>, + flags: Option, + ) -> EthtoolLinkStateGetRequest { + EthtoolLinkStateGetRequest::new(self.0.clone(), iface_name, flags) + } +} diff --git a/src/link_state/mod.rs b/src/link_state/mod.rs new file mode 100644 index 0000000..8850e09 --- /dev/null +++ b/src/link_state/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +mod attr; +mod get; +mod handle; + +pub(crate) use attr::parse_link_state_nlas; + +pub use attr::EthtoolLinkStateAttr; +pub use get::EthtoolLinkStateGetRequest; +pub use handle::EthtoolLinkStateHandle; diff --git a/src/message.rs b/src/message.rs index 6274cc7..a4a0e5c 100644 --- a/src/message.rs +++ b/src/message.rs @@ -10,6 +10,7 @@ use crate::{ feature::{parse_feature_nlas, EthtoolFeatureAttr}, fec::{parse_fec_nlas, EthtoolFecAttr}, link_mode::{parse_link_mode_nlas, EthtoolLinkModeAttr}, + link_state::{parse_link_state_nlas, EthtoolLinkStateAttr}, pause::{parse_pause_nlas, EthtoolPauseAttr}, ring::{parse_ring_nlas, EthtoolRingAttr}, tsinfo::{parse_tsinfo_nlas, EthtoolTsInfoAttr}, @@ -33,6 +34,8 @@ const ETHTOOL_MSG_FEC_GET_REPLY: u8 = 30; const ETHTOOL_MSG_CHANNELS_GET: u8 = 17; const ETHTOOL_MSG_CHANNELS_GET_REPLY: u8 = 18; const ETHTOOL_MSG_CHANNELS_SET: u8 = 18; +const ETHTOOL_MSG_LINKSTATE_GET: u8 = 6; +const ETHTOOL_MSG_LINKSTATE_GET_REPLY: u8 = 6; const ETHTOOL_MSG_MODULE_EEPROM_GET: u8 = 31; const ETHTOOL_MSG_MODULE_EEPROM_GET_REPLY: u8 = 32; @@ -55,6 +58,8 @@ pub enum EthtoolCmd { ChannelGet, ChannelGetReply, ChannelSet, + LinkStateGet, + LinkStateGetReply, ModuleEEPROMGet, ModuleEEPROMGetReply, } @@ -83,6 +88,8 @@ impl From for u8 { EthtoolCmd::ModuleEEPROMGetReply => { ETHTOOL_MSG_MODULE_EEPROM_GET_REPLY } + EthtoolCmd::LinkStateGet => ETHTOOL_MSG_LINKSTATE_GET, + EthtoolCmd::LinkStateGetReply => ETHTOOL_MSG_LINKSTATE_GET_REPLY, } } } @@ -98,6 +105,7 @@ pub enum EthtoolAttr { Fec(EthtoolFecAttr), Channel(EthtoolChannelAttr), ModuleEEPROM(EthtoolModuleEEPROMAttr), + LinkState(EthtoolLinkStateAttr), } impl Nla for EthtoolAttr { @@ -112,6 +120,7 @@ impl Nla for EthtoolAttr { Self::Fec(attr) => attr.value_len(), Self::Channel(attr) => attr.value_len(), Self::ModuleEEPROM(attr) => attr.value_len(), + Self::LinkState(attr) => attr.value_len(), } } @@ -126,6 +135,7 @@ impl Nla for EthtoolAttr { Self::Fec(attr) => attr.kind(), Self::Channel(attr) => attr.kind(), Self::ModuleEEPROM(attr) => attr.kind(), + Self::LinkState(attr) => attr.kind(), } } @@ -140,6 +150,7 @@ impl Nla for EthtoolAttr { Self::Fec(attr) => attr.emit_value(buffer), Self::Channel(attr) => attr.emit_value(buffer), Self::ModuleEEPROM(attr) => attr.emit_value(buffer), + Self::LinkState(attr) => attr.emit_value(buffer), } } } @@ -349,6 +360,25 @@ impl EthtoolMessage { nlas, } } + + pub fn new_link_state_get( + iface_name: Option<&str>, + flags: Option, + ) -> Self { + let mut header = vec![]; + if let Some(s) = iface_name { + header.push(EthtoolHeader::DevName(s.to_string())) + } + if let Some(f) = flags { + header.push(EthtoolHeader::Flags(f)) + } + let nlas = + vec![EthtoolAttr::LinkState(EthtoolLinkStateAttr::Header(header))]; + EthtoolMessage { + cmd: EthtoolCmd::LinkStateGet, + nlas, + } + } } impl Emitable for EthtoolMessage { @@ -403,6 +433,10 @@ impl ParseableParametrized<[u8], GenlHeader> for EthtoolMessage { cmd: EthtoolCmd::ModuleEEPROMGetReply, nlas: parse_module_eeprom_nlas(buffer)?, }, + ETHTOOL_MSG_LINKSTATE_GET_REPLY => Self { + cmd: EthtoolCmd::LinkStateGetReply, + nlas: parse_link_state_nlas(buffer)?, + }, cmd => { return Err(DecodeError::from(format!( "Unsupported ethtool reply command: {cmd}"