| // Copyright 2019 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 { |
| super::{ |
| network_config::{ |
| ConnectFailure, Credential, FailureReason, HiddenProbEvent, NetworkConfig, |
| NetworkConfigError, NetworkIdentifier, PastConnectionData, PastConnectionList, |
| SecurityType, |
| }, |
| stash_conversion::*, |
| }, |
| crate::client::types, |
| anyhow::format_err, |
| async_trait::async_trait, |
| fidl_fuchsia_wlan_common::ScanType, |
| fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme, |
| fuchsia_async as fasync, |
| fuchsia_cobalt::CobaltSender, |
| futures::lock::Mutex, |
| log::{error, info}, |
| rand::Rng, |
| std::{ |
| clone::Clone, |
| collections::{hash_map::Entry, HashMap}, |
| fs, |
| path::Path, |
| }, |
| wlan_metrics_registry::{ |
| SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations, |
| SavedNetworksMetricDimensionSavedNetworks, |
| SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID, SAVED_NETWORKS_METRIC_ID, |
| }, |
| wlan_stash::policy::{PolicyStash as Stash, POLICY_STASH_ID}, |
| }; |
| |
| const MAX_CONFIGS_PER_SSID: usize = 1; |
| |
| pub const LEGACY_KNOWN_NETWORKS_PATH: &str = "/data/known_networks.json"; |
| |
| /// The Saved Network Manager keeps track of saved networks and provides thread-safe access to |
| /// saved networks. Networks are saved by NetworkConfig and accessed by their NetworkIdentifier |
| /// (SSID and security protocol). Network configs are saved in-memory, and part of each network |
| /// data is saved persistently. Futures aware locks are used in order to wait for the stash flush |
| /// operations to complete when data changes. |
| pub struct SavedNetworksManager { |
| saved_networks: Mutex<NetworkConfigMap>, |
| stash: Mutex<Stash>, |
| cobalt_api: Mutex<CobaltSender>, |
| } |
| |
| /// Save multiple network configs per SSID in able to store multiple connections with different |
| /// credentials, for different authentication credentials on the same network or for different |
| /// networks with the same name. |
| type NetworkConfigMap = HashMap<NetworkIdentifier, Vec<NetworkConfig>>; |
| |
| pub enum ScanResultType { |
| #[allow(dead_code)] |
| Undirected, |
| Directed(Vec<types::NetworkIdentifier>), // Contains list of target SSIDs |
| } |
| |
| #[async_trait] |
| pub trait SavedNetworksManagerApi: Send + Sync { |
| /// Attempt to remove the NetworkConfig described by the specified NetworkIdentifier and |
| /// Credential. Return true if a NetworkConfig is remove and false otherwise. |
| async fn remove( |
| &self, |
| network_id: NetworkIdentifier, |
| credential: Credential, |
| ) -> Result<bool, NetworkConfigError>; |
| |
| /// Get the count of networks in store, including multiple values with same SSID |
| async fn known_network_count(&self) -> usize; |
| |
| /// Return a list of network configs that match the given SSID. |
| async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig>; |
| |
| /// Return a list of network configs that could be used with the security type seen in a scan. |
| /// This includes configs that have a lower security type that can be upgraded to match the |
| /// provided detailed security type. |
| async fn lookup_compatible( |
| &self, |
| ssid: &types::Ssid, |
| scan_security: types::SecurityTypeDetailed, |
| ) -> Vec<NetworkConfig>; |
| |
| /// Save a network by SSID and password. If the SSID and password have been saved together |
| /// before, do not modify the saved config. Update the legacy storage to keep it consistent |
| /// with what it did before the new version. If a network is pushed out because of the newly |
| /// saved network, this will return the removed config. |
| async fn store( |
| &self, |
| network_id: NetworkIdentifier, |
| credential: Credential, |
| ) -> Result<Option<NetworkConfig>, NetworkConfigError>; |
| |
| /// Update the specified saved network with the result of an attempted connect. If the |
| /// specified network could have been connected to with a different security type and we |
| /// do not find the specified config, we will check the other possible security type. For |
| /// example if a WPA3 network is specified, we will check WPA2 if it isn't found. If the |
| /// specified network is not saved, this function does not save it. |
| async fn record_connect_result( |
| &self, |
| id: NetworkIdentifier, |
| credential: &Credential, |
| bssid: types::Bssid, |
| connect_result: fidl_sme::ConnectResult, |
| discovered_in_scan: Option<ScanType>, |
| ); |
| |
| /// Record the disconnect from a network, to be used for things such as avoiding connections |
| /// that drop soon after starting. |
| async fn record_disconnect( |
| &self, |
| id: &NetworkIdentifier, |
| credential: &Credential, |
| data: PastConnectionData, |
| ); |
| |
| async fn record_periodic_metrics(&self); |
| |
| /// Update hidden networks probabilities based on scan results. Record either results of a |
| /// passive scan or a directed active scan. |
| async fn record_scan_result( |
| &self, |
| scan_type: ScanResultType, |
| results: Vec<types::NetworkIdentifierDetailed>, |
| ); |
| |
| // Return a list of every network config that has been saved. |
| async fn get_networks(&self) -> Vec<NetworkConfig>; |
| |
| // Get the list of past connections for a specific BSS |
| async fn get_past_connections( |
| &self, |
| id: &NetworkIdentifier, |
| credential: &Credential, |
| bssid: &types::Bssid, |
| ) -> PastConnectionList; |
| } |
| |
| impl SavedNetworksManager { |
| /// Initializes a new Saved Network Manager by reading saved networks from a secure storage |
| /// (stash). It initializes in-memory storage and persistent storage with stash. |
| pub async fn new(cobalt_api: CobaltSender) -> Result<Self, anyhow::Error> { |
| let path = LEGACY_KNOWN_NETWORKS_PATH; |
| Self::new_with_stash_or_paths(POLICY_STASH_ID, Path::new(path), cobalt_api).await |
| } |
| |
| /// Load from persistent data from stash. The path for the legacy storage is used to remove the |
| /// legacy storage if it exists. |
| /// TODO(fxbug.dev/85337) Eventually delete logic for deleting legacy storage |
| pub async fn new_with_stash_or_paths( |
| stash_id: impl AsRef<str>, |
| legacy_path: impl AsRef<Path>, |
| cobalt_api: CobaltSender, |
| ) -> Result<Self, anyhow::Error> { |
| let stash = Stash::new_with_id(stash_id.as_ref())?; |
| let stashed_networks = stash.load().await?; |
| let saved_networks: HashMap<NetworkIdentifier, Vec<NetworkConfig>> = stashed_networks |
| .iter() |
| .map(|(network_id, persistent_data)| { |
| ( |
| NetworkIdentifier::from(network_id.clone()), |
| persistent_data |
| .iter() |
| .filter_map(|data| { |
| NetworkConfig::new( |
| NetworkIdentifier::from(network_id.clone()), |
| data.credential.clone().into(), |
| data.has_ever_connected, |
| ) |
| .ok() |
| }) |
| .collect(), |
| ) |
| }) |
| .collect(); |
| |
| // Clean up the legacy storage file since it is no longer used. |
| if let Err(e) = Self::delete_from_path(legacy_path.as_ref()) { |
| info!("Failed to delete legacy storage file: {}", e); |
| } |
| |
| Ok(Self { |
| saved_networks: Mutex::new(saved_networks), |
| stash: Mutex::new(stash), |
| cobalt_api: Mutex::new(cobalt_api), |
| }) |
| } |
| |
| /// Creates a new config with a random stash ID, ensuring a clean environment for an individual |
| /// test |
| #[cfg(test)] |
| pub async fn new_for_test() -> Result<Self, anyhow::Error> { |
| use crate::util::testing::cobalt::create_mock_cobalt_sender; |
| use rand::{ |
| distributions::{Alphanumeric, DistString as _}, |
| thread_rng, |
| }; |
| |
| let stash_id = Alphanumeric.sample_string(&mut thread_rng(), 20); |
| let path = Alphanumeric.sample_string(&mut thread_rng(), 20); |
| Self::new_with_stash_or_paths(stash_id, Path::new(&path), create_mock_cobalt_sender()).await |
| } |
| |
| /// Creates a new SavedNetworksManager and hands back the other end of the stash proxy used. |
| /// This should be used when a test does something that will interact with stash and uses the |
| /// executor to step through futures. |
| #[cfg(test)] |
| pub async fn new_and_stash_server() -> (Self, fidl_fuchsia_stash::StoreAccessorRequestStream) { |
| use crate::util::testing::cobalt::create_mock_cobalt_sender; |
| use rand::{ |
| distributions::{Alphanumeric, DistString as _}, |
| thread_rng, |
| }; |
| |
| let id = Alphanumeric.sample_string(&mut thread_rng(), 20); |
| use fidl::endpoints::create_proxy; |
| let (store_client, _stash_server) = create_proxy::<fidl_fuchsia_stash::StoreMarker>() |
| .expect("failed to create stash proxy"); |
| store_client.identify(id.as_ref()).expect("failed to identify client to store"); |
| let (store, accessor_server) = create_proxy::<fidl_fuchsia_stash::StoreAccessorMarker>() |
| .expect("failed to create accessor proxy"); |
| let stash = Stash::new_with_stash(store); |
| |
| ( |
| Self { |
| saved_networks: Mutex::new(NetworkConfigMap::new()), |
| stash: Mutex::new(stash), |
| cobalt_api: Mutex::new(create_mock_cobalt_sender()), |
| }, |
| accessor_server.into_stream().expect("failed to create stash request stream"), |
| ) |
| } |
| |
| /// Delete the legacy storage file at the specified path |
| fn delete_from_path(storage_path: impl AsRef<Path>) -> Result<(), anyhow::Error> { |
| if storage_path.as_ref().exists() { |
| fs::remove_file(storage_path) |
| .map_err(|e| format_err!("Failed to delete legacy storage: {}", e)) |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Clear the in memory storage and the persistent storage. Also clear the legacy storage. |
| #[cfg(test)] |
| pub async fn clear(&self) -> Result<(), anyhow::Error> { |
| self.saved_networks.lock().await.clear(); |
| self.stash.lock().await.clear().await |
| } |
| } |
| |
| #[async_trait] |
| impl SavedNetworksManagerApi for SavedNetworksManager { |
| async fn remove( |
| &self, |
| network_id: NetworkIdentifier, |
| credential: Credential, |
| ) -> Result<bool, NetworkConfigError> { |
| // Find any matching NetworkConfig and remove it. |
| let mut saved_networks = self.saved_networks.lock().await; |
| if let Some(network_configs) = saved_networks.get_mut(&network_id) { |
| let original_len = network_configs.len(); |
| // Keep the configs that don't match provided NetworkIdentifier and Credential. |
| network_configs.retain(|cfg| cfg.credential != credential); |
| if original_len != network_configs.len() { |
| self.stash |
| .lock() |
| .await |
| .write( |
| &network_id.clone().into(), |
| &network_config_vec_to_persistent_data(&network_configs), |
| ) |
| .await |
| .map_err(|_| NetworkConfigError::StashWriteError)?; |
| return Ok(true); |
| } else { |
| info!("No matching network with the provided credential was found to remove."); |
| } |
| } else { |
| info!("No network was found to remove with the provided SSID and security."); |
| } |
| Ok(false) |
| } |
| |
| /// Get the count of networks in store, including multiple values with same SSID |
| async fn known_network_count(&self) -> usize { |
| self.saved_networks.lock().await.values().into_iter().flatten().count() |
| } |
| |
| async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig> { |
| self.saved_networks.lock().await.get(id).cloned().unwrap_or_default() |
| } |
| |
| async fn lookup_compatible( |
| &self, |
| ssid: &types::Ssid, |
| scan_security: types::SecurityTypeDetailed, |
| ) -> Vec<NetworkConfig> { |
| let saved_networks_guard = self.saved_networks.lock().await; |
| let mut matching_configs = Vec::new(); |
| for security in compatible_policy_securities(&scan_security) { |
| let id = NetworkIdentifier::new(ssid.clone(), security.into()); |
| let saved_configs = saved_networks_guard.get(&id); |
| if let Some(configs) = saved_configs { |
| matching_configs.extend( |
| configs |
| .iter() |
| // Check for conflicts; PSKs can't be used to connect to WPA3 networks. |
| .filter(|config| security_is_compatible(&scan_security, &config.credential)) |
| .into_iter() |
| .map(Clone::clone), |
| ); |
| } |
| } |
| matching_configs |
| } |
| |
| async fn store( |
| &self, |
| network_id: NetworkIdentifier, |
| credential: Credential, |
| ) -> Result<Option<NetworkConfig>, NetworkConfigError> { |
| let mut saved_networks = self.saved_networks.lock().await; |
| let network_entry = saved_networks.entry(network_id.clone()); |
| |
| if let Entry::Occupied(network_configs) = &network_entry { |
| if network_configs.get().iter().any(|cfg| cfg.credential == credential) { |
| info!("Saving a previously saved network with same password."); |
| return Ok(None); |
| } |
| } |
| let network_config = NetworkConfig::new(network_id.clone(), credential.clone(), false)?; |
| let network_configs = network_entry.or_default(); |
| let evicted_config = evict_if_needed(network_configs); |
| network_configs.push(network_config); |
| |
| self.stash |
| .lock() |
| .await |
| .write( |
| &network_id.clone().into(), |
| &network_config_vec_to_persistent_data(&network_configs), |
| ) |
| .await |
| .map_err(|_| NetworkConfigError::StashWriteError)?; |
| |
| Ok(evicted_config) |
| } |
| |
| async fn record_connect_result( |
| &self, |
| id: NetworkIdentifier, |
| credential: &Credential, |
| bssid: types::Bssid, |
| connect_result: fidl_sme::ConnectResult, |
| discovered_in_scan: Option<ScanType>, |
| ) { |
| let mut saved_networks = self.saved_networks.lock().await; |
| let networks = match saved_networks.get_mut(&id) { |
| Some(networks) => networks, |
| None => { |
| error!("Failed to find network to record result of connect attempt."); |
| return; |
| } |
| }; |
| for network in networks.iter_mut() { |
| if &network.credential == credential { |
| match (connect_result.code, connect_result.is_credential_rejected) { |
| (fidl_ieee80211::StatusCode::Success, _) => { |
| let mut has_change = false; |
| if !network.has_ever_connected { |
| network.has_ever_connected = true; |
| has_change = true; |
| } |
| if let Some(scan_type) = discovered_in_scan { |
| let connect_event = match scan_type { |
| ScanType::Passive => HiddenProbEvent::ConnectPassive, |
| ScanType::Active => HiddenProbEvent::ConnectActive, |
| }; |
| network.update_hidden_prob(connect_event); |
| // TODO(60619): Update the stash with new probability if it has changed |
| } |
| if has_change { |
| // Update persistent storage since a config has changed. |
| let data = network_config_vec_to_persistent_data(&networks); |
| if let Err(e) = self.stash.lock().await.write(&id.into(), &data).await { |
| info!("Failed to record successful connect in stash: {}", e); |
| } |
| } |
| } |
| (fidl_ieee80211::StatusCode::Canceled, _) => {} |
| (_, true) => { |
| network.perf_stats.connect_failures.add( |
| bssid, |
| ConnectFailure { |
| time: fasync::Time::now(), |
| reason: FailureReason::CredentialRejected, |
| bssid: bssid.clone(), |
| }, |
| ); |
| } |
| (_, _) => { |
| network.perf_stats.connect_failures.add( |
| bssid, |
| ConnectFailure { |
| time: fasync::Time::now(), |
| reason: FailureReason::GeneralFailure, |
| bssid: bssid.clone(), |
| }, |
| ); |
| } |
| } |
| return; |
| } |
| } |
| // Will not reach here if we find the saved network with matching SSID and credential. |
| error!("Failed to find matching network to record result of connect attempt."); |
| } |
| |
| async fn record_disconnect( |
| &self, |
| id: &NetworkIdentifier, |
| credential: &Credential, |
| data: PastConnectionData, |
| ) { |
| let bssid = data.bssid; |
| let mut saved_networks = self.saved_networks.lock().await; |
| let networks = match saved_networks.get_mut(&id) { |
| Some(networks) => networks, |
| None => { |
| info!("Failed to find network to record disconnect stats"); |
| return; |
| } |
| }; |
| for network in networks.iter_mut() { |
| if &network.credential == credential { |
| network.perf_stats.past_connections.add(bssid, data); |
| return; |
| } |
| } |
| } |
| |
| async fn record_periodic_metrics(&self) { |
| let saved_networks = self.saved_networks.lock().await; |
| let mut cobalt_api = self.cobalt_api.lock().await; |
| log_cobalt_metrics(&*saved_networks, &mut cobalt_api); |
| } |
| |
| async fn record_scan_result( |
| &self, |
| scan_type: ScanResultType, |
| results: Vec<types::NetworkIdentifierDetailed>, |
| ) { |
| let mut saved_networks = self.saved_networks.lock().await; |
| match scan_type { |
| ScanResultType::Undirected => { |
| // For each network we have seen, look for compatible configs and record results. |
| for scan_id in results { |
| for security in compatible_policy_securities(&scan_id.security_type) { |
| let configs = match saved_networks |
| .get_mut(&NetworkIdentifier::new(scan_id.ssid.clone(), security)) |
| { |
| Some(configs) => configs, |
| None => continue, |
| }; |
| // Check that the credential is compatible with the actual security type of |
| // the scan result. |
| let compatible_configs = configs.iter_mut().filter(|config| { |
| security_is_compatible(&scan_id.security_type, &config.credential) |
| }); |
| for config in compatible_configs { |
| config.update_hidden_prob(HiddenProbEvent::SeenPassive) |
| } |
| // TODO(60619): Update the stash with new probability if it has changed |
| } |
| } |
| } |
| ScanResultType::Directed(target_ids) => { |
| // For each config of each targeted ID, check whether there is a scan result that |
| // could be used to connect. If not, update the hidden probability. |
| for target_id in target_ids.into_iter() { |
| let configs = match saved_networks.get_mut(&target_id.clone().into()) { |
| Some(configs) => configs, |
| None => continue, |
| }; |
| let potential_scans = results |
| .iter() |
| .filter(|scan_id| scan_id.ssid == target_id.ssid) |
| .collect::<Vec<_>>(); |
| for config in configs { |
| if let None = potential_scans.iter().find(|scan_id| { |
| compatible_policy_securities(&scan_id.security_type) |
| .contains(&config.security_type) |
| && security_is_compatible( |
| &scan_id.security_type, |
| &config.credential, |
| ) |
| }) { |
| config.update_hidden_prob(HiddenProbEvent::NotSeenActive); |
| } |
| // TODO(60619): Update the stash with new probability if it has changed |
| } |
| } |
| } |
| } |
| } |
| |
| async fn get_networks(&self) -> Vec<NetworkConfig> { |
| self.saved_networks |
| .lock() |
| .await |
| .values() |
| .into_iter() |
| .map(|cfgs| cfgs.clone()) |
| .flatten() |
| .collect() |
| } |
| |
| async fn get_past_connections( |
| &self, |
| id: &NetworkIdentifier, |
| credential: &Credential, |
| bssid: &types::Bssid, |
| ) -> PastConnectionList { |
| self.saved_networks |
| .lock() |
| .await |
| .get(id) |
| .map(|configs| configs.iter().find(|config| &config.credential == credential)) |
| .flatten() |
| .map(|config| config.perf_stats.past_connections.get_list_for_bss(bssid)) |
| .unwrap_or_default() |
| } |
| } |
| |
| /// Returns a subset of potentially hidden saved networks, filtering probabilistically based |
| /// on how certain they are to be hidden. |
| pub fn select_subset_potentially_hidden_networks( |
| saved_networks: Vec<NetworkConfig>, |
| ) -> Vec<types::NetworkIdentifier> { |
| saved_networks |
| .into_iter() |
| .filter(|saved_network| { |
| // Roll a dice to see if we should scan for it. The function gen_range(low..high) |
| // has an inclusive lower bound and exclusive upper bound, so using it as |
| // `hidden_probability > gen_range(0..1)` means that: |
| // - hidden_probability of 1 will _always_ be selected |
| // - hidden_probability of 0 will _never_ be selected |
| saved_network.hidden_probability > rand::thread_rng().gen_range(0.0..1.0) |
| }) |
| .map(|network| types::NetworkIdentifier { |
| ssid: network.ssid, |
| security_type: network.security_type.into(), |
| }) |
| .collect() |
| } |
| |
| /// Gets compatible `SecurityType`s for network candidates. |
| /// |
| /// This function returns a sequence of `SecurityType`s that may be used to connect to a network |
| /// configured as described by the given `SecurityTypeDetailed`. If there is no compatible |
| /// `SecurityType`, then the sequence will be empty. |
| pub fn compatible_policy_securities( |
| detailed_security: &types::SecurityTypeDetailed, |
| ) -> Vec<SecurityType> { |
| use fidl_sme::Protection::*; |
| match detailed_security { |
| Wpa3Enterprise | Wpa3Personal | Wpa2Wpa3Personal => { |
| vec![SecurityType::Wpa2, SecurityType::Wpa3] |
| } |
| Wpa2Enterprise |
| | Wpa2Personal |
| | Wpa1Wpa2Personal |
| | Wpa2PersonalTkipOnly |
| | Wpa1Wpa2PersonalTkipOnly => vec![SecurityType::Wpa, SecurityType::Wpa2], |
| Wpa1 => vec![SecurityType::Wpa], |
| Wep => vec![SecurityType::Wep], |
| Open => vec![SecurityType::None], |
| Unknown => vec![], |
| } |
| } |
| |
| pub fn security_is_compatible( |
| scan_security: &types::SecurityTypeDetailed, |
| credential: &Credential, |
| ) -> bool { |
| if scan_security == &types::SecurityTypeDetailed::Wpa3Personal |
| || scan_security == &types::SecurityTypeDetailed::Wpa3Enterprise |
| { |
| if let Credential::Psk(_) = credential { |
| return false; |
| } |
| } |
| true |
| } |
| |
| /// If the list of configs is at capacity for the number of saved configs per SSID, |
| /// remove a saved network that has never been successfully connected to. If all have |
| /// been successfully connected to, remove any. If a network config is evicted, that connection |
| /// is forgotten for future connections. |
| /// TODO(fxbug.dev/41232) - when network configs record information about successful connections, |
| /// use this to make a better decision what to forget if all networks have connected before. |
| /// TODO(fxbug.dev/41626) - make sure that we disconnect from the network if we evict a network config |
| /// for a network we are currently connected to. |
| fn evict_if_needed(configs: &mut Vec<NetworkConfig>) -> Option<NetworkConfig> { |
| if configs.len() < MAX_CONFIGS_PER_SSID { |
| return None; |
| } |
| |
| for i in 0..configs.len() { |
| if let Some(config) = configs.get(i) { |
| if !config.has_ever_connected { |
| return Some(configs.remove(i)); |
| } |
| } |
| } |
| // If all saved networks have connected, remove the first network |
| return Some(configs.remove(0)); |
| } |
| |
| /// Record Cobalt metrics related to Saved Networks |
| fn log_cobalt_metrics(saved_networks: &NetworkConfigMap, cobalt_api: &mut CobaltSender) { |
| // Count the total number of saved networks |
| let num_networks = match saved_networks.len() { |
| 0 => SavedNetworksMetricDimensionSavedNetworks::Zero, |
| 1 => SavedNetworksMetricDimensionSavedNetworks::One, |
| 2..=4 => SavedNetworksMetricDimensionSavedNetworks::TwoToFour, |
| 5..=40 => SavedNetworksMetricDimensionSavedNetworks::FiveToForty, |
| 41..=500 => SavedNetworksMetricDimensionSavedNetworks::FortyToFiveHundred, |
| 501..=usize::MAX => SavedNetworksMetricDimensionSavedNetworks::FiveHundredAndOneOrMore, |
| _ => unreachable!(), |
| }; |
| cobalt_api.log_event(SAVED_NETWORKS_METRIC_ID, num_networks); |
| |
| // Count the number of configs for each saved network |
| for saved_network in saved_networks { |
| let configs = saved_network.1; |
| use SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations as ConfigCountDimension; |
| let num_configs = match configs.len() { |
| 0 => ConfigCountDimension::Zero, |
| 1 => ConfigCountDimension::One, |
| 2..=4 => ConfigCountDimension::TwoToFour, |
| 5..=40 => ConfigCountDimension::FiveToForty, |
| 41..=500 => ConfigCountDimension::FortyToFiveHundred, |
| 501..=usize::MAX => ConfigCountDimension::FiveHundredAndOneOrMore, |
| _ => unreachable!(), |
| }; |
| cobalt_api.log_event(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID, num_configs); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{ |
| config_management::{ |
| network_config::AddAndGetRecent, PastConnectionsByBssid, PROB_HIDDEN_DEFAULT, |
| PROB_HIDDEN_IF_CONNECT_ACTIVE, PROB_HIDDEN_IF_CONNECT_PASSIVE, |
| PROB_HIDDEN_IF_SEEN_PASSIVE, |
| }, |
| util::testing::{ |
| cobalt::{create_mock_cobalt_sender, create_mock_cobalt_sender_and_receiver}, |
| random_connection_data, |
| }, |
| }, |
| cobalt_client::traits::AsEventCode, |
| fidl_fuchsia_cobalt::CobaltEvent, |
| fidl_fuchsia_stash as fidl_stash, |
| fuchsia_cobalt::cobalt_event_builder::CobaltEventExt, |
| futures::{task::Poll, TryStreamExt}, |
| pin_utils::pin_mut, |
| rand::{ |
| distributions::{Alphanumeric, DistString as _}, |
| thread_rng, |
| }, |
| std::{convert::TryFrom, io::Write}, |
| tempfile::TempDir, |
| test_case::test_case, |
| wlan_common::assert_variant, |
| }; |
| |
| #[fuchsia::test] |
| async fn store_and_lookup() { |
| let stash_id = "store_and_lookup"; |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| let saved_networks = create_saved_networks(stash_id, &path).await; |
| let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| |
| assert!(saved_networks.lookup(&network_id_foo).await.is_empty()); |
| assert_eq!(0, saved_networks.saved_networks.lock().await.len()); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Store a network and verify it was stored. |
| assert!(saved_networks |
| .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec())) |
| .await |
| .expect("storing 'foo' failed") |
| .is_none()); |
| assert_eq!( |
| vec![network_config("foo", "qwertyuio")], |
| saved_networks.lookup(&network_id_foo).await |
| ); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| // Store another network with the same SSID. |
| let popped_network = saved_networks |
| .store(network_id_foo.clone(), Credential::Password(b"12345678".to_vec())) |
| .await |
| .expect("storing 'foo' a second time failed"); |
| assert_eq!(popped_network, Some(network_config("foo", "qwertyuio"))); |
| |
| // There should only be one saved "foo" network because MAX_CONFIGS_PER_SSID is 1. |
| // When this constant becomes greater than 1, both network configs should be found |
| assert_eq!( |
| vec![network_config("foo", "12345678")], |
| saved_networks.lookup(&network_id_foo).await |
| ); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| // Store another network and verify. |
| let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap(); |
| let psk = Credential::Psk(vec![1; 32]); |
| let config_baz = NetworkConfig::new(network_id_baz.clone(), psk.clone(), false) |
| .expect("failed to create network config"); |
| assert!(saved_networks |
| .store(network_id_baz.clone(), psk) |
| .await |
| .expect("storing 'baz' with PSK failed") |
| .is_none()); |
| assert_eq!(vec![config_baz.clone()], saved_networks.lookup(&network_id_baz).await); |
| assert_eq!(2, saved_networks.known_network_count().await); |
| |
| // Saved networks should persist when we create a saved networks manager with the same ID. |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("failed to create saved networks store"); |
| assert_eq!( |
| vec![network_config("foo", "12345678")], |
| saved_networks.lookup(&network_id_foo).await |
| ); |
| assert_eq!(vec![config_baz], saved_networks.lookup(&network_id_baz).await); |
| assert_eq!(2, saved_networks.known_network_count().await); |
| } |
| |
| #[fuchsia::test] |
| async fn store_twice() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| |
| assert!(saved_networks |
| .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec())) |
| .await |
| .expect("storing 'foo' failed") |
| .is_none()); |
| let popped_network = saved_networks |
| .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec())) |
| .await |
| .expect("storing 'foo' a second time failed"); |
| // Because the same network was stored twice, nothing was evicted, so popped_network == None |
| assert_eq!(popped_network, None); |
| let expected_cfgs = vec![network_config("foo", "qwertyuio")]; |
| assert_eq!(expected_cfgs, saved_networks.lookup(&network_id).await); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| } |
| |
| #[fuchsia::test] |
| async fn store_many_same_ssid() { |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| |
| // save max + 1 networks with same SSID and different credentials |
| for i in 0..MAX_CONFIGS_PER_SSID + 1 { |
| let mut password = b"password".to_vec(); |
| password.push(i as u8); |
| let popped_network = saved_networks |
| .store(network_id.clone(), Credential::Password(password)) |
| .await |
| .expect("Failed to saved network"); |
| if i >= MAX_CONFIGS_PER_SSID { |
| assert!(popped_network.is_some()); |
| } else { |
| assert!(popped_network.is_none()); |
| } |
| } |
| |
| // since none have been connected to yet, we don't care which config was removed |
| assert_eq!(MAX_CONFIGS_PER_SSID, saved_networks.lookup(&network_id).await.len()); |
| } |
| |
| #[fuchsia::test] |
| async fn store_and_remove() { |
| let stash_id = "store_and_remove"; |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| let saved_networks = create_saved_networks(stash_id, &path).await; |
| |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Password(b"qwertyuio".to_vec()); |
| assert!(saved_networks.lookup(&network_id).await.is_empty()); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Store a network and verify it was stored. |
| assert!(saved_networks |
| .store(network_id.clone(), credential.clone()) |
| .await |
| .expect("storing 'foo' failed") |
| .is_none()); |
| assert_eq!( |
| vec![network_config("foo", "qwertyuio")], |
| saved_networks.lookup(&network_id).await |
| ); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| // Remove a network with the same NetworkIdentifier but differenct credential and verify |
| // that the saved network is unaffected. |
| assert_eq!( |
| false, |
| saved_networks |
| .remove(network_id.clone(), Credential::Password(b"diff-password".to_vec())) |
| .await |
| .expect("removing 'foo' failed") |
| ); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| // Remove the network and check it is gone |
| assert_eq!( |
| true, |
| saved_networks |
| .remove(network_id.clone(), credential.clone()) |
| .await |
| .expect("removing 'foo' failed") |
| ); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // If we try to remove the network again, we won't get an error and nothing happens |
| assert_eq!( |
| false, |
| saved_networks |
| .remove(network_id.clone(), credential) |
| .await |
| .expect("removing 'foo' failed") |
| ); |
| |
| // Check that removal persists. |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| assert!(saved_networks.lookup(&network_id).await.is_empty()); |
| } |
| |
| #[fuchsia::test] |
| fn sme_protection_converts_to_lower_compatible() { |
| use fidl_sme::Protection::*; |
| let lower_compatible_pairs = vec![ |
| (Wpa3Enterprise, vec![SecurityType::Wpa2, SecurityType::Wpa3]), |
| (Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]), |
| (Wpa2Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]), |
| (Wpa2Enterprise, vec![SecurityType::Wpa, SecurityType::Wpa2]), |
| (Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]), |
| (Wpa1Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]), |
| (Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]), |
| (Wpa1Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]), |
| (Wpa1, vec![SecurityType::Wpa]), |
| (Wep, vec![SecurityType::Wep]), |
| (Open, vec![SecurityType::None]), |
| (Unknown, vec![]), |
| ]; |
| for (detailed_security, security) in lower_compatible_pairs { |
| assert_eq!(compatible_policy_securities(&detailed_security), security); |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn lookup_compatible_returns_both_compatible_configs() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let ssid = types::Ssid::try_from("foo").unwrap(); |
| let network_id_wpa2 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2); |
| let network_id_wpa3 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3); |
| let credential_wpa2 = Credential::Password(b"password".to_vec()); |
| let credential_wpa3 = Credential::Password(b"wpa3-password".to_vec()); |
| |
| // Check that lookup_compatible does not modify the SavedNetworksManager and returns an |
| // empty vector if there is no matching config. |
| let results = saved_networks |
| .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal) |
| .await; |
| assert!(results.is_empty()); |
| assert_eq!(saved_networks.known_network_count().await, 0); |
| |
| // Store a couple of network configs that could both be use to connect to a WPA2/WPA3 |
| // network. |
| assert!(saved_networks |
| .store(network_id_wpa2.clone(), credential_wpa2.clone()) |
| .await |
| .expect("Failed to store network") |
| .is_none()); |
| assert!(saved_networks |
| .store(network_id_wpa3.clone(), credential_wpa3.clone()) |
| .await |
| .expect("Failed to store network") |
| .is_none()); |
| // Store a network with the same SSID but a not-compatible security type. |
| let network_id_wep = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa); |
| assert!(saved_networks |
| .store(network_id_wep.clone(), Credential::Password(b"abcdefgh".to_vec())) |
| .await |
| .expect("Failed to store network") |
| .is_none()); |
| |
| let results = saved_networks |
| .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal) |
| .await; |
| let expected_config_wpa2 = NetworkConfig::new(network_id_wpa2, credential_wpa2, false) |
| .expect("Failed to create config"); |
| let expected_config_wpa3 = NetworkConfig::new(network_id_wpa3, credential_wpa3, false) |
| .expect("Failed to create config"); |
| assert_eq!(results.len(), 2); |
| assert!(results.contains(&expected_config_wpa2)); |
| assert!(results.contains(&expected_config_wpa3)); |
| } |
| |
| #[test_case(types::SecurityTypeDetailed::Wpa3Personal)] |
| #[test_case(types::SecurityTypeDetailed::Wpa3Enterprise)] |
| #[fuchsia::test(add_test_attr = false)] |
| fn lookup_compatible_does_not_return_wpa3_psk( |
| wpa3_detailed_security: types::SecurityTypeDetailed, |
| ) { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create executor"); |
| let saved_networks = exec |
| .run_singlethreaded(SavedNetworksManager::new_for_test()) |
| .expect("Failed to create SavedNetworksManager"); |
| |
| // Store a WPA3 config with a password that will match and a PSK config that won't match |
| // to a WPA3 network. |
| let ssid = types::Ssid::try_from("foo").unwrap(); |
| let network_id_psk = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2); |
| let network_id_password = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3); |
| let credential_psk = Credential::Psk(vec![5; 32]); |
| let credential_password = Credential::Password(b"mypassword".to_vec()); |
| assert!(exec |
| .run_singlethreaded( |
| saved_networks.store(network_id_psk.clone(), credential_psk.clone()), |
| ) |
| .expect("Failed to store network") |
| .is_none()); |
| assert!(exec |
| .run_singlethreaded( |
| saved_networks.store(network_id_password.clone(), credential_password.clone()), |
| ) |
| .expect("Failed to store network") |
| .is_none()); |
| |
| // Only the WPA3 config with a credential should be returned. |
| let expected_config_wpa3 = |
| NetworkConfig::new(network_id_password, credential_password, false) |
| .expect("Failed to create configc"); |
| let results = exec |
| .run_singlethreaded(saved_networks.lookup_compatible(&ssid, wpa3_detailed_security)); |
| assert_eq!(results, vec![expected_config_wpa3]); |
| } |
| |
| #[fuchsia::test] |
| async fn connect_network() { |
| let stash_id = "connect_network"; |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| |
| let saved_networks = create_saved_networks(stash_id, &path).await; |
| |
| let network_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Password(b"password".to_vec()); |
| let bssid = types::Bssid([4; 6]); |
| |
| // If connect and network hasn't been saved, we should not save the network. |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fake_successful_connect_result(), |
| None, |
| ) |
| .await; |
| assert!(saved_networks.lookup(&network_id).await.is_empty()); |
| assert_eq!(saved_networks.saved_networks.lock().await.len(), 0); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Save the network and record a successful connection. |
| assert!(saved_networks |
| .store(network_id.clone(), credential.clone()) |
| .await |
| .expect("Failed save network") |
| .is_none()); |
| |
| let config = network_config("bar", "password"); |
| assert_eq!(vec![config], saved_networks.lookup(&network_id).await); |
| |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fake_successful_connect_result(), |
| None, |
| ) |
| .await; |
| |
| // The network should be saved with the connection recorded. We should not have recorded |
| // that the network was connected to passively or actively. |
| assert_variant!(saved_networks.lookup(&network_id).await.as_slice(), [config] => { |
| assert_eq!(config.has_ever_connected, true); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| }); |
| |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fake_successful_connect_result(), |
| Some(ScanType::Active), |
| ) |
| .await; |
| // We should now see that we connected to the network after an active scan. |
| assert_variant!(saved_networks.lookup(&network_id).await.as_slice(), [config] => { |
| assert_eq!(config.has_ever_connected, true); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE); |
| }); |
| |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fake_successful_connect_result(), |
| Some(ScanType::Passive), |
| ) |
| .await; |
| // The config should have a lower hidden probability after connecting after a passive scan. |
| assert_variant!(saved_networks.lookup(&network_id).await.as_slice(), [config] => { |
| assert_eq!(config.has_ever_connected, true); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE); |
| }); |
| |
| // Success connects should be saved as persistent data. |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| assert_variant!(saved_networks.lookup(&network_id).await.as_slice(), [config] => { |
| assert_eq!(config.has_ever_connected, true); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_connect_updates_one() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let net_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let net_id_also_valid = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(); |
| let credential = Credential::Password(b"some_password".to_vec()); |
| let bssid = types::Bssid([2; 6]); |
| |
| // Save the networks and record a successful connection. |
| assert!(saved_networks |
| .store(net_id.clone(), credential.clone()) |
| .await |
| .expect("Failed save network") |
| .is_none()); |
| assert!(saved_networks |
| .store(net_id_also_valid.clone(), credential.clone()) |
| .await |
| .expect("Failed save network") |
| .is_none()); |
| saved_networks |
| .record_connect_result( |
| net_id.clone(), |
| &credential, |
| bssid, |
| fake_successful_connect_result(), |
| None, |
| ) |
| .await; |
| |
| assert_variant!(saved_networks.lookup(&net_id).await.as_slice(), [config] => { |
| assert!(config.has_ever_connected); |
| }); |
| // If the specified network identifier is found, record_conenct_result should not mark |
| // another config even if it could also have been used for the connect attempt. |
| assert_variant!(saved_networks.lookup(&net_id_also_valid).await.as_slice(), [config] => { |
| assert!(!config.has_ever_connected); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_connect_failure() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(); |
| let credential = Credential::None; |
| let bssid = types::Bssid([1; 6]); |
| let before_recording = fasync::Time::now(); |
| |
| // Verify that recording connect result does not save the network. |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| assert!(saved_networks.lookup(&network_id).await.is_empty()); |
| assert_eq!(0, saved_networks.saved_networks.lock().await.len()); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Record that the connect failed. |
| assert!(saved_networks |
| .store(network_id.clone(), credential.clone()) |
| .await |
| .expect("Failed save network") |
| .is_none()); |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| is_credential_rejected: true, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| |
| // Check that the failures were recorded correctly. |
| assert_eq!(1, saved_networks.known_network_count().await); |
| let saved_config = saved_networks |
| .lookup(&network_id) |
| .await |
| .pop() |
| .expect("Failed to get saved network config"); |
| let connect_failures = |
| saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording); |
| assert_variant!(connect_failures, failures => { |
| // There are 2 failures. One is a general failure and one rejected credentials failure. |
| assert_eq!(failures.len(), 2); |
| assert!(failures.iter().any(|failure| failure.reason == FailureReason::GeneralFailure)); |
| assert!(failures.iter().any(|failure| failure.reason == FailureReason::CredentialRejected)); |
| // Both failures have the correct BSSID |
| for failure in failures.iter() { |
| assert_eq!(failure.bssid, bssid); |
| assert_eq!(failure.bssid, bssid); |
| } |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_connect_cancelled_ignored() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(); |
| let credential = Credential::None; |
| let bssid = types::Bssid([0; 6]); |
| let before_recording = fasync::Time::now(); |
| |
| // Verify that recording connect result does not save the network. |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid.clone(), |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Canceled, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| assert!(saved_networks.lookup(&network_id).await.is_empty()); |
| assert_eq!(saved_networks.saved_networks.lock().await.len(), 0); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Record that the connect was canceled. |
| assert!(saved_networks |
| .store(network_id.clone(), credential.clone()) |
| .await |
| .expect("Failed save network") |
| .is_none()); |
| saved_networks |
| .record_connect_result( |
| network_id.clone(), |
| &credential, |
| bssid, |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Canceled, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| |
| // Check that there are no failures recorded for this saved network. |
| assert_eq!(1, saved_networks.known_network_count().await); |
| let saved_config = saved_networks |
| .lookup(&network_id) |
| .await |
| .pop() |
| .expect("Failed to get saved network config"); |
| let connect_failures = |
| saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording); |
| assert_eq!(0, connect_failures.len()); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_disconnect() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Psk(vec![1; 32]); |
| let data = random_connection_data(); |
| |
| saved_networks.record_disconnect(&id, &credential, data.clone()).await; |
| // Verify that nothing happens if the network was not already saved. |
| assert_eq!(saved_networks.saved_networks.lock().await.len(), 0); |
| assert_eq!(saved_networks.known_network_count().await, 0); |
| |
| // Save the network and record a disconnect. |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| saved_networks.record_disconnect(&id, &credential, data.clone()).await; |
| |
| // Check that a data was recorded about the connection that just ended. |
| let recent_connections = saved_networks |
| .lookup(&id) |
| .await |
| .pop() |
| .expect("Failed to get saved network") |
| .perf_stats |
| .past_connections |
| .get_recent_for_network(fasync::Time::INFINITE_PAST); |
| assert_variant!(recent_connections.as_slice(), [connection_data] => { |
| assert_eq!(connection_data, &data); |
| }) |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_passive_scan() { |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let saved_seen_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(); |
| let saved_seen_network = types::NetworkIdentifierDetailed { |
| ssid: saved_seen_id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Open, |
| }; |
| let unsaved_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap(); |
| let unsaved_network = types::NetworkIdentifierDetailed { |
| ssid: unsaved_id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa2Personal, |
| }; |
| let saved_unseen_id = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap(); |
| let seen_credential = Credential::None; |
| let unseen_credential = Credential::Password(b"password".to_vec()); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(saved_seen_id.clone(), seen_credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| assert!(saved_networks |
| .store(saved_unseen_id.clone(), unseen_credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| |
| // Record passive scan results, including the saved network and another network. |
| let seen_networks = vec![saved_seen_network, unsaved_network]; |
| saved_networks.record_scan_result(ScanResultType::Undirected, seen_networks).await; |
| |
| assert_variant!(saved_networks.lookup(&saved_seen_id).await.as_slice(), [config] => { |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE); |
| }); |
| assert_variant!(saved_networks.lookup(&saved_unseen_id).await.as_slice(), [config] => { |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_undirected_scan_with_upgraded_security() { |
| // Test that if we see a different compatible (higher) scan result for a saved network that |
| // could be used to connect, recording the scan results will change the hidden probability. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Password(b"credential".to_vec()); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| |
| // Record passive scan results |
| let seen_networks = vec![types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa3Personal, |
| }]; |
| saved_networks.record_scan_result(ScanResultType::Undirected, seen_networks).await; |
| // The network was seen in a passive scan, so hidden probability should be updated. |
| assert_variant!(saved_networks.lookup(&id).await.as_slice(), [config] => { |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_undirected_scan_incompatible_credential() { |
| // Test that if we see a different compatible (higher) scan result for a saved network that |
| // could be used to connect, recording the scan results will change the hidden probability. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Psk(vec![8; 32]); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| |
| // Record passive scan results, including the saved network and another network. |
| let seen_networks = vec![types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa3Personal, |
| }]; |
| saved_networks.record_scan_result(ScanResultType::Undirected, seen_networks).await; |
| // The network in the passive scan results was not compatible, so hidden probability should |
| // not have been updated. |
| assert_variant!(saved_networks.lookup(&id).await.as_slice(), [config] => { |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_directed_scan_for_upgraded_security() { |
| // Test that if we see a different compatible (higher) scan result for a saved network that |
| // could be used to connect in a directed scan, the hidden probability will not be lowered. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa).unwrap(); |
| let credential = Credential::Password(b"credential".to_vec()); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| // Record directed scan results. The config's probability hidden should not be lowered |
| // since we did not fail to see it in a directed scan. |
| let seen_networks = vec![types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa2Personal, |
| }]; |
| let target = vec![types::NetworkIdentifier { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityType::Wpa, |
| }]; |
| saved_networks.record_scan_result(ScanResultType::Directed(target), seen_networks).await; |
| |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_directed_scan_for_incompatible_credential() { |
| // Test that if we see a network that is not compatible because of the saved credential |
| // (but is otherwise compatible), the directed scan is not considered successful and the |
| // hidden probability of the config is lowered. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Psk(vec![11; 32]); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| // Record directed scan results. The seen network does not match the saved network even |
| // though security is compatible, since the security type is not compatible with the PSK. |
| let target = vec![types::NetworkIdentifier { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }]; |
| let seen_networks = vec![types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa3Personal, |
| }]; |
| saved_networks.record_scan_result(ScanResultType::Directed(target), seen_networks).await; |
| // The hidden probability should have been lowered because a directed scan failed to find |
| // the network. |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_directed_scan_no_ssid_match() { |
| // Test that recording directed active scan results does not mistakenly match a config with |
| // a network with a different SSID. |
| |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Psk(vec![11; 32]); |
| let diff_ssid = types::Ssid::try_from("other-ssid").unwrap(); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| // Record directed scan results. We target the saved network but see a different one. |
| let target = vec![types::NetworkIdentifier { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }]; |
| let seen_networks = vec![types::NetworkIdentifierDetailed { |
| ssid: diff_ssid, |
| security_type: types::SecurityTypeDetailed::Wpa2Personal, |
| }]; |
| saved_networks.record_scan_result(ScanResultType::Directed(target), seen_networks).await; |
| |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_directed_one_not_compatible_one_compatible() { |
| // Test that if we see two networks with the same SSID but only one is compatible, the scan |
| // is recorded as successful for the config. In other words it isn't mistakenly recorded as |
| // a failure because of the config that isn't compatible. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let credential = Credential::Password(b"foo-pass".to_vec()); |
| |
| // Save the networks |
| assert!(saved_networks |
| .store(id.clone(), credential.clone()) |
| .await |
| .expect("Failed to save network") |
| .is_none()); |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| // Record directed scan results. We see one network with the same SSID that doesn't match, |
| // and one that does match. |
| let target = vec![types::NetworkIdentifier { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }]; |
| let seen_networks = vec![ |
| types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa1, |
| }, |
| types::NetworkIdentifierDetailed { |
| ssid: id.ssid.clone(), |
| security_type: types::SecurityTypeDetailed::Wpa2Personal, |
| }, |
| ]; |
| saved_networks.record_scan_result(ScanResultType::Directed(target), seen_networks).await; |
| // Since the directed scan found a matching network, the hidden probability should not |
| // have been lowered. |
| let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config"); |
| assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT); |
| } |
| |
| #[fuchsia::test] |
| fn evict_if_needed_removes_unconnected() { |
| // this test is less meaningful when MAX_CONFIGS_PER_SSID is greater than 1, otherwise |
| // the only saved configs should be removed when the max capacity is met, regardless of |
| // whether it has been connected to. |
| let unconnected_config = network_config("foo", "password"); |
| let mut connected_config = unconnected_config.clone(); |
| connected_config.has_ever_connected = false; |
| let mut network_configs = vec![connected_config; MAX_CONFIGS_PER_SSID - 1]; |
| network_configs.insert(MAX_CONFIGS_PER_SSID / 2, unconnected_config.clone()); |
| |
| assert_eq!(evict_if_needed(&mut network_configs), Some(unconnected_config)); |
| assert_eq!(MAX_CONFIGS_PER_SSID - 1, network_configs.len()); |
| // check that everything left has been connected to before, only one removed is |
| // the one that has never been connected to |
| for config in network_configs.iter() { |
| assert_eq!(true, config.has_ever_connected); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn evict_if_needed_already_has_space() { |
| let mut configs = vec![]; |
| assert_eq!(evict_if_needed(&mut configs), None); |
| let expected_cfgs: Vec<NetworkConfig> = vec![]; |
| assert_eq!(expected_cfgs, configs); |
| |
| if MAX_CONFIGS_PER_SSID > 1 { |
| let mut configs = vec![network_config("foo", "password")]; |
| assert_eq!(evict_if_needed(&mut configs), None); |
| // if MAX_CONFIGS_PER_SSID is 1, this wouldn't be true |
| assert_eq!(vec![network_config("foo", "password")], configs); |
| } |
| } |
| |
| #[fuchsia::test] |
| async fn clear() { |
| let stash_id = "clear"; |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let saved_networks = create_saved_networks(stash_id, &path).await; |
| |
| assert!(saved_networks |
| .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec())) |
| .await |
| .expect("storing 'foo' failed") |
| .is_none()); |
| assert_eq!( |
| vec![network_config("foo", "qwertyuio")], |
| saved_networks.lookup(&network_id).await |
| ); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| saved_networks.clear().await.expect("clearing store failed"); |
| assert_eq!(0, saved_networks.saved_networks.lock().await.len()); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Load store from stash to verify it is also gone from persistent storage |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("failed to create saved networks manager"); |
| |
| assert_eq!(0, saved_networks.known_network_count().await); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn do_not_read_network_from_legacy_storage_and_delete_file() { |
| // Possible contents of a file generated from KnownEssStore, with networks foo and bar with |
| // passwords foobar and password respecitively. Network foo should not be read into new |
| // saved network manager because the password is too short for a valid network password. |
| let contents = b"[{\"ssid\":[102,111,111],\"password\":[102,111,111,98,97,114]}, |
| {\"ssid\":[98,97,114],\"password\":[112, 97, 115, 115, 119, 111, 114, 100]}]"; |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| let mut file = fs::File::create(&path).expect("failed to open file for writing"); |
| |
| assert_eq!(file.write(contents).expect("Failed to write to file"), contents.len()); |
| file.flush().expect("failed to flush contents of file"); |
| |
| let stash_id = "read_network_from_legacy_storage"; |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("failed to create saved networks store"); |
| |
| // Network should not be read. The backing file should be deleted. |
| assert_eq!(0, saved_networks.known_network_count().await); |
| assert!(!path.exists()); |
| } |
| |
| #[fuchsia::test] |
| fn test_store_waits_for_stash() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create executor"); |
| let (saved_networks, mut stash_server) = |
| exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server()); |
| |
| let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(); |
| let save_fut = saved_networks.store(network_id, Credential::None); |
| pin_mut!(save_fut); |
| |
| // Verify that storing the network does not complete until stash responds. |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending); |
| assert_variant!( |
| exec.run_until_stalled(&mut stash_server.try_next()), |
| Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::SetValue { .. }))) |
| ); |
| assert_variant!( |
| exec.run_until_stalled(&mut stash_server.try_next()), |
| Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::Flush{responder}))) => { |
| responder.send(&mut Ok(())).expect("failed to send stash response"); |
| } |
| ); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None))); |
| } |
| |
| /// Create a saved networks manager and clear the contents. Stash ID should be different for |
| /// each test so that they don't interfere. |
| async fn create_saved_networks( |
| stash_id: impl AsRef<str>, |
| path: impl AsRef<Path>, |
| ) -> SavedNetworksManager { |
| let saved_networks = SavedNetworksManager::new_with_stash_or_paths( |
| stash_id, |
| &path, |
| create_mock_cobalt_sender(), |
| ) |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| saved_networks.clear().await.expect("Failed to clear new SavedNetworksManager"); |
| saved_networks |
| } |
| |
| /// Convience function for creating network configs with default values as they would be |
| /// initialized when read from KnownEssStore. Credential is password or none, and security |
| /// type is WPA2 or none. |
| fn network_config(ssid: &str, password: impl Into<Vec<u8>>) -> NetworkConfig { |
| let credential = Credential::from_bytes(password.into()); |
| let id = NetworkIdentifier::try_from(ssid, credential.derived_security_type()).unwrap(); |
| let has_ever_connected = false; |
| NetworkConfig::new(id, credential, has_ever_connected).unwrap() |
| } |
| |
| fn rand_string() -> String { |
| Alphanumeric.sample_string(&mut thread_rng(), 20) |
| } |
| |
| #[fuchsia::test] |
| async fn record_metrics_when_called_on_class() { |
| let stash_id = rand_string(); |
| let temp_dir = TempDir::new().expect("failed to create temporary directory"); |
| let path = temp_dir.path().join("networks.json"); |
| let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver(); |
| |
| let saved_networks = |
| SavedNetworksManager::new_with_stash_or_paths(&stash_id, &path, cobalt_api) |
| .await |
| .unwrap(); |
| let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap(); |
| |
| assert!(saved_networks.lookup(&network_id_foo).await.is_empty()); |
| assert_eq!(0, saved_networks.saved_networks.lock().await.len()); |
| assert_eq!(0, saved_networks.known_network_count().await); |
| |
| // Store a network and verify it was stored. |
| assert!(saved_networks |
| .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec())) |
| .await |
| .expect("storing 'foo' failed") |
| .is_none()); |
| assert_eq!(1, saved_networks.known_network_count().await); |
| |
| // Store another network and verify. |
| assert!(saved_networks |
| .store(network_id_baz.clone(), Credential::Psk(vec![1; 32])) |
| .await |
| .expect("storing 'baz' with PSK failed") |
| .is_none()); |
| assert_eq!(2, saved_networks.known_network_count().await); |
| |
| // Record metrics |
| saved_networks.record_periodic_metrics().await; |
| |
| // Two saved networks |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_NETWORKS_METRIC_ID) |
| .with_event_code( |
| SavedNetworksMetricDimensionSavedNetworks::TwoToFour.as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| |
| // One config for each network |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID) |
| .with_event_code( |
| SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::One |
| .as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID) |
| .with_event_code( |
| SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::One |
| .as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| |
| // No more metrics |
| assert!(cobalt_events.try_next().is_err()); |
| } |
| |
| #[fuchsia::test] |
| async fn metrics_count_configs() { |
| let (mut cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver(); |
| |
| let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(); |
| let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap(); |
| |
| let networks: NetworkConfigMap = [ |
| (network_id_foo, vec![]), |
| ( |
| network_id_baz.clone(), |
| vec![ |
| NetworkConfig::new( |
| network_id_baz.clone(), |
| Credential::Password(b"qwertyuio".to_vec()), |
| false, |
| ) |
| .unwrap(), |
| NetworkConfig::new( |
| network_id_baz, |
| Credential::Password(b"asdfasdfasdf".to_vec()), |
| false, |
| ) |
| .unwrap(), |
| ], |
| ), |
| ] |
| .iter() |
| .cloned() |
| .collect(); |
| |
| log_cobalt_metrics(&networks, &mut cobalt_api); |
| |
| // Two saved networks |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_NETWORKS_METRIC_ID) |
| .with_event_code( |
| SavedNetworksMetricDimensionSavedNetworks::TwoToFour.as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| |
| // Extract the next two events, their order is not guaranteed |
| let cobalt_metrics = vec![ |
| cobalt_events.try_next().unwrap().unwrap(), |
| cobalt_events.try_next().unwrap().unwrap(), |
| ]; |
| // Zero configs for one network |
| assert!(cobalt_metrics.iter().any(|metric| metric |
| == &CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID) |
| .with_event_code( |
| SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::Zero |
| .as_event_code() |
| ) |
| .as_event())); |
| // Two configs for the other network |
| assert!(cobalt_metrics.iter().any(|metric| metric |
| == &CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID) |
| .with_event_code( |
| SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::TwoToFour |
| .as_event_code() |
| ) |
| .as_event())); |
| |
| // No more metrics |
| assert!(cobalt_events.try_next().is_err()); |
| } |
| |
| #[fuchsia::test] |
| async fn probabilistic_choosing_of_hidden_networks() { |
| // Create three networks with 1, 0, 0.5 hidden probability |
| let id_hidden = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("hidden").unwrap(), |
| security_type: types::SecurityType::Wpa2, |
| }; |
| let mut net_config_hidden = NetworkConfig::new( |
| id_hidden.clone().into(), |
| Credential::Password(b"password".to_vec()), |
| false, |
| ) |
| .expect("failed to create network config"); |
| net_config_hidden.hidden_probability = 1.0; |
| |
| let id_not_hidden = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("not_hidden").unwrap(), |
| security_type: types::SecurityType::Wpa2, |
| }; |
| let mut net_config_not_hidden = NetworkConfig::new( |
| id_not_hidden.clone().into(), |
| Credential::Password(b"password".to_vec()), |
| false, |
| ) |
| .expect("failed to create network config"); |
| net_config_not_hidden.hidden_probability = 0.0; |
| |
| let id_maybe_hidden = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("maybe_hidden").unwrap(), |
| security_type: types::SecurityType::Wpa2, |
| }; |
| let mut net_config_maybe_hidden = NetworkConfig::new( |
| id_maybe_hidden.clone().into(), |
| Credential::Password(b"password".to_vec()), |
| false, |
| ) |
| .expect("failed to create network config"); |
| net_config_maybe_hidden.hidden_probability = 0.5; |
| |
| let mut maybe_hidden_selection_count = 0; |
| let mut hidden_selection_count = 0; |
| |
| // Run selection many times, to ensure the probability is working as expected. |
| for _ in 1..100 { |
| let selected_networks = select_subset_potentially_hidden_networks(vec![ |
| net_config_hidden.clone(), |
| net_config_not_hidden.clone(), |
| net_config_maybe_hidden.clone(), |
| ]); |
| // The 1.0 probability should always be picked |
| assert!(selected_networks.contains(&id_hidden)); |
| // The 0 probability should never be picked |
| assert!(!selected_networks.contains(&id_not_hidden)); |
| |
| // Keep track of how often the networks were selected |
| if selected_networks.contains(&id_maybe_hidden) { |
| maybe_hidden_selection_count += 1; |
| } |
| if selected_networks.contains(&id_hidden) { |
| hidden_selection_count += 1; |
| } |
| } |
| |
| // The 0.5 probability network should be picked at least once, but not every time. With 100 |
| // runs, the chances of either of these assertions flaking is 1 / (0.5^100), i.e. 1 in 1e30. |
| // Even with a hypothetical 1,000,000 test runs per day, there would be an average of 1e24 |
| // days between flakes due to this test. |
| assert!(maybe_hidden_selection_count > 0); |
| assert!(maybe_hidden_selection_count < hidden_selection_count); |
| } |
| |
| #[fuchsia::test] |
| async fn test_record_not_seen_active_scan() { |
| // Test that if we update that we haven't seen a couple of networks in active scans, their |
| // hidden probability is updated. |
| let saved_networks = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| |
| // Seen in active scans |
| let id_1 = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(); |
| let credential_1 = Credential::Password(b"some_password".to_vec()); |
| let id_2 = NetworkIdentifier::try_from("bar", SecurityType::Wpa3).unwrap(); |
| let credential_2 = Credential::Password(b"another_password".to_vec()); |
| // Seen in active scan but not saved |
| let id_3 = NetworkIdentifier::try_from("baz", SecurityType::None).unwrap(); |
| // Saved and targeted in active scan but not seen |
| let id_4 = NetworkIdentifier::try_from("foobar", SecurityType::None).unwrap(); |
| let credential_4 = Credential::None; |
| |
| // Save 3 of the 4 networks |
| assert!(saved_networks |
| .store(id_1.clone(), credential_1) |
| .await |
| .expect("failed to store network") |
| .is_none()); |
| assert!(saved_networks |
| .store(id_2.clone(), credential_2) |
| .await |
| .expect("failed to store network") |
| .is_none()); |
| assert!(saved_networks |
| .store(id_4.clone(), credential_4) |
| .await |
| .expect("failed to store network") |
| .is_none()); |
| // Check that the saved networks have the default hidden probability so later we can just |
| // check that the probability has changed. |
| let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup"); |
| assert_eq!(config_1.hidden_probability, PROB_HIDDEN_DEFAULT); |
| let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup"); |
| assert_eq!(config_2.hidden_probability, PROB_HIDDEN_DEFAULT); |
| let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup"); |
| assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| let seen_ids = vec![]; |
| let not_seen_ids = vec![id_1.clone().into(), id_2.clone().into(), id_3.clone().into()]; |
| saved_networks.record_scan_result(ScanResultType::Directed(not_seen_ids), seen_ids).await; |
| |
| // Check that the configs' hidden probability has decreased |
| let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup"); |
| assert!(config_1.hidden_probability < PROB_HIDDEN_DEFAULT); |
| let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup"); |
| assert!(config_2.hidden_probability < PROB_HIDDEN_DEFAULT); |
| |
| // Check that for the network that was target but not seen in the active scan, its hidden |
| // probability isn't lowered. |
| let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup"); |
| assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT); |
| |
| // Check that a config was not saved for the identifier that was not saved before. |
| assert!(saved_networks.lookup(&id_3).await.is_empty()); |
| } |
| |
| #[fuchsia::test] |
| async fn test_get_past_connections() { |
| let saved_networks_manager = SavedNetworksManager::new_for_test() |
| .await |
| .expect("Failed to create SavedNetworksManager"); |
| |
| let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(); |
| let credential = Credential::Password(b"some_password".to_vec()); |
| let mut config = NetworkConfig::new(id.clone(), credential.clone(), true) |
| .expect("failed to create config"); |
| let mut past_connections = PastConnectionsByBssid::new(); |
| |
| // Add two past connections with the same bssid |
| let data_1 = random_connection_data(); |
| let bssid_1 = data_1.bssid; |
| let mut data_2 = random_connection_data(); |
| data_2.bssid = bssid_1; |
| past_connections.add(bssid_1, data_1.clone()); |
| past_connections.add(bssid_1, data_2.clone()); |
| |
| // Add a past connection with different bssid |
| let data_3 = random_connection_data(); |
| let bssid_2 = data_3.bssid; |
| past_connections.add(bssid_2, data_3.clone()); |
| config.perf_stats.past_connections = past_connections; |
| |
| // Create SavedNetworksManager with configs that have past connections |
| assert!(saved_networks_manager |
| .saved_networks |
| .lock() |
| .await |
| .insert(id.clone(), vec![config]) |
| .is_none()); |
| |
| // Check that get_past_connections gets the two PastConnectionLists for the BSSIDs. |
| let mut expected_past_connections = PastConnectionList::new(); |
| expected_past_connections.add(data_1); |
| expected_past_connections.add(data_2); |
| let actual_past_connections = |
| saved_networks_manager.get_past_connections(&id, &credential, &bssid_1).await; |
| assert_eq!(actual_past_connections, expected_past_connections); |
| |
| let mut expected_past_connections = PastConnectionList::new(); |
| expected_past_connections.add(data_3); |
| let actual_past_connections = |
| saved_networks_manager.get_past_connections(&id, &credential, &bssid_2).await; |
| assert_eq!(actual_past_connections, expected_past_connections); |
| |
| // Check that get_past_connections will not get the PastConnectionLists if the specified |
| // Credential is different. |
| let actual_past_connections = saved_networks_manager |
| .get_past_connections(&id, &Credential::Password(b"other-password".to_vec()), &bssid_1) |
| .await; |
| assert_eq!(actual_past_connections, PastConnectionList::new()); |
| } |
| |
| fn fake_successful_connect_result() -> fidl_sme::ConnectResult { |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Success, |
| is_credential_rejected: false, |
| is_reconnect: false, |
| } |
| } |
| } |