#[cfg(any(target_os = "linux", target_os = "windows"))]
use std::time::Duration;
use std::{fmt::Display, hash::Hash};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use super::CapabilityInfo;
#[cfg(target_os = "linux")]
use crate::nl80211::{BssScanWidth, BssStatus};
use crate::{
Band, ChannelWidth, SecurityProtocols, WifiAmendments, WifiProtocols,
ies::{Ie, IeData},
};
#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
pub struct Bss {
pub(super) bssid: [u8; 6],
pub(super) frequency_mhz: u32,
pub(super) signal_dbm: i32,
pub(super) beacon_interval_tu: u16,
#[serde(with = "crate::ies::serde_raw::ies_as_base64")]
pub(super) ies: Vec<Ie>,
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(super) tsf: u64,
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[serde(with = "crate::ies::serde_raw::capability_info_as_u16")]
pub(super) capability_info: CapabilityInfo,
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(super) last_seen_utc: Option<DateTime<Utc>>,
#[cfg(target_os = "linux")]
pub(super) status: Option<BssStatus>,
#[cfg(target_os = "linux")]
pub(super) is_from_probe_response: bool,
#[cfg(target_os = "linux")]
pub(super) parent_bssid: Option<[u8; 6]>,
#[cfg(target_os = "linux")]
pub(super) parent_tsf: Option<u64>,
#[cfg(target_os = "linux")]
pub(super) beacon_tsf: Option<u64>,
#[cfg(target_os = "linux")]
pub(super) frequency_offset_khz: Option<u32>,
#[cfg(target_os = "linux")]
pub(super) signal_percent: Option<u8>,
#[cfg(target_os = "linux")]
#[serde(with = "crate::ies::serde_raw::option_ies_as_base64")]
pub(super) beacon_ies: Option<Vec<Ie>>,
#[cfg(target_os = "linux")]
pub(super) scan_width: Option<BssScanWidth>,
#[cfg(target_os = "linux")]
pub(super) last_seen_boottime: Option<u64>,
#[cfg(target_os = "linux")]
pub(super) seen_ms_ago: Option<u32>,
#[cfg(target_os = "linux")]
pub(super) mlo_link_id: Option<u8>,
#[cfg(target_os = "linux")]
pub(super) mld_address: Option<[u8; 6]>,
#[cfg(target_os = "windows")]
pub(super) link_quality: u8,
#[cfg(target_os = "macos")]
pub(super) noise_dbm: i32,
#[cfg(target_os = "macos")]
pub(super) is_wep: bool,
}
impl Bss {
pub fn bssid(&self) -> &[u8; 6] {
&self.bssid
}
pub fn frequency_mhz(&self) -> u32 {
self.frequency_mhz
}
pub fn band(&self) -> Band {
Band::from_freq_mhz(self.frequency_mhz)
}
pub fn channel_width(&self) -> ChannelWidth {
ChannelWidth::from(self.ies())
}
pub fn center_frequency_mhz(&self) -> u32 {
use crate::ies::ht_operation::SecondaryChannelOffset;
let primary = self.frequency_mhz;
let band = self.band();
let chan_to_freq = |chan: u8| -> u32 {
match band {
Band::FiveGhz => 5000 + (chan as u32 * 5),
Band::SixGhz => 5950 + (chan as u32 * 5),
Band::TwoPointFourGhz => primary,
}
};
let (mut eht_op, mut he_op, mut vht_op, mut ht_op) = (None, None, None, None);
for ie in &self.ies {
match &ie.data {
IeData::EhtOperation(op) => eht_op = Some(op),
IeData::HeOperation(op) => he_op = Some(op),
IeData::VhtOperation(op) => vht_op = Some(op),
IeData::HtOperation(op) => ht_op = Some(op),
_ => {}
}
}
if let Some(eht_op) = eht_op
&& let Some(info) = eht_op.eht_operation_information
{
if info.ccfs1 != 0 {
return chan_to_freq(info.ccfs1);
}
if info.ccfs0 != 0 {
return chan_to_freq(info.ccfs0);
}
}
if let Some(he_op) = he_op {
if let Some(six_ghz) = &he_op.six_ghz_operation_information {
let (ccfs0, ccfs1) = (
six_ghz.channel_center_frequency_segment_0,
six_ghz.channel_center_frequency_segment_1,
);
if ccfs1 != 0 && ccfs1.abs_diff(ccfs0) == 8 {
return chan_to_freq(ccfs1);
}
if ccfs0 != 0 {
return chan_to_freq(ccfs0);
}
}
if let Some(vht_info) = &he_op.vht_operation_information {
let (ccfs0, ccfs1) = (
vht_info.channel_center_frequency_segment_0,
vht_info.channel_center_frequency_segment_1,
);
if ccfs1 != 0 && ccfs1.abs_diff(ccfs0) == 8 {
return chan_to_freq(ccfs1);
}
if ccfs0 != 0 {
return chan_to_freq(ccfs0);
}
}
}
if let Some(vht_op) = vht_op {
let info = &vht_op.vht_operation_information;
let (ccfs0, ccfs1) = (
info.channel_center_frequency_segment_0,
info.channel_center_frequency_segment_1,
);
if ccfs1 != 0 && ccfs1.abs_diff(ccfs0) == 8 {
return chan_to_freq(ccfs1);
}
if ccfs0 != 0 {
return chan_to_freq(ccfs0);
}
}
if let Some(ht_op) = ht_op {
match ht_op.ht_operation_information.secondary_channel_offset {
SecondaryChannelOffset::AbovePrimary => return primary + 10,
SecondaryChannelOffset::BelowPrimary => return primary - 10,
SecondaryChannelOffset::NoSecondary => {}
}
}
primary
}
pub fn channel_number(&self) -> u8 {
self.ies
.iter()
.find_map(|ie| match &ie.data {
IeData::DsParameterSet(ds_parameter_set) => Some(ds_parameter_set.current_channel),
IeData::HtOperation(ht_operation) => Some(ht_operation.primary_channel),
_ => None,
})
.unwrap_or_else(|| {
let freq = self.frequency_mhz();
if Band::TwoPointFourGhz.range_mhz().contains(&freq) {
if freq == 2484 {
14
} else {
((freq - 2407) / 5) as u8
}
} else if Band::FiveGhz.range_mhz().contains(&freq) {
((freq - 5000) / 5) as u8
} else if Band::SixGhz.range_mhz().contains(&freq) {
((freq - 5950) / 5) as u8
} else {
0
}
})
}
pub fn signal_dbm(&self) -> i32 {
self.signal_dbm
}
pub fn beacon_interval_tu(&self) -> u16 {
self.beacon_interval_tu
}
pub fn beacon_interval_ms(&self) -> f64 {
f64::from(self.beacon_interval_tu) * 1.024
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn capability_info(&self) -> &CapabilityInfo {
&self.capability_info
}
pub fn ies(&self) -> &[Ie] {
&self.ies
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn tsf(&self) -> u64 {
self.tsf
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn uptime(&self) -> Duration {
Duration::from_micros(self.tsf)
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn last_seen_utc(&self) -> Option<DateTime<Utc>> {
self.last_seen_utc
}
pub fn ssid(&self) -> Option<&str> {
fn find_ssid(ies: &[Ie]) -> Option<&str> {
ies.iter().find_map(|ie| {
if let IeData::Ssid(ssid) = &ie.data
&& !ssid.is_empty()
{
ssid.as_str().ok()
} else {
None
}
})
}
#[cfg(target_os = "linux")]
{
find_ssid(&self.ies).or_else(|| self.beacon_ies.as_deref().and_then(find_ssid))
}
#[cfg(not(target_os = "linux"))]
{
find_ssid(&self.ies)
}
}
pub fn security_protocols(&self) -> SecurityProtocols {
let mut protocols = SecurityProtocols::from(self.ies.as_slice());
#[cfg(any(target_os = "linux", target_os = "windows"))]
if self.capability_info.privacy && protocols.is_empty() {
protocols |=
SecurityProtocols::from(enumflags2::BitFlags::from(crate::SecurityProtocol::WEP));
}
#[cfg(target_os = "macos")]
if self.is_wep {
protocols |=
SecurityProtocols::from(enumflags2::BitFlags::from(crate::SecurityProtocol::WEP));
}
protocols
}
pub fn wifi_protocols(&self) -> WifiProtocols {
WifiProtocols::from_ies_for_band(self.ies.as_slice(), self.band())
}
pub fn wifi_amendments(&self) -> WifiAmendments {
let amendments = WifiAmendments::from(self.ies.as_slice());
#[cfg(any(target_os = "linux", target_os = "windows"))]
let amendments = amendments | WifiAmendments::from(&self.capability_info);
amendments
}
pub fn max_rate_mbps(&self) -> f64 {
let (mut eht_caps, mut he_caps, mut vht_caps, mut ht_caps) = (None, None, None, None);
let (mut supported_rates, mut extended_supported_rates) = (None, None);
for ie in self.ies.iter() {
match &ie.data {
IeData::EhtCapabilities(ie_data) => eht_caps = Some(ie_data),
IeData::HeCapabilities(ie_data) => he_caps = Some(ie_data),
IeData::VhtCapabilities(ie_data) => vht_caps = Some(ie_data),
IeData::HtCapabilities(ie_data) => ht_caps = Some(ie_data),
IeData::SupportedRates(ie_data) => supported_rates = Some(ie_data),
IeData::ExtendedSupportedRates(ie_data) => extended_supported_rates = Some(ie_data),
_ => continue,
}
}
let channel_width = self.channel_width();
let rates = [
eht_caps.map(|c| c.max_rate(channel_width)),
he_caps.map(|c| c.max_rate(channel_width)),
vht_caps.map(|c| c.max_rate(channel_width, ht_caps.map(|h| h.as_ref()))),
ht_caps.map(|c| c.max_rate(channel_width)),
];
if let Some(max_rate) = rates.into_iter().flatten().max_by(|a, b| a.total_cmp(b)) {
return max_rate;
}
let mut max_rate = 0.0f64;
if let Some(supported_rates) = supported_rates {
max_rate = max_rate.max(
supported_rates
.rates()
.iter()
.map(|rate| rate.value())
.max_by(|r1, r2| r1.total_cmp(r2))
.unwrap_or_default(),
);
}
if let Some(extended_supported_rates) = extended_supported_rates {
max_rate = max_rate.max(
extended_supported_rates
.rates()
.iter()
.map(|rate| rate.value())
.max_by(|r1, r2| r1.total_cmp(r2))
.unwrap_or_default(),
);
}
max_rate
}
pub fn channel_utilization(&self) -> Option<u8> {
self.ies.iter().find_map(|ie| {
if let IeData::BssLoad(bss_load) = &ie.data {
Some(bss_load.channel_utilization)
} else {
None
}
})
}
pub fn station_count(&self) -> Option<u16> {
self.ies.iter().find_map(|ie| {
if let IeData::BssLoad(bss_load) = &ie.data {
Some(bss_load.station_count)
} else {
None
}
})
}
pub fn max_spatial_streams(&self) -> u8 {
let (mut eht_caps, mut he_caps, mut vht_caps, mut ht_caps) = (None, None, None, None);
for ie in self.ies.iter() {
match &ie.data {
IeData::EhtCapabilities(ie_data) => eht_caps = Some(ie_data),
IeData::HeCapabilities(ie_data) => he_caps = Some(ie_data),
IeData::VhtCapabilities(ie_data) => vht_caps = Some(ie_data),
IeData::HtCapabilities(ie_data) => ht_caps = Some(ie_data),
_ => continue,
}
}
if let Some(caps) = eht_caps
&& let Some(mcs_nss_set) = &caps.supported_eht_mcs_and_nss_set
{
let default_mcs_map_spatial_streams = mcs_nss_set
.eht_mcs_map_bw_lte_80_mhz_except_20_mhz_only_non_ap_sta
.max_spatial_streams();
return match self.channel_width() {
ChannelWidth::TwentyMhz | ChannelWidth::FortyMhz | ChannelWidth::EightyMhz => {
default_mcs_map_spatial_streams
}
ChannelWidth::EightyPlusEightyMhz | ChannelWidth::OneSixtyMhz => mcs_nss_set
.eht_mcs_map_bw_eq_160_mhz
.map_or(default_mcs_map_spatial_streams, |mcs_map| {
mcs_map.max_spatial_streams()
}),
ChannelWidth::ThreeHundredTwentyMhz => mcs_nss_set
.eht_mcs_map_bw_eq_320_mhz
.map_or(default_mcs_map_spatial_streams, |mcs_map| {
mcs_map.max_spatial_streams()
}),
};
}
if let Some(caps) = he_caps {
let default_mcs_map_spatial_streams = caps
.supported_he_mcs_and_nss_set
.rx_he_mcs_map_less_than_or_equal_to_eighty_mhz
.max_spatial_streams();
return match self.channel_width() {
ChannelWidth::TwentyMhz | ChannelWidth::FortyMhz | ChannelWidth::EightyMhz => {
default_mcs_map_spatial_streams
}
ChannelWidth::EightyPlusEightyMhz => caps
.supported_he_mcs_and_nss_set
.rx_he_mcs_map_eighty_plus_eighty_mhz
.map_or(default_mcs_map_spatial_streams, |mcs_map| {
mcs_map.max_spatial_streams()
}),
ChannelWidth::OneSixtyMhz | ChannelWidth::ThreeHundredTwentyMhz => caps
.supported_he_mcs_and_nss_set
.rx_he_mcs_map_one_hundred_sixty_mhz
.map_or(default_mcs_map_spatial_streams, |mcs_map| {
mcs_map.max_spatial_streams()
}),
};
}
if let Some(caps) = vht_caps {
return caps
.supported_vht_mcs_and_nss_set
.rx_vht_mcs_map
.max_spatial_streams();
}
if let Some(caps) = ht_caps {
return caps.supported_mcs_set.max_spatial_streams();
}
1
}
pub(crate) fn resolve_ie_dependencies(&mut self) {
crate::ies::resolve_ie_dependencies(&mut self.ies);
}
}
impl Hash for Bss {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.bssid.hash(state);
}
}
impl PartialEq for Bss {
fn eq(&self, other: &Self) -> bool {
self.bssid == other.bssid
}
}
impl Display for Bss {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"BSSID: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}\nSSID: {}\nRSSI: {} dBm\nChannel Number: {}\nChannel Width: {}\nWi-Fi Protocols: {}",
self.bssid[0],
self.bssid[1],
self.bssid[2],
self.bssid[3],
self.bssid[4],
self.bssid[5],
self.ssid().unwrap_or_default(),
self.signal_dbm,
self.channel_number(),
self.channel_width(),
self.wifi_protocols()
)?;
#[cfg(any(target_os = "linux", target_os = "windows"))]
writeln!(f, "{}", self.capability_info)?;
Ok(())
}
}