| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{Config, Ssid}, |
| fidl_fuchsia_wlan_mlme::{self as fidl_mlme, BssDescription}, |
| std::{cmp::Ordering, collections::HashSet}, |
| wlan_common::{ |
| bss::{BssDescriptionExt, Protection}, |
| channel::Channel, |
| ie::{self, rsn::rsne, wsc}, |
| }, |
| }; |
| |
| #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] |
| pub struct ClientConfig { |
| cfg: Config, |
| pub wpa3_supported: bool, |
| } |
| |
| impl ClientConfig { |
| pub fn from_config(cfg: Config, wpa3_supported: bool) -> Self { |
| Self { cfg, wpa3_supported } |
| } |
| |
| /// Converts a given BssDescription into a BssInfo. |
| pub fn convert_bss_description( |
| &self, |
| bss: &BssDescription, |
| wmm_param: Option<ie::WmmParam>, |
| ) -> BssInfo { |
| let mut probe_resp_wsc = None; |
| match bss.find_wsc_ie() { |
| Some(ie) => match wsc::parse_probe_resp_wsc(ie) { |
| Ok(wsc) => probe_resp_wsc = Some(wsc), |
| Err(_e) => { |
| // Parsing could fail because the WSC IE comes from a beacon, which does |
| // not contain all the information that a probe response WSC is expected |
| // to have. We don't have the information to distinguish between a beacon |
| // and a probe response, so we let this case fail silently. |
| } |
| }, |
| None => (), |
| } |
| |
| BssInfo { |
| bssid: bss.bssid.clone(), |
| ssid: bss.ssid.clone(), |
| rx_dbm: get_rx_dbm(bss), |
| snr_db: bss.snr_db, |
| channel: Channel::from_fidl(bss.chan), |
| protection: bss.get_protection(), |
| compatible: self.is_bss_compatible(bss), |
| ht_cap: bss.ht_cap.as_ref().map(|cap| **cap), |
| vht_cap: bss.vht_cap.as_ref().map(|cap| **cap), |
| probe_resp_wsc, |
| wmm_param, |
| } |
| } |
| |
| /// Compares two BSS based on |
| /// (1) their compatibility |
| /// (2) their security protocol |
| /// (3) their Beacon's RSSI |
| pub fn compare_bss(&self, left: &BssDescription, right: &BssDescription) -> Ordering { |
| self.is_bss_compatible(left) |
| .cmp(&self.is_bss_compatible(right)) |
| .then(left.get_protection().cmp(&right.get_protection())) |
| .then(compare_dbm(get_rx_dbm(left), get_rx_dbm(right))) |
| } |
| |
| /// Determines whether a given BSS is compatible with this client SME configuration. |
| pub fn is_bss_compatible(&self, bss: &BssDescription) -> bool { |
| let privacy = wlan_common::mac::CapabilityInfo(bss.cap).privacy(); |
| let protection = bss.get_protection(); |
| match &protection { |
| Protection::Open => true, |
| Protection::Wep => self.cfg.wep_supported, |
| Protection::Wpa1 => self.cfg.wpa1_supported, |
| Protection::Wpa2Wpa3Personal | Protection::Wpa3Personal if self.wpa3_supported => { |
| match bss.rsne.as_ref() { |
| Some(rsne) if privacy => match rsne::from_bytes(&rsne[..]) { |
| Ok((_, a_rsne)) => a_rsne.is_wpa3_rsn_compatible(), |
| _ => false, |
| }, |
| _ => false, |
| } |
| } |
| Protection::Wpa1Wpa2Personal |
| | Protection::Wpa2Personal |
| | Protection::Wpa2Wpa3Personal => match bss.rsne.as_ref() { |
| Some(rsne) if privacy => match rsne::from_bytes(&rsne[..]) { |
| Ok((_, a_rsne)) => a_rsne.is_wpa2_rsn_compatible(), |
| _ => false, |
| }, |
| _ => false, |
| }, |
| _ => false, |
| } |
| } |
| |
| /// Returns the 'best' BSS from a given BSS list. The 'best' BSS is determined by comparing |
| /// all BSS with `compare_bss(BssDescription, BssDescription)`. |
| pub fn get_best_bss<'a>(&self, bss_list: &'a [BssDescription]) -> Option<&'a BssDescription> { |
| bss_list.iter().max_by(|x, y| self.compare_bss(x, y)) |
| } |
| |
| /// Counts unique SSID in a given BSS list |
| pub fn count_ssid(&self, bss_set: &[BssDescription]) -> usize { |
| bss_set.into_iter().map(|b| b.ssid.clone()).collect::<HashSet<_>>().len() |
| } |
| } |
| |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct BssInfo { |
| pub bssid: [u8; 6], |
| pub ssid: Ssid, |
| pub rx_dbm: i8, |
| pub snr_db: i8, |
| pub channel: wlan_common::channel::Channel, |
| pub protection: Protection, |
| pub compatible: bool, |
| pub ht_cap: Option<fidl_mlme::HtCapabilities>, |
| pub vht_cap: Option<fidl_mlme::VhtCapabilities>, |
| pub probe_resp_wsc: Option<wsc::ProbeRespWsc>, |
| pub wmm_param: Option<ie::WmmParam>, |
| } |
| |
| fn get_rx_dbm(bss: &BssDescription) -> i8 { |
| if bss.rcpi_dbmh != 0 { |
| (bss.rcpi_dbmh / 2) as i8 |
| } else { |
| bss.rssi_dbm |
| } |
| } |
| |
| fn compare_dbm(left: i8, right: i8) -> Ordering { |
| match (left, right) { |
| (0, 0) => Ordering::Equal, |
| (0, _) => Ordering::Less, |
| (_, 0) => Ordering::Greater, |
| (left, right) => left.cmp(&right), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::client::test_utils::fake_wmm_param, |
| fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_mlme as fidl_mlme, |
| std::cmp::Ordering, |
| wlan_common::channel::Cbw, |
| wlan_common::{ |
| fake_bss, |
| ie::{ |
| self, |
| fake_ies::{fake_ht_cap_bytes, fake_vht_cap_bytes}, |
| }, |
| }, |
| }; |
| |
| #[test] |
| fn compare() { |
| // BSSes with the same RCPI, RSSI, and protection are equivalent. |
| let cfg = ClientConfig::default(); |
| assert_eq!( |
| Ordering::Equal, |
| cfg.compare_bss( |
| &fake_bss!(Wpa2, rssi_dbm: -10, rcpi_dbmh: -30), |
| &fake_bss!(Wpa2, rssi_dbm: -10, rcpi_dbmh: -30) |
| ) |
| ); |
| // Compatibility takes priority over everything else |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa1, rssi_dbm: -10, rcpi_dbmh: -10), |
| &fake_bss!(Wpa2, rssi_dbm: -50, rcpi_dbmh: -50), |
| ); |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa1, rssi_dbm: -10, rcpi_dbmh: -10), |
| &fake_bss!(Wpa2, rssi_dbm: -50, rcpi_dbmh: -50), |
| ); |
| // Higher security is better. |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Open, rssi_dbm: -10, rcpi_dbmh: -10), |
| &fake_bss!(Wpa2, rssi_dbm: -50, rcpi_dbmh: -50), |
| ); |
| |
| // RCPI in dBmh takes priority over RSSI in dBmh |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa2, rssi_dbm: -20, rcpi_dbmh: -30), |
| &fake_bss!(Wpa2, rssi_dbm: -30, rcpi_dbmh: -20), |
| ); |
| // Compare RSSI if RCPI is absent |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa2, rssi_dbm: -30, rcpi_dbmh: 0), |
| &fake_bss!(Wpa2, rssi_dbm: -20, rcpi_dbmh: 0), |
| ); |
| // Having an RCPI measurement is always better than not having any measurement |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa2, rssi_dbm: 0, rcpi_dbmh: 0), |
| &fake_bss!(Wpa2, rssi_dbm: 0, rcpi_dbmh: -200), |
| ); |
| // Having an RSSI measurement is always better than not having any measurement |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa2, rssi_dbm: 0, rcpi_dbmh: 0), |
| &fake_bss!(Wpa2, rssi_dbm: -100, rcpi_dbmh: 0), |
| ); |
| } |
| |
| #[test] |
| fn compare_with_wep_supported() { |
| let cfg = ClientConfig::from_config(Config::default().with_wep(), false); |
| // WEP is supported while WPA1 is not, so we prefer it. |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wpa1, rssi_dbm: -10, rcpi_dbmh: -10), |
| &fake_bss!(Wep, rssi_dbm: -50, rcpi_dbmh: -50), |
| ); |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wep, rssi_dbm: -10, rcpi_dbmh: -10), |
| &fake_bss!(Wpa2, rssi_dbm: -50, rcpi_dbmh: -50), |
| ); |
| } |
| |
| #[test] |
| fn compare_with_wep_and_wpa1_supported() { |
| let cfg = ClientConfig::from_config(Config::default().with_wep().with_wpa1(), false); |
| // WEP is worse than WPA1 when both are supported. |
| assert_bss_cmp( |
| &cfg, |
| &fake_bss!(Wep, rssi_dbm: -50, rcpi_dbmh: -50), |
| &fake_bss!(Wpa1, rssi_dbm: -10, rcpi_dbmh: -10), |
| ); |
| } |
| |
| #[test] |
| fn get_best_bss_empty_list() { |
| let cfg = ClientConfig::default(); |
| assert!(cfg.get_best_bss(&vec![]).is_none()); |
| } |
| |
| #[test] |
| fn get_best_bss_nonempty_list() { |
| let cfg = ClientConfig::default(); |
| let bss1 = fake_bss!(Wep, rssi_dbm: -30, rcpi_dbmh: -10); |
| let bss2 = fake_bss!(Wpa2, rssi_dbm: -20, rcpi_dbmh: -10); |
| let bss3 = fake_bss!(Wpa2, rssi_dbm: -80, rcpi_dbmh: -80); |
| let bss_list = vec![bss1, bss2, bss3]; |
| assert_eq!(cfg.get_best_bss(&bss_list), Some(&bss_list[1])); |
| } |
| |
| #[test] |
| fn verify_compatibility() { |
| // Compatible: |
| let cfg = ClientConfig::default(); |
| assert!(cfg.is_bss_compatible(&fake_bss!(Open))); |
| assert!(cfg.is_bss_compatible(&fake_bss!(Wpa2))); |
| assert!(cfg.is_bss_compatible(&fake_bss!(Wpa2Wpa3))); |
| |
| // Not compatible: |
| assert!(!cfg.is_bss_compatible(&fake_bss!(Wpa1))); |
| assert!(!cfg.is_bss_compatible(&fake_bss!(Wpa2Legacy))); |
| assert!(!cfg.is_bss_compatible(&fake_bss!(Wpa2NoPrivacy))); |
| assert!(!cfg.is_bss_compatible(&fake_bss!(Wpa3))); |
| assert!(!cfg.is_bss_compatible(&fake_bss!(Eap))); |
| |
| // WEP support is configurable to be on or off: |
| let cfg = ClientConfig::from_config(Config::default().with_wep(), false); |
| assert!(cfg.is_bss_compatible(&fake_bss!(Wep))); |
| |
| // WPA3 support is configurable to be on or off: |
| let cfg = ClientConfig::from_config(Config::default(), true); |
| assert!(cfg.is_bss_compatible(&fake_bss!(Wpa3))); |
| } |
| |
| #[test] |
| fn convert_bss() { |
| let cfg = ClientConfig::default(); |
| assert_eq!( |
| cfg.convert_bss_description( |
| &fake_bss!(Wpa2, |
| ssid: vec![], |
| bssid: [0u8; 6], |
| rssi_dbm: -30, |
| snr_db: 0, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| ht_cap: Some(Box::new(fidl_mlme::HtCapabilities { |
| bytes: fake_ht_cap_bytes() |
| })), |
| vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities { |
| bytes: fake_vht_cap_bytes() |
| })), |
| ), |
| None |
| ), |
| BssInfo { |
| bssid: [0u8; 6], |
| ssid: vec![], |
| rx_dbm: -30, |
| snr_db: 0, |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| protection: Protection::Wpa2Personal, |
| compatible: true, |
| ht_cap: Some(fidl_mlme::HtCapabilities { bytes: fake_ht_cap_bytes() }), |
| vht_cap: Some(fidl_mlme::VhtCapabilities { bytes: fake_vht_cap_bytes() }), |
| probe_resp_wsc: None, |
| wmm_param: None, |
| } |
| ); |
| |
| let wmm_param = *ie::parse_wmm_param(&fake_wmm_param().bytes[..]) |
| .expect("expect WMM param to be parseable"); |
| assert_eq!( |
| cfg.convert_bss_description( |
| &fake_bss!(Wpa2, |
| ssid: vec![], |
| bssid: [0u8; 6], |
| rssi_dbm: -30, |
| snr_db: 0, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| ht_cap: Some(Box::new(fidl_mlme::HtCapabilities { |
| bytes: fake_ht_cap_bytes() |
| })), |
| vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities { |
| bytes: fake_vht_cap_bytes() |
| })), |
| ), |
| Some(wmm_param) |
| ), |
| BssInfo { |
| bssid: [0u8; 6], |
| ssid: vec![], |
| rx_dbm: -30, |
| snr_db: 0, |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| protection: Protection::Wpa2Personal, |
| compatible: true, |
| ht_cap: Some(fidl_mlme::HtCapabilities { bytes: fake_ht_cap_bytes() }), |
| vht_cap: Some(fidl_mlme::VhtCapabilities { bytes: fake_vht_cap_bytes() }), |
| probe_resp_wsc: None, |
| wmm_param: Some(wmm_param), |
| } |
| ); |
| |
| assert_eq!( |
| cfg.convert_bss_description( |
| &fake_bss!(Wep, |
| ssid: vec![], |
| bssid: [0u8; 6], |
| rssi_dbm: -30, |
| snr_db: 0, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| ht_cap: Some(Box::new(fidl_mlme::HtCapabilities { |
| bytes: fake_ht_cap_bytes() |
| })), |
| vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities { |
| bytes: fake_vht_cap_bytes() |
| })), |
| ), |
| None |
| ), |
| BssInfo { |
| bssid: [0u8; 6], |
| ssid: vec![], |
| rx_dbm: -30, |
| snr_db: 0, |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| protection: Protection::Wep, |
| compatible: false, |
| ht_cap: Some(fidl_mlme::HtCapabilities { bytes: fake_ht_cap_bytes() }), |
| vht_cap: Some(fidl_mlme::VhtCapabilities { bytes: fake_vht_cap_bytes() }), |
| probe_resp_wsc: None, |
| wmm_param: None, |
| } |
| ); |
| |
| let cfg = ClientConfig::from_config(Config::default().with_wep(), false); |
| assert_eq!( |
| cfg.convert_bss_description( |
| &fake_bss!(Wep, |
| ssid: vec![], |
| bssid: [0u8; 6], |
| rssi_dbm: -30, |
| snr_db: 0, |
| chan: fidl_common::WlanChan { |
| primary: 1, |
| secondary80: 0, |
| cbw: fidl_common::Cbw::Cbw20, |
| }, |
| ht_cap: Some(Box::new(fidl_mlme::HtCapabilities { |
| bytes: fake_ht_cap_bytes() |
| })), |
| vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities { |
| bytes: fake_vht_cap_bytes() |
| })), |
| ), |
| None |
| ), |
| BssInfo { |
| bssid: [0u8; 6], |
| ssid: vec![], |
| rx_dbm: -30, |
| snr_db: 0, |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| protection: Protection::Wep, |
| compatible: true, |
| ht_cap: Some(fidl_mlme::HtCapabilities { bytes: fake_ht_cap_bytes() }), |
| vht_cap: Some(fidl_mlme::VhtCapabilities { bytes: fake_vht_cap_bytes() }), |
| probe_resp_wsc: None, |
| wmm_param: None, |
| } |
| ); |
| } |
| |
| #[test] |
| fn count_ssid_in_bss_list() { |
| let cfg = ClientConfig::default(); |
| let bss1 = fake_bss!(Open, ssid: b"foo".to_vec(), bssid: [1, 1, 1, 1, 1, 1]); |
| let bss2 = fake_bss!(Open, ssid: b"bar".to_vec(), bssid: [2, 2, 2, 2, 2, 2]); |
| let bss3 = fake_bss!(Open, ssid: b"foo".to_vec(), bssid: [3, 3, 3, 3, 3, 3]); |
| let num_ssid = cfg.count_ssid(&vec![bss1, bss2, bss3]); |
| |
| assert_eq!(2, num_ssid); |
| } |
| |
| #[test] |
| fn test_compare_dbm() { |
| assert_eq!(compare_dbm(0, -5), Ordering::Less); |
| assert_eq!(compare_dbm(-3, 0), Ordering::Greater); |
| assert_eq!(compare_dbm(0, 0), Ordering::Equal); |
| assert_eq!(compare_dbm(-6, -5), Ordering::Less); |
| assert_eq!(compare_dbm(-3, -4), Ordering::Greater); |
| assert_eq!(compare_dbm(-128, -128), Ordering::Equal); |
| } |
| |
| // ======== helper functions below ======== // |
| |
| fn assert_bss_cmp( |
| cfg: &ClientConfig, |
| worse: &fidl_mlme::BssDescription, |
| better: &fidl_mlme::BssDescription, |
| ) { |
| assert_eq!(Ordering::Less, cfg.compare_bss(worse, better)); |
| assert_eq!(Ordering::Greater, cfg.compare_bss(better, worse)); |
| } |
| } |