diff --git a/Cargo.toml b/Cargo.toml index 095436b..9c53a46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ version = "0.2.0" authors = ["Gris Ge "] license = "MIT" edition = "2021" +rust-version = "1.87" description = "Linux kernel wireless(802.11) netlink Library" homepage = "https://github.com/rust-netlink/wl-nl80211" repository = "https://github.com/rust-netlink/wl-nl80211" diff --git a/examples/dump_nl80211_survey.rs b/examples/dump_nl80211_survey.rs new file mode 100644 index 0000000..37f020e --- /dev/null +++ b/examples/dump_nl80211_survey.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +use std::env::args; + +use anyhow::{bail, Context, Error}; +use futures::stream::TryStreamExt; + +fn main() -> Result<(), Error> { + let argv: Vec<_> = args().collect(); + + if argv.len() < 2 { + eprintln!("Usage: dump_nl80211_survey "); + bail!("Required arguments not given"); + } + + let err_msg = format!("Invalid interface index value: {}", argv[1]); + let index = argv[1].parse::().context(err_msg)?; + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(dump_survey(index))?; + + Ok(()) +} + +async fn dump_survey(if_index: u32) -> Result<(), Error> { + let (connection, handle, _) = wl_nl80211::new_connection()?; + tokio::spawn(connection); + + let mut survey_handle = handle + .survey() + .dump(wl_nl80211::Nl80211Survey::new(if_index).radio(true).build()) + .execute() + .await; + + let mut msgs = Vec::new(); + while let Some(msg) = survey_handle.try_next().await? { + msgs.push(msg); + } + assert!(!msgs.is_empty()); + for msg in msgs { + println!("{:?}", msg); + } + Ok(()) +} diff --git a/examples/nl80211_trigger_scan.rs b/examples/nl80211_trigger_scan.rs index 74ad307..8328780 100644 --- a/examples/nl80211_trigger_scan.rs +++ b/examples/nl80211_trigger_scan.rs @@ -21,35 +21,37 @@ fn main() -> Result<(), Error> { .enable_time() .build() .unwrap(); - rt.block_on(dump_scan(index)); + rt.block_on(dump_scan(index))?; Ok(()) } -async fn dump_scan(if_index: u32) { - let (connection, handle, _) = wl_nl80211::new_connection().unwrap(); +async fn dump_scan(if_index: u32) -> Result<(), Error> { + let (connection, handle, _) = wl_nl80211::new_connection()?; tokio::spawn(connection); + let duration = 5000; let attrs = wl_nl80211::Nl80211Scan::new(if_index) - .duration(5000) + .duration(duration) .passive(true) .build(); let mut scan_handle = handle.scan().trigger(attrs).execute().await; let mut msgs = Vec::new(); - while let Some(msg) = scan_handle.try_next().await.unwrap() { + while let Some(msg) = scan_handle.try_next().await? { msgs.push(msg); } - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + tokio::time::sleep(std::time::Duration::from_millis(duration.into())).await; let mut dump = handle.scan().dump(if_index).execute().await; let mut msgs = Vec::new(); - while let Some(msg) = dump.try_next().await.unwrap() { + while let Some(msg) = dump.try_next().await? { msgs.push(msg); } assert!(!msgs.is_empty()); for msg in msgs { println!("{msg:?}"); } + Ok(()) } diff --git a/src/attr.rs b/src/attr.rs index d5b90a2..094f1df 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -47,8 +47,8 @@ use crate::{ Nl80211IfTypeExtCapas, Nl80211IfaceComb, Nl80211IfaceFrameType, Nl80211InterfaceType, Nl80211InterfaceTypes, Nl80211MloLink, Nl80211ScanFlags, Nl80211SchedScanMatch, Nl80211SchedScanPlan, - Nl80211StationInfo, Nl80211TransmitQueueStat, Nl80211VhtCapability, - Nl80211WowlanTriggersSupport, + Nl80211StationInfo, Nl80211SurveyInfo, Nl80211TransmitQueueStat, + Nl80211VhtCapability, Nl80211WowlanTriggersSupport, }; const ETH_ALEN: usize = 6; @@ -204,7 +204,7 @@ const NL80211_ATTR_WIPHY_RTS_THRESHOLD: u16 = 64; // const NL80211_ATTR_KEYS:u16 = 81; // const NL80211_ATTR_PID:u16 = 82; const NL80211_ATTR_4ADDR: u16 = 83; -// const NL80211_ATTR_SURVEY_INFO:u16 = 84; +const NL80211_ATTR_SURVEY_INFO: u16 = 84; // const NL80211_ATTR_PMKID:u16 = 85; const NL80211_ATTR_MAX_NUM_PMKIDS: u16 = 86; // const NL80211_ATTR_DURATION:u16 = 87; @@ -339,7 +339,7 @@ const NL80211_ATTR_MAX_CSA_COUNTERS: u16 = 206; const NL80211_ATTR_MAC_MASK: u16 = 215; const NL80211_ATTR_WIPHY_SELF_MANAGED_REG: u16 = 216; const NL80211_ATTR_EXT_FEATURES: u16 = 217; -// const NL80211_ATTR_SURVEY_RADIO_STATS:u16 = 218; +const NL80211_ATTR_SURVEY_RADIO_STATS: u16 = 218; // const NL80211_ATTR_NETNS_FD:u16 = 219; const NL80211_ATTR_SCHED_SCAN_DELAY: u16 = 220; // const NL80211_ATTR_REG_INDOOR:u16 = 221; @@ -480,6 +480,7 @@ pub enum Nl80211Attr { WiphyTxPowerLevel(u32), Ssid(String), StationInfo(Vec), + SurveyInfo(Vec), TransmitQueueStats(Vec), TransmitQueueLimit(u32), TransmitQueueMemoryLimit(u32), @@ -519,6 +520,7 @@ pub enum Nl80211Attr { /// in milliseconds MaxRemainOnChannelDuration(u32), OffchannelTxOk, + SurveyRadioStats, WowlanTriggersSupport(Vec), SoftwareIftypes(Vec), Features(Nl80211Features), @@ -625,6 +627,7 @@ impl Nla for Nl80211Attr { | Self::MaxNumPmkids(_) => 1, Self::TransmitQueueStats(nlas) => nlas.as_slice().buffer_len(), Self::StationInfo(nlas) => nlas.as_slice().buffer_len(), + Self::SurveyInfo(nlas) => nlas.as_slice().buffer_len(), Self::MloLinks(links) => links.as_slice().buffer_len(), Self::MaxScanIeLen(_) | Self::MaxSchedScanIeLen(_) => 2, Self::SupportIbssRsn @@ -635,6 +638,7 @@ impl Nla for Nl80211Attr { | Self::TdlsExternalSetup | Self::ControlPortEthertype | Self::OffchannelTxOk + | Self::SurveyRadioStats | Self::WiphySelfManagedReg => 0, Self::CipherSuites(s) => 4 * s.len(), Self::SupportedIftypes(s) => s.as_slice().buffer_len(), @@ -704,6 +708,8 @@ impl Nla for Nl80211Attr { Self::WiphyTxPowerLevel(_) => NL80211_ATTR_WIPHY_TX_POWER_LEVEL, Self::Ssid(_) => NL80211_ATTR_SSID, Self::StationInfo(_) => NL80211_ATTR_STA_INFO, + Self::SurveyInfo(_) => NL80211_ATTR_SURVEY_INFO, + Self::SurveyRadioStats => NL80211_ATTR_SURVEY_RADIO_STATS, Self::TransmitQueueStats(_) => NL80211_ATTR_TXQ_STATS, Self::TransmitQueueLimit(_) => NL80211_ATTR_TXQ_LIMIT, Self::TransmitQueueMemoryLimit(_) => NL80211_ATTR_TXQ_MEMORY_LIMIT, @@ -836,10 +842,12 @@ impl Nla for Nl80211Attr { | Self::TdlsExternalSetup | Self::ControlPortEthertype | Self::OffchannelTxOk + | Self::SurveyRadioStats | Self::WiphySelfManagedReg => (), Self::WiphyChannelType(d) => write_u32(buffer, (*d).into()), Self::ChannelWidth(d) => write_u32(buffer, (*d).into()), Self::StationInfo(nlas) => nlas.as_slice().emit(buffer), + Self::SurveyInfo(nlas) => nlas.as_slice().emit(buffer), Self::TransmitQueueStats(nlas) => nlas.as_slice().emit(buffer), Self::MloLinks(links) => links.as_slice().emit(buffer), Self::WiphyRetryShort(d) @@ -1048,6 +1056,21 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> for Nl80211Attr { } Self::StationInfo(nlas) } + NL80211_ATTR_SURVEY_INFO => { + let err_msg = format!( + "Invalid NL80211_ATTR_SURVEY_INFO value {payload:?}" + ); + let mut nlas = Vec::new(); + for nla in NlasIterator::new(payload) { + let nla = &nla.context(err_msg.clone())?; + nlas.push( + Nl80211SurveyInfo::parse(nla) + .context(err_msg.clone())?, + ); + } + Self::SurveyInfo(nlas) + } + NL80211_ATTR_SURVEY_RADIO_STATS => Self::SurveyRadioStats, NL80211_ATTR_TXQ_STATS => { let err_msg = format!("Invalid NL80211_ATTR_TXQ_STATS value {payload:?}"); diff --git a/src/handle.rs b/src/handle.rs index 5761041..a5fef77 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,7 +8,8 @@ use netlink_packet_utils::DecodeError; use crate::{ try_nl80211, Nl80211Error, Nl80211InterfaceHandle, Nl80211Message, - Nl80211ScanHandle, Nl80211StationHandle, Nl80211WiphyHandle, + Nl80211ScanHandle, Nl80211StationHandle, Nl80211SurveyHandle, + Nl80211WiphyHandle, }; #[derive(Clone, Debug)] @@ -41,6 +42,11 @@ impl Nl80211Handle { Nl80211ScanHandle::new(self.clone()) } + // equivalent to `iw DEVICE survey dump` command + pub fn survey(&self) -> Nl80211SurveyHandle { + Nl80211SurveyHandle::new(self.clone()) + } + pub async fn request( &mut self, message: NetlinkMessage>, diff --git a/src/lib.rs b/src/lib.rs index 84148b0..df74fd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ mod mlo; mod scan; mod station; mod stats; +mod survey; mod wifi4; mod wifi5; mod wifi6; @@ -63,6 +64,10 @@ pub use self::station::{ pub use self::stats::{ NestedNl80211TidStats, Nl80211TidStats, Nl80211TransmitQueueStat, }; +pub use self::survey::{ + Nl80211Survey, Nl80211SurveyGetRequest, Nl80211SurveyHandle, + Nl80211SurveyInfo, +}; pub use self::wifi4::{ Nl80211ElementHtCap, Nl80211HtAMpduPara, Nl80211HtAselCaps, Nl80211HtCapabilityMask, Nl80211HtCaps, Nl80211HtExtendedCap, diff --git a/src/survey/get.rs b/src/survey/get.rs new file mode 100644 index 0000000..95dac30 --- /dev/null +++ b/src/survey/get.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +use futures::TryStream; +use netlink_packet_core::{NLM_F_DUMP, NLM_F_REQUEST}; +use netlink_packet_generic::GenlMessage; + +use crate::{ + nl80211_execute, Nl80211Attr, Nl80211Command, Nl80211Error, Nl80211Handle, + Nl80211Message, +}; + +pub struct Nl80211SurveyGetRequest { + handle: Nl80211Handle, + attributes: Vec, +} + +impl Nl80211SurveyGetRequest { + pub(crate) fn new( + handle: Nl80211Handle, + attributes: Vec, + ) -> Self { + Self { handle, attributes } + } + + pub async fn execute( + self, + ) -> impl TryStream, Error = Nl80211Error> + { + let Self { + mut handle, + attributes, + } = self; + + let nl80211_msg = Nl80211Message { + cmd: Nl80211Command::GetSurvey, + attributes, + }; + + let flags = NLM_F_REQUEST | NLM_F_DUMP; + + nl80211_execute(&mut handle, nl80211_msg, flags).await + } +} diff --git a/src/survey/handle.rs b/src/survey/handle.rs new file mode 100644 index 0000000..d6e6fb9 --- /dev/null +++ b/src/survey/handle.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +use netlink_packet_utils::nla::Nla; + +use crate::{ + Nl80211Attr, Nl80211AttrsBuilder, Nl80211Handle, Nl80211SurveyGetRequest, +}; + +pub struct Nl80211SurveyHandle(Nl80211Handle); + +#[derive(Debug)] +pub struct Nl80211Survey; + +impl Nl80211Survey { + /// Perform a survey dump + pub fn new(if_index: u32) -> Nl80211AttrsBuilder { + Nl80211AttrsBuilder::::new().if_index(if_index) + } +} + +impl Nl80211AttrsBuilder { + /// Request overall radio statistics to be returned along with other survey + /// data + pub fn radio(self, value: bool) -> Self { + if value { + self.replace(Nl80211Attr::SurveyRadioStats) + } else { + self.remove(Nl80211Attr::SurveyRadioStats.kind()) + } + } +} + +impl Nl80211SurveyHandle { + pub fn new(handle: Nl80211Handle) -> Self { + Self(handle) + } + + /// Retrieve the survey info + /// (equivalent to `iw dev DEV survey dump`) + pub fn dump( + &mut self, + attributes: Vec, + ) -> Nl80211SurveyGetRequest { + Nl80211SurveyGetRequest::new(self.0.clone(), attributes) + } +} diff --git a/src/survey/mod.rs b/src/survey/mod.rs new file mode 100644 index 0000000..60bedf3 --- /dev/null +++ b/src/survey/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +mod get; +mod handle; +mod survey_info; + +pub use self::get::Nl80211SurveyGetRequest; +pub use self::handle::{Nl80211Survey, Nl80211SurveyHandle}; +pub use self::survey_info::Nl80211SurveyInfo; diff --git a/src/survey/survey_info.rs b/src/survey/survey_info.rs new file mode 100644 index 0000000..bddb904 --- /dev/null +++ b/src/survey/survey_info.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT + +// Most documentation comments are copied and modified from linux kernel +// include/uapi/linux/nl80211.h which is holding these license disclaimer: +/* + * 802.11 netlink interface public header + * + * Copyright 2006-2010 Johannes Berg + * Copyright 2008 Michael Wu + * Copyright 2008 Luis Carlos Cobo + * Copyright 2008 Michael Buesch + * Copyright 2008, 2009 Luis R. Rodriguez + * Copyright 2008 Jouni Malinen + * Copyright 2008 Colin McCabe + * Copyright 2015-2017 Intel Deutschland GmbH + * Copyright (C) 2018-2024 Intel Corporation + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ +use std::fmt::Debug; + +use anyhow::Context; +use netlink_packet_utils::{ + nla::{DefaultNla, Nla, NlaBuffer}, + parsers::{parse_u32, parse_u64}, + DecodeError, Emitable, Parseable, +}; + +use crate::bytes::{write_u32, write_u64}; + +const NL80211_SURVEY_INFO_FREQUENCY: u16 = 1; +const NL80211_SURVEY_INFO_NOISE: u16 = 2; +// const NL80211_SURVEY_INFO_IN_USE: u16 = 3; +const NL80211_SURVEY_INFO_TIME: u16 = 4; +const NL80211_SURVEY_INFO_TIME_BUSY: u16 = 5; +const NL80211_SURVEY_INFO_TIME_EXT_BUSY: u16 = 6; +const NL80211_SURVEY_INFO_TIME_RX: u16 = 7; +const NL80211_SURVEY_INFO_TIME_TX: u16 = 8; +// const NL80211_SURVEY_INFO_TIME_SCAN: u16 = 9; +// const NL80211_SURVEY_INFO_PAD: u16 = 10; +// const NL80211_SURVEY_INFO_TIME_BSS_RX: u16 = 11; +// const NL80211_SURVEY_INFO_FREQUENCY_OFFSET: u16 = 12; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum Nl80211SurveyInfo { + // Center frequency of channel in MHz + Frequency(u32), + /// Noise level of channel (dBm) + Noise(i8), + /// Amount of time (in ms) that the radio was turned on (on channel or + /// globally) + ActiveTime(u64), + /// Amount of the time the primary channel was sensed busy (either due to + /// activity or energy detect) + BusyTime(u64), + /// Amount of time the extension channel was sensed busy + ExtensionBusyTime(u64), + /// Amount of time the radio spent receiving data (on channel or globally) + TimeRx(u64), + /// Amount of time the radio spent transmitting data (on channel or + /// globally) + TimeTx(u64), + Other(DefaultNla), +} + +impl Nla for Nl80211SurveyInfo { + fn value_len(&self) -> usize { + match self { + Self::Frequency(v) => std::mem::size_of_val(v), + Self::Noise(v) => std::mem::size_of_val(v), + Self::ActiveTime(v) + | Self::BusyTime(v) + | Self::ExtensionBusyTime(v) + | Self::TimeRx(v) + | Self::TimeTx(v) => std::mem::size_of_val(v), + Self::Other(attr) => attr.value_len(), + } + } + + fn kind(&self) -> u16 { + match self { + Self::Frequency(_) => NL80211_SURVEY_INFO_FREQUENCY, + Self::Noise(_) => NL80211_SURVEY_INFO_NOISE, + Self::ActiveTime(_) => NL80211_SURVEY_INFO_TIME, + Self::BusyTime(_) => NL80211_SURVEY_INFO_TIME_BUSY, + Self::ExtensionBusyTime(_) => NL80211_SURVEY_INFO_TIME_EXT_BUSY, + Self::TimeRx(_) => NL80211_SURVEY_INFO_TIME_RX, + Self::TimeTx(_) => NL80211_SURVEY_INFO_TIME_TX, + Self::Other(attr) => attr.kind(), + } + } + + fn emit_value(&self, buffer: &mut [u8]) { + #[allow(clippy::cast_sign_loss)] + match self { + Self::Frequency(d) => write_u32(buffer, *d), + Self::Noise(d) => buffer[0] = d.cast_unsigned(), + Self::ActiveTime(d) + | Self::BusyTime(d) + | Self::ExtensionBusyTime(d) + | Self::TimeRx(d) + | Self::TimeTx(d) => write_u64(buffer, *d), + Self::Other(ref attr) => attr.emit(buffer), + } + } +} + +// TODO: Use netlink_packet_utils version, once released. +pub fn parse_i8(payload: &[u8]) -> Result { + if payload.len() != 1 { + return Err(format!("invalid i8: {payload:?}").into()); + } + #[allow(clippy::cast_possible_wrap)] + Ok(payload[0].cast_signed()) +} + +impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> + for Nl80211SurveyInfo +{ + fn parse(buf: &NlaBuffer<&'a T>) -> Result { + let payload = buf.value(); + Ok(match buf.kind() { + NL80211_SURVEY_INFO_FREQUENCY => { + let err_msg = format!( + "Invalid NL80211_SURVEY_INFO_FREQUENCY value {payload:?}" + ); + Self::Frequency(parse_u32(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_NOISE => { + let err_msg = format!( + "Invalid NL80211_SURVEY_INFO_NOISE value {payload:?}" + ); + // In kernel level, this is treated as a u8, but `iw` and the + // real word treat this as a two's complement + // 8bit singed integer. + Self::Noise(parse_i8(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_TIME => { + let err_msg = format!( + "Invalid {} value {payload:?}", + stringify!(NL80211_SURVEY_INFO_TIME), + ); + Self::ActiveTime(parse_u64(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_TIME_BUSY => { + let err_msg = format!( + "Invalid {} value {payload:?}", + stringify!(NL80211_SURVEY_INFO_TIME_BUSY), + ); + Self::ExtensionBusyTime(parse_u64(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_TIME_EXT_BUSY => { + let err_msg = format!( + "Invalid {} value {payload:?}", + stringify!(NL80211_SURVEY_INFO_TIME_EXT_BUSY), + ); + Self::ExtensionBusyTime(parse_u64(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_TIME_RX => { + let err_msg = format!( + "Invalid {} value {payload:?}", + stringify!(NL80211_SURVEY_INFO_TIME_RX), + ); + Self::TimeTx(parse_u64(payload).context(err_msg)?) + } + NL80211_SURVEY_INFO_TIME_TX => { + let err_msg = format!( + "Invalid {} value {payload:?}", + stringify!(NL80211_SURVEY_INFO_TIME_TX), + ); + Self::TimeRx(parse_u64(payload).context(err_msg)?) + } + _ => Self::Other( + DefaultNla::parse(buf).context("invalid NLA (unknown kind)")?, + ), + }) + } +}