| // Copyright 2021 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::{ |
| client::{ |
| scan::{self, ScanReason::NetworkSelection as NetworkSelectionScan, ScanResultUpdate}, |
| state_machine::PeriodicConnectionStats, |
| types, |
| }, |
| config_management::{ |
| network_config::{AddAndGetRecent, PastConnectionsByBssid}, |
| ConnectFailure, Credential, FailureReason, SavedNetworksManagerApi, |
| }, |
| mode_management::iface_manager_api::IfaceManagerApi, |
| telemetry::{self, TelemetryEvent, TelemetrySender}, |
| }, |
| async_trait::async_trait, |
| fidl_fuchsia_wlan_internal as fidl_internal, fidl_fuchsia_wlan_sme as fidl_sme, |
| fuchsia_async as fasync, |
| fuchsia_cobalt::CobaltSender, |
| fuchsia_inspect::{Node as InspectNode, StringReference}, |
| fuchsia_inspect_contrib::{ |
| auto_persist::{self, AutoPersist}, |
| inspect_insert, inspect_log, |
| log::{InspectList, WriteInspect}, |
| nodes::BoundedListNode as InspectBoundedListNode, |
| }, |
| fuchsia_zircon as zx, |
| futures::lock::Mutex, |
| log::{debug, error, info, trace}, |
| std::{collections::HashMap, convert::TryInto as _, sync::Arc}, |
| wlan_common::{self, hasher::WlanHasher}, |
| wlan_inspect::wrappers::InspectWlanChan, |
| wlan_metrics_registry::{ |
| SavedNetworkInScanResultMetricDimensionBssCount, |
| SavedNetworkInScanResultWithActiveScanMetricDimensionActiveScanSsidsObserved as ActiveScanSsidsObserved, |
| ScanResultsReceivedMetricDimensionSavedNetworksCount, |
| LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_METRIC_ID, SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID, |
| SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_METRIC_ID, SCAN_RESULTS_RECEIVED_METRIC_ID, |
| }, |
| }; |
| |
| const RECENT_FAILURE_WINDOW: zx::Duration = zx::Duration::from_seconds(60 * 5); // 5 minutes |
| const RECENT_DISCONNECT_WINDOW: zx::Duration = zx::Duration::from_seconds(60 * 15); // 15 minutes |
| |
| // TODO(fxbug.dev/67791) Remove code or rework cache to be useful |
| // TODO(fxbug.dev/61992) Tweak duration |
| const STALE_SCAN_AGE: zx::Duration = zx::Duration::from_millis(50); |
| |
| /// Above or at this RSSI, we'll give 5G networks a preference |
| const RSSI_CUTOFF_5G_PREFERENCE: i16 = -64; |
| /// The score boost for 5G networks that we are giving preference to. |
| const RSSI_5G_PREFERENCE_BOOST: i16 = 20; |
| /// The amount to decrease the score by for each failed connection attempt. |
| const SCORE_PENALTY_FOR_RECENT_FAILURE: i16 = 5; |
| /// This penalty is much higher than for a general failure because we are not likely to succeed |
| /// on a retry. |
| const SCORE_PENALTY_FOR_RECENT_CREDENTIAL_REJECTED: i16 = 30; |
| /// The amount to decrease the score for each time we are connected for only a short amount |
| /// of time before disconncting. This amount is the same as the penalty for 4 failed connect |
| /// attempts to a BSS. |
| const SCORE_PENALTY_FOR_SHORT_CONNECTION: i16 = 20; |
| // Threshold for what we consider a short time to be connected |
| const SHORT_CONNECT_DURATION: zx::Duration = zx::Duration::from_seconds(7 * 60); |
| |
| const INSPECT_EVENT_LIMIT_FOR_NETWORK_SELECTIONS: usize = 10; |
| |
| pub struct NetworkSelector { |
| saved_network_manager: Arc<dyn SavedNetworksManagerApi>, |
| scan_result_cache: Arc<Mutex<ScanResultCache>>, |
| cobalt_api: Arc<Mutex<CobaltSender>>, |
| hasher: WlanHasher, |
| _inspect_node_root: Arc<Mutex<InspectNode>>, |
| inspect_node_for_network_selections: Arc<Mutex<AutoPersist<InspectBoundedListNode>>>, |
| telemetry_sender: TelemetrySender, |
| } |
| |
| struct ScanResultCache { |
| updated_at: zx::Time, |
| results: Vec<types::ScanResult>, |
| } |
| |
| #[derive(Debug, PartialEq, Clone)] |
| struct InternalSavedNetworkData { |
| network_id: types::NetworkIdentifier, |
| credential: Credential, |
| has_ever_connected: bool, |
| recent_failures: Vec<ConnectFailure>, |
| past_connections: PastConnectionsByBssid, |
| } |
| |
| #[derive(Debug, Clone, PartialEq)] |
| struct InternalBss<'a> { |
| saved_network_info: InternalSavedNetworkData, |
| scanned_bss: &'a types::Bss, |
| security_type_detailed: types::SecurityTypeDetailed, |
| multiple_bss_candidates: bool, |
| hasher: WlanHasher, |
| } |
| |
| impl InternalBss<'_> { |
| /// This function scores a BSS based on 3 factors: (1) RSSI (2) whether the BSS is 2.4 or 5 GHz |
| /// and (3) recent failures to connect to this BSS. No single factor is enough to decide which |
| /// BSS to connect to. |
| fn score(&self) -> i16 { |
| let mut score = self.scanned_bss.rssi as i16; |
| let channel = types::WlanChan::from(self.scanned_bss.channel); |
| |
| // If the network is 5G and has a strong enough RSSI, give it a bonus |
| if channel.is_5ghz() && score >= RSSI_CUTOFF_5G_PREFERENCE { |
| score = score.saturating_add(RSSI_5G_PREFERENCE_BOOST); |
| } |
| |
| // Penalize APs with recent failures to connect |
| let matching_failures = self |
| .saved_network_info |
| .recent_failures |
| .iter() |
| .filter(|failure| failure.bssid == self.scanned_bss.bssid); |
| for failure in matching_failures { |
| // Count failures for rejected credentials higher since we probably won't succeed |
| // another try with the same credentials. |
| if failure.reason == FailureReason::CredentialRejected { |
| score = score.saturating_sub(SCORE_PENALTY_FOR_RECENT_CREDENTIAL_REJECTED); |
| } else { |
| score = score.saturating_sub(SCORE_PENALTY_FOR_RECENT_FAILURE); |
| } |
| } |
| // Penalize APs with recent short connections before disconnecting. |
| let short_connection_score: i16 = self |
| .recent_short_connections() |
| .try_into() |
| .unwrap_or_else(|_| i16::MAX) |
| .saturating_mul(SCORE_PENALTY_FOR_SHORT_CONNECTION); |
| |
| return score.saturating_sub(short_connection_score); |
| } |
| |
| fn recent_failure_count(&self) -> u64 { |
| self.saved_network_info |
| .recent_failures |
| .iter() |
| .filter(|failure| failure.bssid == self.scanned_bss.bssid) |
| .count() |
| .try_into() |
| .unwrap_or_else(|e| { |
| error!("{}", e); |
| u64::MAX |
| }) |
| } |
| |
| fn recent_short_connections(&self) -> usize { |
| self.saved_network_info |
| .past_connections |
| .get_list_for_bss(&self.scanned_bss.bssid) |
| .get_recent(fasync::Time::now() - RECENT_DISCONNECT_WINDOW) |
| .iter() |
| .filter(|d| d.connection_uptime < SHORT_CONNECT_DURATION) |
| .collect::<Vec<_>>() |
| .len() |
| } |
| |
| fn saved_security_type_to_string(&self) -> String { |
| match self.saved_network_info.network_id.security_type { |
| types::SecurityType::None => "open", |
| types::SecurityType::Wep => "WEP", |
| types::SecurityType::Wpa => "WPA1", |
| types::SecurityType::Wpa2 => "WPA2", |
| types::SecurityType::Wpa3 => "WPA3", |
| } |
| .to_string() |
| } |
| |
| fn scanned_security_type_to_string(&self) -> String { |
| match self.security_type_detailed { |
| types::SecurityTypeDetailed::Unknown => "unknown", |
| types::SecurityTypeDetailed::Open => "open", |
| types::SecurityTypeDetailed::Wep => "WEP", |
| types::SecurityTypeDetailed::Wpa1 => "WPA1", |
| types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly => "WPA1/2Tk", |
| types::SecurityTypeDetailed::Wpa2PersonalTkipOnly => "WPA2Tk", |
| types::SecurityTypeDetailed::Wpa1Wpa2Personal => "WPA1/2", |
| types::SecurityTypeDetailed::Wpa2Personal => "WPA2", |
| types::SecurityTypeDetailed::Wpa2Wpa3Personal => "WPA2/3", |
| types::SecurityTypeDetailed::Wpa3Personal => "WPA3", |
| types::SecurityTypeDetailed::Wpa2Enterprise => "WPA2Ent", |
| types::SecurityTypeDetailed::Wpa3Enterprise => "WPA3Ent", |
| } |
| .to_string() |
| } |
| |
| fn to_string_without_pii(&self) -> String { |
| let channel = types::WlanChan::from(self.scanned_bss.channel); |
| let rssi = self.scanned_bss.rssi; |
| let recent_failure_count = self.recent_failure_count(); |
| let recent_short_connection_count = self.recent_short_connections(); |
| format!( |
| "{}({:4}), {}({:6}), {:>4}dBm, channel {:8}, score {:4}{}{}{}{}", |
| self.hasher.hash_ssid(&self.saved_network_info.network_id.ssid), |
| self.saved_security_type_to_string(), |
| self.hasher.hash_mac_addr(&self.scanned_bss.bssid.0), |
| self.scanned_security_type_to_string(), |
| rssi, |
| channel, |
| self.score(), |
| if !self.scanned_bss.compatible { ", NOT compatible" } else { "" }, |
| if recent_failure_count > 0 { |
| format!(", {} recent failures", recent_failure_count) |
| } else { |
| "".to_string() |
| }, |
| if recent_short_connection_count > 0 { |
| format!(", {} recent short disconnects", recent_short_connection_count) |
| } else { |
| "".to_string() |
| }, |
| if !self.saved_network_info.has_ever_connected { ", never used yet" } else { "" }, |
| ) |
| } |
| } |
| impl<'a> WriteInspect for InternalBss<'a> { |
| fn write_inspect<'b>(&self, writer: &InspectNode, key: impl Into<StringReference<'b>>) { |
| inspect_insert!(writer, var key: { |
| ssid_hash: self.hasher.hash_ssid(&self.saved_network_info.network_id.ssid), |
| bssid_hash: self.hasher.hash_mac_addr(&self.scanned_bss.bssid.0), |
| rssi: self.scanned_bss.rssi, |
| score: self.score(), |
| security_type_saved: self.saved_security_type_to_string(), |
| security_type_scanned: format!("{}", wlan_common::bss::Protection::from(self.security_type_detailed)), |
| channel: InspectWlanChan(&self.scanned_bss.channel.into()), |
| compatible: self.scanned_bss.compatible, |
| recent_failure_count: self.recent_failure_count(), |
| saved_network_has_ever_connected: self.saved_network_info.has_ever_connected, |
| }); |
| } |
| } |
| |
| impl NetworkSelector { |
| pub fn new( |
| saved_network_manager: Arc<dyn SavedNetworksManagerApi>, |
| cobalt_api: CobaltSender, |
| hasher: WlanHasher, |
| inspect_node: InspectNode, |
| persistence_req_sender: auto_persist::PersistenceReqSender, |
| telemetry_sender: TelemetrySender, |
| ) -> Self { |
| let inspect_node_for_network_selection = InspectBoundedListNode::new( |
| inspect_node.create_child("network_selection"), |
| INSPECT_EVENT_LIMIT_FOR_NETWORK_SELECTIONS, |
| ); |
| let inspect_node_for_network_selection = AutoPersist::new( |
| inspect_node_for_network_selection, |
| "wlancfg-network-selection", |
| persistence_req_sender.clone(), |
| ); |
| Self { |
| saved_network_manager, |
| scan_result_cache: Arc::new(Mutex::new(ScanResultCache { |
| updated_at: zx::Time::ZERO, |
| results: Vec::new(), |
| })), |
| cobalt_api: Arc::new(Mutex::new(cobalt_api)), |
| hasher, |
| _inspect_node_root: Arc::new(Mutex::new(inspect_node)), |
| inspect_node_for_network_selections: Arc::new(Mutex::new( |
| inspect_node_for_network_selection, |
| )), |
| telemetry_sender, |
| } |
| } |
| |
| pub fn generate_scan_result_updater(&self) -> NetworkSelectorScanUpdater { |
| NetworkSelectorScanUpdater { |
| scan_result_cache: Arc::clone(&self.scan_result_cache), |
| saved_network_manager: Arc::clone(&self.saved_network_manager), |
| cobalt_api: Arc::clone(&self.cobalt_api), |
| hasher: self.hasher.clone(), |
| } |
| } |
| |
| async fn perform_scan(&self, iface_manager: Arc<Mutex<dyn IfaceManagerApi + Send>>) { |
| // Get the scan age. |
| let scan_result_guard = self.scan_result_cache.lock().await; |
| let last_scan_result_time = scan_result_guard.updated_at; |
| drop(scan_result_guard); |
| let scan_age = zx::Time::get_monotonic() - last_scan_result_time; |
| |
| // Log a metric for scan age, to help us optimize the STALE_SCAN_AGE |
| if last_scan_result_time != zx::Time::ZERO { |
| let mut cobalt_api_guard = self.cobalt_api.lock().await; |
| let cobalt_api = &mut *cobalt_api_guard; |
| cobalt_api.log_elapsed_time( |
| LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_METRIC_ID, |
| Vec::<u32>::new(), |
| scan_age.into_micros(), |
| ); |
| drop(cobalt_api_guard); |
| } |
| |
| // Determine if a new scan is warranted |
| if scan_age >= STALE_SCAN_AGE { |
| if last_scan_result_time != zx::Time::ZERO { |
| info!("Scan results are {}s old, triggering a scan", scan_age.into_seconds()); |
| } |
| |
| // Clear out the old scan results |
| let mut scan_result_guard = self.scan_result_cache.lock().await; |
| scan_result_guard.results = vec![]; |
| drop(scan_result_guard); |
| |
| let wpa3_supported = |
| iface_manager.lock().await.has_wpa3_capable_client().await.unwrap_or_else(|e| { |
| error!("Failed to determine WPA3 support. Assuming no WPA3 support. {}", e); |
| false |
| }); |
| |
| scan::perform_scan( |
| iface_manager, |
| self.saved_network_manager.clone(), |
| None, |
| self.generate_scan_result_updater(), |
| scan::LocationSensorUpdater { wpa3_supported }, |
| NetworkSelectionScan, |
| Some(self.cobalt_api.clone()), |
| ) |
| .await; |
| } else { |
| info!("Using cached scan results from {}s ago", scan_age.into_seconds()); |
| } |
| } |
| |
| /// Select the best available network, based on the current saved networks and the most |
| /// recent scan results provided to this module. |
| /// Only networks that are both saved and visible in the most recent scan results are eligible |
| /// for consideration. Among those, the "best" network based on compatibility and quality (e.g. |
| /// RSSI, recent failures) is selected. |
| pub(crate) async fn find_best_connection_candidate( |
| &self, |
| iface_manager: Arc<Mutex<dyn IfaceManagerApi + Send>>, |
| ignore_list: &Vec<types::NetworkIdentifier>, |
| ) -> Option<types::ConnectionCandidate> { |
| self.perform_scan(iface_manager.clone()).await; |
| let scan_result_guard = self.scan_result_cache.lock().await; |
| let networks = merge_saved_networks_and_scan_data( |
| &self.saved_network_manager, |
| &scan_result_guard.results, |
| &self.hasher, |
| ) |
| .await; |
| // TODO(fxbug.dev/78170): When there's a scan error, this should be an `Err`, not `Ok(0)`. |
| let num_candidates = Ok(networks.len()); |
| |
| let mut inspect_node = self.inspect_node_for_network_selections.lock().await; |
| let result = |
| match select_best_connection_candidate(networks, ignore_list, &mut inspect_node) { |
| Some((selected, channel, bssid)) => Some( |
| augment_bss_with_active_scan(selected, channel, bssid, iface_manager).await, |
| ), |
| None => None, |
| }; |
| |
| self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision { |
| network_selection_type: telemetry::NetworkSelectionType::Undirected, |
| num_candidates, |
| selected_any: result.is_some(), |
| }); |
| result |
| } |
| |
| /// Find a suitable BSS for the given network. |
| pub(crate) async fn find_connection_candidate_for_network( |
| &self, |
| sme_proxy: fidl_sme::ClientSmeProxy, |
| network: types::NetworkIdentifier, |
| ) -> Option<types::ConnectionCandidate> { |
| // TODO: check if we have recent enough scan results that we can pull from instead? |
| let scan_results = |
| scan::perform_directed_active_scan(&sme_proxy, &network.ssid, None).await; |
| |
| let (result, num_candidates) = match scan_results { |
| Err(_) => (None, Err(())), |
| Ok(scan_results) => { |
| let networks = merge_saved_networks_and_scan_data( |
| &self.saved_network_manager, |
| &scan_results, |
| &self.hasher, |
| ) |
| .await; |
| let num_candidates = Ok(networks.len()); |
| let ignore_list = vec![]; |
| let mut inspect_node = self.inspect_node_for_network_selections.lock().await; |
| let result = |
| select_best_connection_candidate(networks, &ignore_list, &mut inspect_node) |
| .map(|(mut candidate, _, _)| { |
| // Strip out the information about passive vs active scan, because we can't know |
| // if this network would have been observed in a passive scan (since we never |
| // performed a passive scan). |
| if let Some(types::ScannedCandidate { ref mut observation, .. }) = |
| candidate.scanned |
| { |
| *observation = types::ScanObservation::Unknown; |
| } |
| candidate |
| }); |
| (result, num_candidates) |
| } |
| }; |
| |
| self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision { |
| network_selection_type: telemetry::NetworkSelectionType::Directed, |
| num_candidates, |
| selected_any: result.is_some(), |
| }); |
| result |
| } |
| } |
| |
| /// Merge the saved networks and scan results into a vector of BSSs that correspond to a saved |
| /// network. |
| async fn merge_saved_networks_and_scan_data<'a>( |
| saved_network_manager: &Arc<dyn SavedNetworksManagerApi>, |
| scan_results: &'a Vec<types::ScanResult>, |
| hasher: &WlanHasher, |
| ) -> Vec<InternalBss<'a>> { |
| let mut merged_networks = vec![]; |
| for scan_result in scan_results { |
| for saved_config in saved_network_manager |
| .lookup_compatible(&scan_result.ssid, scan_result.security_type_detailed) |
| .await |
| { |
| let multiple_bss_candidates = scan_result.entries.len() > 1; |
| for bss in &scan_result.entries { |
| merged_networks.push(InternalBss { |
| scanned_bss: bss, |
| multiple_bss_candidates, |
| security_type_detailed: scan_result.security_type_detailed, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: types::NetworkIdentifier { |
| ssid: saved_config.ssid.clone(), |
| security_type: saved_config.security_type.into(), |
| }, |
| credential: saved_config.credential.clone(), |
| has_ever_connected: saved_config.has_ever_connected, |
| recent_failures: saved_config |
| .perf_stats |
| .connect_failures |
| .get_recent_for_network(fasync::Time::now() - RECENT_FAILURE_WINDOW), |
| past_connections: saved_config.perf_stats.past_connections.clone(), |
| }, |
| hasher: hasher.clone(), |
| }) |
| } |
| } |
| } |
| merged_networks |
| } |
| |
| pub struct NetworkSelectorScanUpdater { |
| scan_result_cache: Arc<Mutex<ScanResultCache>>, |
| saved_network_manager: Arc<dyn SavedNetworksManagerApi>, |
| cobalt_api: Arc<Mutex<CobaltSender>>, |
| hasher: WlanHasher, |
| } |
| #[async_trait] |
| impl ScanResultUpdate for NetworkSelectorScanUpdater { |
| async fn update_scan_results(&mut self, scan_results: &Vec<types::ScanResult>) { |
| // Update internal scan result cache |
| let scan_results_clone = scan_results.clone(); |
| let mut scan_result_guard = self.scan_result_cache.lock().await; |
| scan_result_guard.results = scan_results_clone; |
| scan_result_guard.updated_at = zx::Time::get_monotonic(); |
| drop(scan_result_guard); |
| |
| // Record metrics for this scan |
| let merged_networks = merge_saved_networks_and_scan_data( |
| &self.saved_network_manager, |
| scan_results, |
| &self.hasher, |
| ) |
| .await; |
| let mut cobalt_api_guard = self.cobalt_api.lock().await; |
| let cobalt_api = &mut *cobalt_api_guard; |
| record_metrics_on_scan(merged_networks, cobalt_api); |
| drop(cobalt_api_guard); |
| } |
| } |
| |
| fn select_best_connection_candidate<'a>( |
| bss_list: Vec<InternalBss<'a>>, |
| ignore_list: &Vec<types::NetworkIdentifier>, |
| inspect_node: &mut AutoPersist<InspectBoundedListNode>, |
| ) -> Option<(types::ConnectionCandidate, types::WlanChan, types::Bssid)> { |
| info!("Selecting from {} BSSs found for saved networks", bss_list.len()); |
| |
| let selected = bss_list |
| .iter() |
| .inspect(|bss| { |
| info!("{}", bss.to_string_without_pii()); |
| }) |
| .filter(|bss| { |
| // Filter out incompatible BSSs |
| if !bss.scanned_bss.compatible { |
| trace!("BSS is incompatible, filtering: {:?}", bss); |
| return false; |
| }; |
| // Filter out networks we've been told to ignore |
| if ignore_list.contains(&bss.saved_network_info.network_id) { |
| trace!("Network is ignored, filtering: {:?}", bss); |
| return false; |
| } |
| true |
| }) |
| .max_by_key(|bss| bss.score()); |
| |
| // Log the candidates into Inspect |
| inspect_log!(inspect_node.get_mut(), candidates: InspectList(&bss_list), selected?: selected); |
| |
| selected.map(|bss| { |
| info!("Selected BSS:"); |
| info!("{}", bss.to_string_without_pii()); |
| ( |
| types::ConnectionCandidate { |
| network: bss.saved_network_info.network_id.clone(), |
| credential: bss.saved_network_info.credential.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss.scanned_bss.bss_description.clone(), |
| observation: bss.scanned_bss.observation, |
| has_multiple_bss_candidates: bss.multiple_bss_candidates, |
| security_type_detailed: bss.security_type_detailed, |
| }), |
| }, |
| bss.scanned_bss.channel, |
| bss.scanned_bss.bssid, |
| ) |
| }) |
| } |
| |
| /// If a BSS was discovered via a passive scan, we need to perform an active scan on it to discover |
| /// all the information potentially needed by the SME layer. |
| async fn augment_bss_with_active_scan( |
| mut selected_network: types::ConnectionCandidate, |
| channel: types::WlanChan, |
| bssid: types::Bssid, |
| iface_manager: Arc<Mutex<dyn IfaceManagerApi + Send>>, |
| ) -> types::ConnectionCandidate { |
| // This internal function encapsulates all the logic and has a Result<> return type, allowing us |
| // to use the `?` operator inside it to reduce nesting. |
| async fn get_enhanced_bss_description( |
| selected_network: &types::ConnectionCandidate, |
| channel: types::WlanChan, |
| bssid: types::Bssid, |
| iface_manager: Arc<Mutex<dyn IfaceManagerApi + Send>>, |
| ) -> Result<fidl_internal::BssDescription, ()> { |
| // Ensure that a scan is necessary. If this expression returns `Unknown`, then either the |
| // network has been scanned but the observation is unknown or the network has not be |
| // scanned at all. |
| let observation = selected_network |
| .scanned |
| .as_ref() |
| .map_or(types::ScanObservation::Unknown, |scanned| scanned.observation); |
| match observation { |
| types::ScanObservation::Passive => { |
| info!("Performing directed active scan on selected network") |
| } |
| types::ScanObservation::Active => { |
| debug!("Network already discovered via active scan."); |
| return Err(()); |
| } |
| types::ScanObservation::Unknown => { |
| error!("Unexpected `Unknown` variant of network `observation`."); |
| return Err(()); |
| } |
| } |
| |
| // Get an SME proxy |
| let mut iface_manager_guard = iface_manager.lock().await; |
| let sme_proxy = iface_manager_guard.get_sme_proxy_for_scan().await.map_err(|e| { |
| info!("Failed to get an SME proxy for scan: {:?}", e); |
| })?; |
| drop(iface_manager_guard); |
| |
| // Perform the scan |
| let mut directed_scan_result = scan::perform_directed_active_scan( |
| &sme_proxy, |
| &selected_network.network.ssid, |
| Some(vec![channel.primary]), |
| ) |
| .await |
| .map_err(|_| { |
| info!("Failed to perform active scan to augment BSS info."); |
| })?; |
| |
| // Find the bss in the results |
| let bss_description = directed_scan_result |
| .drain(..) |
| .find_map(|mut network| { |
| if network.ssid == selected_network.network.ssid { |
| for bss in network.entries.drain(..) { |
| if bss.bssid == bssid { |
| return Some(bss.bss_description); |
| } |
| } |
| } |
| None |
| }) |
| .ok_or_else(|| { |
| info!("BSS info will lack active scan augmentation, proceeding anyway."); |
| })?; |
| |
| Ok(bss_description) |
| } |
| |
| match get_enhanced_bss_description(&selected_network, channel, bssid, iface_manager).await { |
| Ok(new_bss_description) => { |
| let combined_scanned = selected_network.scanned.take().map(|original_scanned| { |
| types::ScannedCandidate { bss_description: new_bss_description, ..original_scanned } |
| }); |
| types::ConnectionCandidate { scanned: combined_scanned, ..selected_network } |
| } |
| Err(()) => selected_network, |
| } |
| } |
| |
| /// Give a numerical score to the connection quality in order to decide whether to look for a new |
| /// network and to ultimately decide whether to switch to a new network or stay on the same one. |
| /// score should be between 0 and 1, where 0 is an unusable connection and 1 is a great connection. |
| pub fn score_connection_quality(_connection_stats: &PeriodicConnectionStats) -> f32 { |
| // TODO(fxbug.dev/84551) Actually implement the connection quality scoring and the threshold |
| // for a bad connection |
| return 1.0; |
| } |
| |
| fn record_metrics_on_scan( |
| mut merged_networks: Vec<InternalBss<'_>>, |
| cobalt_api: &mut CobaltSender, |
| ) { |
| let mut merged_network_map: HashMap<types::NetworkIdentifier, Vec<InternalBss<'_>>> = |
| HashMap::new(); |
| for bss in merged_networks.drain(..) { |
| merged_network_map.entry(bss.saved_network_info.network_id.clone()).or_default().push(bss); |
| } |
| |
| let num_saved_networks_observed = merged_network_map.len(); |
| let mut num_actively_scanned_networks = 0; |
| for (_network_id, bsss) in merged_network_map { |
| // Record how many BSSs are visible in the scan results for this saved network. |
| let num_bss = match bsss.len() { |
| 0 => unreachable!(), // The ::Zero enum exists, but we shouldn't get a scan result with no BSS |
| 1 => SavedNetworkInScanResultMetricDimensionBssCount::One, |
| 2..=4 => SavedNetworkInScanResultMetricDimensionBssCount::TwoToFour, |
| 5..=10 => SavedNetworkInScanResultMetricDimensionBssCount::FiveToTen, |
| 11..=20 => SavedNetworkInScanResultMetricDimensionBssCount::ElevenToTwenty, |
| 21..=usize::MAX => SavedNetworkInScanResultMetricDimensionBssCount::TwentyOneOrMore, |
| _ => unreachable!(), |
| }; |
| cobalt_api.log_event(SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID, num_bss); |
| |
| // Check if the network was found via active scan. |
| if bsss |
| .iter() |
| .any(|bss| matches!(bss.scanned_bss.observation, types::ScanObservation::Active)) |
| { |
| num_actively_scanned_networks += 1; |
| }; |
| } |
| |
| let saved_network_count_metric = match num_saved_networks_observed { |
| 0 => ScanResultsReceivedMetricDimensionSavedNetworksCount::Zero, |
| 1 => ScanResultsReceivedMetricDimensionSavedNetworksCount::One, |
| 2..=4 => ScanResultsReceivedMetricDimensionSavedNetworksCount::TwoToFour, |
| 5..=20 => ScanResultsReceivedMetricDimensionSavedNetworksCount::FiveToTwenty, |
| 21..=40 => ScanResultsReceivedMetricDimensionSavedNetworksCount::TwentyOneToForty, |
| 41..=usize::MAX => ScanResultsReceivedMetricDimensionSavedNetworksCount::FortyOneOrMore, |
| _ => unreachable!(), |
| }; |
| cobalt_api.log_event(SCAN_RESULTS_RECEIVED_METRIC_ID, saved_network_count_metric); |
| |
| let actively_scanned_networks_metrics = match num_actively_scanned_networks { |
| 0 => ActiveScanSsidsObserved::Zero, |
| 1 => ActiveScanSsidsObserved::One, |
| 2..=4 => ActiveScanSsidsObserved::TwoToFour, |
| 5..=10 => ActiveScanSsidsObserved::FiveToTen, |
| 11..=20 => ActiveScanSsidsObserved::ElevenToTwenty, |
| 21..=50 => ActiveScanSsidsObserved::TwentyOneToFifty, |
| 51..=100 => ActiveScanSsidsObserved::FiftyOneToOneHundred, |
| 101..=usize::MAX => ActiveScanSsidsObserved::OneHundredAndOneOrMore, |
| _ => unreachable!(), |
| }; |
| cobalt_api.log_event( |
| SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_METRIC_ID, |
| actively_scanned_networks_metrics, |
| ); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{ |
| access_point::state_machine as ap_fsm, |
| config_management::{ |
| network_config::{PastConnectionData, PastConnectionsByBssid}, |
| SavedNetworksManager, |
| }, |
| util::testing::{ |
| create_inspect_persistence_channel, create_mock_cobalt_sender_and_receiver, |
| create_wlan_hasher, generate_channel, generate_random_bss, |
| generate_random_scan_result, |
| poll_for_and_validate_sme_scan_request_and_send_results, random_connection_data, |
| validate_sme_scan_request_and_send_results, |
| }, |
| }, |
| anyhow::Error, |
| cobalt_client::traits::AsEventCode, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_cobalt::CobaltEvent, |
| fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, |
| fidl_fuchsia_wlan_sme as fidl_sme, fuchsia_async as fasync, |
| fuchsia_cobalt::cobalt_event_builder::CobaltEventExt, |
| fuchsia_inspect::{self as inspect, assert_data_tree}, |
| futures::{ |
| channel::{mpsc, oneshot}, |
| prelude::*, |
| task::Poll, |
| }, |
| pin_utils::pin_mut, |
| rand::Rng, |
| std::{ |
| convert::{TryFrom, TryInto}, |
| sync::Arc, |
| }, |
| test_case::test_case, |
| wlan_common::{assert_variant, random_fidl_bss_description}, |
| }; |
| |
| struct TestValues { |
| network_selector: Arc<NetworkSelector>, |
| saved_network_manager: Arc<dyn SavedNetworksManagerApi>, |
| cobalt_events: mpsc::Receiver<CobaltEvent>, |
| iface_manager: Arc<Mutex<FakeIfaceManager>>, |
| sme_stream: fidl_sme::ClientSmeRequestStream, |
| inspector: inspect::Inspector, |
| telemetry_receiver: mpsc::Receiver<TelemetryEvent>, |
| } |
| |
| async fn test_setup() -> TestValues { |
| // setup modules |
| let (cobalt_api, cobalt_events) = create_mock_cobalt_sender_and_receiver(); |
| let saved_network_manager = Arc::new(SavedNetworksManager::new_for_test().await.unwrap()); |
| let inspector = inspect::Inspector::new(); |
| let inspect_node = inspector.root().create_child("net_select_test"); |
| let (persistence_req_sender, _persistence_stream) = create_inspect_persistence_channel(); |
| let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100); |
| |
| let network_selector = Arc::new(NetworkSelector::new( |
| saved_network_manager.clone(), |
| cobalt_api, |
| create_wlan_hasher(), |
| inspect_node, |
| persistence_req_sender, |
| TelemetrySender::new(telemetry_sender), |
| )); |
| let (client_sme, remote) = |
| create_proxy::<fidl_sme::ClientSmeMarker>().expect("error creating proxy"); |
| let iface_manager = Arc::new(Mutex::new(FakeIfaceManager::new(client_sme))); |
| |
| TestValues { |
| network_selector, |
| saved_network_manager, |
| cobalt_events, |
| iface_manager, |
| sme_stream: remote.into_stream().expect("failed to create stream"), |
| inspector, |
| telemetry_receiver, |
| } |
| } |
| |
| struct FakeIfaceManager { |
| pub sme_proxy: fidl_fuchsia_wlan_sme::ClientSmeProxy, |
| } |
| |
| impl FakeIfaceManager { |
| pub fn new(proxy: fidl_fuchsia_wlan_sme::ClientSmeProxy) -> Self { |
| FakeIfaceManager { sme_proxy: proxy } |
| } |
| } |
| |
| #[async_trait] |
| impl IfaceManagerApi for FakeIfaceManager { |
| async fn disconnect( |
| &mut self, |
| _network_id: types::NetworkIdentifier, |
| _reason: types::DisconnectReason, |
| ) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn connect( |
| &mut self, |
| _connect_req: types::ConnectRequest, |
| ) -> Result<oneshot::Receiver<()>, Error> { |
| unimplemented!() |
| } |
| |
| async fn record_idle_client(&mut self, _iface_id: u16) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn has_idle_client(&mut self) -> Result<bool, Error> { |
| unimplemented!() |
| } |
| |
| async fn handle_added_iface(&mut self, _iface_id: u16) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn handle_removed_iface(&mut self, _iface_id: u16) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn scan( |
| &mut self, |
| mut scan_request: fidl_sme::ScanRequest, |
| ) -> Result<fidl_fuchsia_wlan_sme::ScanTransactionProxy, Error> { |
| let (local, remote) = fidl::endpoints::create_proxy()?; |
| let _ = self.sme_proxy.scan(&mut scan_request, remote); |
| Ok(local) |
| } |
| |
| async fn get_sme_proxy_for_scan( |
| &mut self, |
| ) -> Result<fidl_fuchsia_wlan_sme::ClientSmeProxy, Error> { |
| Ok(self.sme_proxy.clone()) |
| } |
| |
| async fn stop_client_connections( |
| &mut self, |
| _reason: types::DisconnectReason, |
| ) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn start_client_connections(&mut self) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn start_ap( |
| &mut self, |
| _config: ap_fsm::ApConfig, |
| ) -> Result<oneshot::Receiver<()>, Error> { |
| unimplemented!() |
| } |
| |
| async fn stop_ap(&mut self, _ssid: types::Ssid, _password: Vec<u8>) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| async fn stop_all_aps(&mut self) -> Result<(), Error> { |
| unimplemented!() |
| } |
| |
| // Many tests use wpa3 networks expecting them to be used normally, so by default this |
| // is true. |
| async fn has_wpa3_capable_client(&mut self) -> Result<bool, Error> { |
| Ok(true) |
| } |
| |
| async fn set_country( |
| &mut self, |
| _country_code: Option<[u8; types::REGION_CODE_LEN]>, |
| ) -> Result<(), Error> { |
| unimplemented!() |
| } |
| } |
| |
| fn generate_random_saved_network() -> (types::NetworkIdentifier, InternalSavedNetworkData) { |
| let mut rng = rand::thread_rng(); |
| let net_id = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from(format!("saved network rand {}", rng.gen::<i32>())) |
| .expect("Failed to create random SSID from String"), |
| security_type: types::SecurityType::Wpa, |
| }; |
| ( |
| net_id.clone(), |
| InternalSavedNetworkData { |
| network_id: net_id, |
| credential: Credential::Password( |
| format!("password {}", rng.gen::<i32>()).as_bytes().to_vec(), |
| ), |
| has_ever_connected: false, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| ) |
| } |
| |
| #[fuchsia::test] |
| async fn scan_results_are_stored() { |
| let mut test_values = test_setup().await; |
| let network_selector = test_values.network_selector; |
| |
| // check there are 0 scan results to start with |
| let guard = network_selector.scan_result_cache.lock().await; |
| assert_eq!(guard.results.len(), 0); |
| drop(guard); |
| |
| // provide some new scan results |
| let mock_scan_results = vec![generate_random_scan_result(), generate_random_scan_result()]; |
| let mut updater = network_selector.generate_scan_result_updater(); |
| updater.update_scan_results(&mock_scan_results).await; |
| |
| // check that the scan results are stored |
| let guard = network_selector.scan_result_cache.lock().await; |
| assert_eq!(guard.results, mock_scan_results); |
| |
| // check there are some metric events for the incoming scan results |
| // note: the actual metrics are checked in unit tests for the metric recording function |
| assert!(test_values.cobalt_events.try_next().unwrap().is_some()); |
| } |
| |
| #[fuchsia::test] |
| async fn scan_results_merged_with_saved_networks() { |
| let test_values = test_setup().await; |
| |
| // create some identifiers |
| let test_ssid_1 = types::Ssid::try_from("foo").unwrap(); |
| let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal; |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: test_ssid_1.clone(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_ssid_2 = types::Ssid::try_from("bar").unwrap(); |
| let test_security_2 = types::SecurityTypeDetailed::Wpa1; |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: test_ssid_2.clone(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| // insert the saved networks |
| assert!(test_values |
| .saved_network_manager |
| .store(test_id_1.clone().into(), credential_1.clone()) |
| .await |
| .unwrap() |
| .is_none()); |
| |
| assert!(test_values |
| .saved_network_manager |
| .store(test_id_2.clone().into(), credential_2.clone()) |
| .await |
| .unwrap() |
| .is_none()); |
| |
| // build some scan results |
| let mock_scan_results = vec![ |
| types::ScanResult { |
| ssid: test_ssid_1.clone(), |
| security_type_detailed: test_security_1.clone(), |
| entries: vec![generate_random_bss(), generate_random_bss(), generate_random_bss()], |
| compatibility: types::Compatibility::Supported, |
| }, |
| types::ScanResult { |
| ssid: test_ssid_2.clone(), |
| security_type_detailed: test_security_2.clone(), |
| entries: vec![generate_random_bss()], |
| compatibility: types::Compatibility::DisallowedNotSupported, |
| }, |
| ]; |
| |
| let bssid_1 = mock_scan_results[0].entries[0].bssid; |
| let bssid_2 = mock_scan_results[0].entries[1].bssid; |
| |
| // mark the first one as having connected |
| test_values |
| .saved_network_manager |
| .record_connect_result( |
| test_id_1.clone().into(), |
| &credential_1.clone(), |
| bssid_1, |
| fake_successful_connect_result(), |
| None, |
| ) |
| .await; |
| |
| // mark the second one as having a failure |
| test_values |
| .saved_network_manager |
| .record_connect_result( |
| test_id_1.clone().into(), |
| &credential_1.clone(), |
| bssid_2, |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| is_credential_rejected: true, |
| ..fake_successful_connect_result() |
| }, |
| None, |
| ) |
| .await; |
| |
| // build our expected result |
| let failure_time = test_values |
| .saved_network_manager |
| .lookup(&test_id_1.clone().into()) |
| .await |
| .get(0) |
| .expect("failed to get config") |
| .perf_stats |
| .connect_failures |
| .get_recent_for_network(fasync::Time::now() - RECENT_FAILURE_WINDOW) |
| .get(0) |
| .expect("failed to get recent failure") |
| .time; |
| let recent_failures = vec![ConnectFailure { |
| bssid: bssid_2, |
| time: failure_time, |
| reason: FailureReason::CredentialRejected, |
| }]; |
| let hasher = WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()); |
| let expected_internal_data_1 = InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: recent_failures.clone(), |
| past_connections: PastConnectionsByBssid::new(), |
| }; |
| let expected_result = vec![ |
| InternalBss { |
| security_type_detailed: test_security_1, |
| saved_network_info: expected_internal_data_1.clone(), |
| scanned_bss: &mock_scan_results[0].entries[0], |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }, |
| InternalBss { |
| security_type_detailed: test_security_1, |
| saved_network_info: expected_internal_data_1.clone(), |
| scanned_bss: &mock_scan_results[0].entries[1], |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }, |
| InternalBss { |
| security_type_detailed: test_security_1, |
| saved_network_info: expected_internal_data_1, |
| scanned_bss: &mock_scan_results[0].entries[2], |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }, |
| InternalBss { |
| security_type_detailed: test_security_2, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: false, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &mock_scan_results[1].entries[0], |
| multiple_bss_candidates: false, |
| hasher: hasher.clone(), |
| }, |
| ]; |
| |
| // validate the function works |
| let result = merge_saved_networks_and_scan_data( |
| &test_values.saved_network_manager, |
| &mock_scan_results, |
| &hasher, |
| ) |
| .await; |
| assert_eq!(result, expected_result); |
| } |
| |
| #[test_case(types::Bss { |
| rssi: -8, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }, |
| -8; "2.4GHz BSS score is RSSI")] |
| #[test_case(types::Bss { |
| rssi: -49, |
| channel: generate_channel(36), |
| ..generate_random_bss() |
| }, |
| -29; "5GHz score is (RSSI + mod), when above threshold")] |
| #[test_case(types::Bss { |
| rssi: -71, |
| channel: generate_channel(36), |
| ..generate_random_bss() |
| }, |
| -71; "5GHz score is RSSI, when below threshold")] |
| #[fuchsia::test(add_test_attr = false)] |
| fn scoring_test(bss: types::Bss, expected_score: i16) { |
| let _exec = |
| fasync::TestExecutor::new_with_fake_time().expect("failed to create an executor"); |
| let mut rng = rand::thread_rng(); |
| |
| let network_id = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("test").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let internal_bss = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: network_id, |
| credential: Credential::None, |
| has_ever_connected: rng.gen::<bool>(), |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| |
| assert_eq!(internal_bss.score(), expected_score) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_score_bss_prefers_less_short_connections() { |
| let bss_worse = |
| types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() }; |
| let bss_better = |
| types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() }; |
| let (_test_id, mut internal_data) = generate_random_saved_network(); |
| let short_uptime = zx::Duration::from_seconds(30); |
| let okay_uptime = zx::Duration::from_minutes(100); |
| // Record a short uptime for the worse network and a long enough uptime for the better one. |
| let short_uptime_data = past_connection_with_bssid_uptime(bss_worse.bssid, short_uptime); |
| let okay_uptime_data = past_connection_with_bssid_uptime(bss_better.bssid, okay_uptime); |
| internal_data.past_connections.add(bss_worse.bssid, short_uptime_data); |
| internal_data.past_connections.add(bss_better.bssid, okay_uptime_data); |
| let bss_worse = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data.clone(), |
| scanned_bss: &bss_worse, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| let bss_better = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data, |
| scanned_bss: &bss_better, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| // Check that the better BSS has a higher score than the worse BSS. |
| assert!(bss_better.score() > bss_worse.score()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_score_bss_prefers_less_failures() { |
| let bss_worse = |
| types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() }; |
| let bss_better = |
| types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() }; |
| let (_test_id, mut internal_data) = generate_random_saved_network(); |
| // Add many test failures for the worse BSS and one for the better BSS |
| let mut failures = vec![connect_failure_with_bssid(bss_worse.bssid); 12]; |
| failures.push(connect_failure_with_bssid(bss_better.bssid)); |
| internal_data.recent_failures = failures; |
| let bss_worse = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data.clone(), |
| scanned_bss: &bss_worse, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| let bss_better = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data, |
| scanned_bss: &bss_better, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| // Check that the better BSS has a higher score than the worse BSS. |
| assert!(bss_better.score() > bss_worse.score()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_score_bss_prefers_stronger_with_failures() { |
| // Test test that if one network has a few network failures but is 5 Ghz instead of 2.4, |
| // the 5 GHz network has a higher score. |
| let bss_worse = |
| types::Bss { rssi: -35, channel: generate_channel(3), ..generate_random_bss() }; |
| let bss_better = |
| types::Bss { rssi: -35, channel: generate_channel(36), ..generate_random_bss() }; |
| let (_test_id, mut internal_data) = generate_random_saved_network(); |
| // Set the failure list to have 0 failures for the worse BSS and 4 failures for the |
| // stronger BSS. |
| internal_data.recent_failures = vec![connect_failure_with_bssid(bss_better.bssid); 2]; |
| let bss_worse = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data.clone(), |
| scanned_bss: &bss_worse, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| let bss_better = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data, |
| scanned_bss: &bss_better, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| assert!(bss_better.score() > bss_worse.score()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_score_credentials_rejected_worse() { |
| // If two BSS are identical other than one failed to connect with wrong credentials and |
| // the other failed with a few connect failurs, the one with wrong credentials has a lower |
| // score. |
| let bss_worse = |
| types::Bss { rssi: -30, channel: generate_channel(44), ..generate_random_bss() }; |
| let bss_better = |
| types::Bss { rssi: -30, channel: generate_channel(44), ..generate_random_bss() }; |
| let (_test_id, mut internal_data) = generate_random_saved_network(); |
| // Add many test failures for the worse BSS and one for the better BSS |
| let mut failures = vec![connect_failure_with_bssid(bss_better.bssid); 4]; |
| failures.push(ConnectFailure { |
| bssid: bss_worse.bssid, |
| time: fasync::Time::now(), |
| reason: FailureReason::CredentialRejected, |
| }); |
| internal_data.recent_failures = failures; |
| |
| let bss_worse = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data.clone(), |
| scanned_bss: &bss_worse, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| let bss_better = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data, |
| scanned_bss: &bss_better, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| |
| assert!(bss_better.score() > bss_worse.score()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn score_many_penalties_do_not_cause_panic() { |
| let bss = types::Bss { rssi: -80, channel: generate_channel(1), ..generate_random_bss() }; |
| let (_test_id, mut internal_data) = generate_random_saved_network(); |
| // Add 10 general failures and 10 rejected credentials failures |
| internal_data.recent_failures = vec![connect_failure_with_bssid(bss.bssid); 10]; |
| for _ in 0..1200 { |
| internal_data.recent_failures.push(ConnectFailure { |
| bssid: bss.bssid, |
| time: fasync::Time::now(), |
| reason: FailureReason::CredentialRejected, |
| }); |
| } |
| let short_uptime = zx::Duration::from_seconds(30); |
| let data = past_connection_with_bssid_uptime(bss.bssid, short_uptime); |
| for _ in 0..10 { |
| internal_data.past_connections.add(bss.bssid, data); |
| } |
| let internal_bss = InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: internal_data.clone(), |
| scanned_bss: &bss, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }; |
| |
| assert_eq!(internal_bss.score(), i16::MIN); |
| } |
| |
| #[fuchsia::test] |
| fn select_best_connection_candidate_sorts_by_score() { |
| let _executor = fasync::TestExecutor::new(); |
| // generate Inspect nodes |
| let inspector = inspect::Inspector::new(); |
| let mut inspect_node = AutoPersist::new( |
| InspectBoundedListNode::new(inspector.root().create_child("test"), 10), |
| "sample-persistence-tag", |
| create_inspect_persistence_channel().0, |
| ); |
| // build networks list |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| let mut networks = vec![]; |
| |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -14, |
| channel: generate_channel(36), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_1, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_2 = types::Bss { |
| compatible: true, |
| rssi: -10, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_2, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_3 = types::Bss { |
| compatible: true, |
| rssi: -8, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_3, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| // there's a network on 5G, it should get a boost and be selected |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: bss_1.observation, |
| has_multiple_bss_candidates: true, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_1.channel, |
| bss_1.bssid |
| )) |
| ); |
| |
| // make the 5GHz network into a 2.4GHz network |
| let mut modified_network = networks[0].clone(); |
| let modified_bss = |
| types::Bss { channel: generate_channel(6), ..modified_network.scanned_bss.clone() }; |
| modified_network.scanned_bss = &modified_bss; |
| networks[0] = modified_network; |
| |
| // all networks are 2.4GHz, strongest RSSI network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: networks[2].scanned_bss.bss_description.clone(), |
| observation: networks[2].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| }), |
| }, |
| networks[2].scanned_bss.channel, |
| networks[2].scanned_bss.bssid |
| )) |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn select_best_connection_candidate_sorts_by_failure_count() { |
| let _executor = fasync::TestExecutor::new(); |
| // generate Inspect nodes |
| let inspector = inspect::Inspector::new(); |
| let mut inspect_node = AutoPersist::new( |
| InspectBoundedListNode::new(inspector.root().create_child("test"), 10), |
| "sample-persistence-tag", |
| create_inspect_persistence_channel().0, |
| ); |
| // build networks list |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| let mut networks = vec![]; |
| |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -34, |
| channel: generate_channel(3), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_1, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_2 = types::Bss { |
| compatible: true, |
| rssi: -50, |
| channel: generate_channel(3), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_2, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| // stronger network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: networks[0].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: |
| types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_1.channel, |
| bss_1.bssid |
| )) |
| ); |
| |
| // mark the stronger network as having some failures |
| let num_failures = 4; |
| networks[0].saved_network_info.recent_failures = |
| vec![connect_failure_with_bssid(bss_1.bssid); num_failures]; |
| networks[1].saved_network_info.recent_failures = |
| vec![connect_failure_with_bssid(bss_1.bssid); num_failures]; |
| |
| // weaker network (with no failures) returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_2.bss_description.clone(), |
| observation: networks[1].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: |
| types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_2.channel, |
| bss_2.bssid |
| )) |
| ); |
| |
| // give them both the same number of failures |
| networks[1].saved_network_info.recent_failures = |
| vec![connect_failure_with_bssid(bss_2.bssid.clone()); num_failures]; |
| |
| // stronger network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: networks[0].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: |
| types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_1.channel, |
| bss_1.bssid |
| )) |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn select_best_connection_candidate_incompatible() { |
| let _executor = fasync::TestExecutor::new(); |
| // generate Inspect nodes |
| let inspector = inspect::Inspector::new(); |
| let mut inspect_node = AutoPersist::new( |
| InspectBoundedListNode::new(inspector.root().create_child("test"), 10), |
| "sample-persistence-tag", |
| create_inspect_persistence_channel().0, |
| ); |
| // build networks list |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| let mut networks = vec![]; |
| |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -14, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_1, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_2 = types::Bss { |
| compatible: false, |
| rssi: -10, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_2, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_3 = types::Bss { |
| compatible: true, |
| rssi: -12, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa1, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_3, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| // stronger network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_3.bss_description.clone(), |
| observation: networks[2].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa1, |
| }), |
| }, |
| bss_3.channel, |
| bss_3.bssid |
| )) |
| ); |
| |
| // mark the stronger network as incompatible |
| let mut modified_network = networks[2].clone(); |
| let modified_bss = types::Bss { compatible: false, ..modified_network.scanned_bss.clone() }; |
| modified_network.scanned_bss = &modified_bss; |
| networks[2] = modified_network; |
| |
| // other network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: networks[0].scanned_bss.bss_description.clone(), |
| observation: networks[0].scanned_bss.observation, |
| has_multiple_bss_candidates: true, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| }, |
| networks[0].scanned_bss.channel, |
| networks[0].scanned_bss.bssid |
| )) |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn select_best_connection_candidate_ignore_list() { |
| let _executor = fasync::TestExecutor::new(); |
| // generate Inspect nodes |
| let inspector = inspect::Inspector::new(); |
| let mut inspect_node = AutoPersist::new( |
| InspectBoundedListNode::new(inspector.root().create_child("test"), 10), |
| "sample-persistence-tag", |
| create_inspect_persistence_channel().0, |
| ); |
| // build networks list |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| let mut networks = vec![]; |
| |
| let bss_1 = types::Bss { compatible: true, rssi: -100, ..generate_random_bss() }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_1, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_2 = types::Bss { compatible: true, rssi: -12, ..generate_random_bss() }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_2, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| // stronger network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_2.bss_description.clone(), |
| observation: networks[1].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_2.channel, |
| bss_2.bssid |
| )) |
| ); |
| |
| // ignore the stronger network, other network returned |
| assert_eq!( |
| select_best_connection_candidate( |
| networks.clone(), |
| &vec![test_id_2.clone()], |
| &mut inspect_node |
| ), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: networks[0].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_1.channel, |
| bss_1.bssid |
| )) |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn select_best_connection_candidate_logs_to_inspect() { |
| let _executor = fasync::TestExecutor::new(); |
| // generate Inspect nodes |
| let inspector = inspect::Inspector::new(); |
| let mut inspect_node = AutoPersist::new( |
| InspectBoundedListNode::new(inspector.root().create_child("test"), 10), |
| "sample-persistence-tag", |
| create_inspect_persistence_channel().0, |
| ); |
| // build networks list |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| |
| let mut networks = vec![]; |
| |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -14, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_1, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_2 = types::Bss { |
| compatible: false, |
| rssi: -10, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: credential_1.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_2, |
| multiple_bss_candidates: true, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| let bss_3 = types::Bss { |
| compatible: true, |
| rssi: -12, |
| channel: generate_channel(1), |
| ..generate_random_bss() |
| }; |
| networks.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: credential_2.clone(), |
| has_ever_connected: true, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &bss_3, |
| multiple_bss_candidates: false, |
| hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()), |
| }); |
| |
| // stronger network returned |
| assert_eq!( |
| select_best_connection_candidate(networks.clone(), &vec![], &mut inspect_node), |
| Some(( |
| types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_3.bss_description.clone(), |
| observation: networks[2].scanned_bss.observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| }), |
| }, |
| bss_3.channel, |
| bss_3.bssid |
| )) |
| ); |
| |
| let fidl_channel = fidl_common::WlanChannel::from(networks[2].scanned_bss.channel); |
| assert_data_tree!(inspector, root: { |
| test: { |
| "0": { |
| "@time": inspect::testing::AnyProperty, |
| "candidates": { |
| "0": contains { |
| score: inspect::testing::AnyProperty, |
| }, |
| "1": contains { |
| score: inspect::testing::AnyProperty, |
| }, |
| "2": contains { |
| score: inspect::testing::AnyProperty, |
| }, |
| }, |
| "selected": { |
| ssid_hash: networks[2].hasher.hash_ssid(&networks[2].saved_network_info.network_id.ssid), |
| bssid_hash: networks[2].hasher.hash_mac_addr(&networks[2].scanned_bss.bssid.0), |
| rssi: i64::from(networks[2].scanned_bss.rssi), |
| score: i64::from(networks[2].score()), |
| security_type_saved: networks[2].saved_security_type_to_string(), |
| security_type_scanned: format!("{}", wlan_common::bss::Protection::from(networks[2].security_type_detailed)), |
| channel: { |
| cbw: inspect::testing::AnyProperty, |
| primary: u64::from(fidl_channel.primary), |
| secondary80: u64::from(fidl_channel.secondary80), |
| }, |
| compatible: networks[2].scanned_bss.compatible, |
| recent_failure_count: networks[2].recent_failure_count(), |
| saved_network_has_ever_connected: networks[2].saved_network_info.has_ever_connected, |
| }, |
| } |
| }, |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn perform_scan_cache_is_fresh() { |
| let mut test_values = test_setup().await; |
| let network_selector = test_values.network_selector; |
| |
| // Set the scan result cache to be fresher than STALE_SCAN_AGE |
| let mut scan_result_guard = network_selector.scan_result_cache.lock().await; |
| let last_scan_age = zx::Duration::from_millis(1); |
| assert!(last_scan_age < STALE_SCAN_AGE); |
| scan_result_guard.updated_at = zx::Time::get_monotonic() - last_scan_age; |
| drop(scan_result_guard); |
| |
| network_selector.perform_scan(test_values.iface_manager).await; |
| |
| // Metric logged for scan age |
| let metric = test_values.cobalt_events.try_next().unwrap().unwrap(); |
| let expected_metric = |
| CobaltEvent::builder(LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_METRIC_ID).as_elapsed_time(0); |
| // We need to individually check each field, since the elapsed time is non-deterministic |
| assert_eq!(metric.metric_id, expected_metric.metric_id); |
| assert_eq!(metric.event_codes, expected_metric.event_codes); |
| assert_eq!(metric.component, expected_metric.component); |
| assert_variant!( |
| metric.payload, fidl_fuchsia_cobalt::EventPayload::ElapsedMicros(elapsed_micros) => { |
| let elapsed_time = zx::Duration::from_micros(elapsed_micros.try_into().unwrap()); |
| assert!(elapsed_time < STALE_SCAN_AGE); |
| } |
| ); |
| |
| // No scan performed |
| assert!(test_values.sme_stream.next().await.is_none()); |
| } |
| |
| #[fuchsia::test] |
| fn perform_scan_cache_is_stale() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| let test_start_time = zx::Time::get_monotonic(); |
| |
| // Set the scan result cache to be older than STALE_SCAN_AGE |
| let mut scan_result_guard = |
| exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| scan_result_guard.updated_at = |
| zx::Time::get_monotonic() - (STALE_SCAN_AGE + zx::Duration::from_seconds(1)); |
| drop(scan_result_guard); |
| |
| // Kick off scan |
| let scan_fut = network_selector.perform_scan(test_values.iface_manager); |
| pin_mut!(scan_fut); |
| assert_variant!(exec.run_until_stalled(&mut scan_fut), Poll::Pending); |
| |
| // Metric logged for scan age |
| let metric = test_values.cobalt_events.try_next().unwrap().unwrap(); |
| let expected_metric = |
| CobaltEvent::builder(LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_METRIC_ID).as_elapsed_time(0); |
| assert_eq!(metric.metric_id, expected_metric.metric_id); |
| assert_eq!(metric.event_codes, expected_metric.event_codes); |
| assert_eq!(metric.component, expected_metric.component); |
| assert_variant!( |
| metric.payload, fidl_fuchsia_cobalt::EventPayload::ElapsedMicros(elapsed_micros) => { |
| let elapsed_time = zx::Duration::from_micros(elapsed_micros.try_into().unwrap()); |
| assert!(elapsed_time > STALE_SCAN_AGE); |
| } |
| ); |
| |
| // Check that a scan request was sent to the sme and send back results |
| let expected_scan_request = fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}); |
| validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| vec![], |
| ); |
| // Process scan |
| exec.run_singlethreaded(&mut scan_fut); |
| |
| // Check scan results were updated |
| let scan_result_guard = exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| assert!(scan_result_guard.updated_at > test_start_time); |
| assert!(scan_result_guard.updated_at < zx::Time::get_monotonic()); |
| drop(scan_result_guard); |
| } |
| |
| #[fuchsia::test] |
| fn perform_scan_error_doesnt_use_stale_results() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| |
| // Set the scan result cache to be older than STALE_SCAN_AGE |
| let mut scan_result_guard = |
| exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| scan_result_guard.updated_at = |
| zx::Time::get_monotonic() - (STALE_SCAN_AGE + zx::Duration::from_seconds(1)); |
| // Add some stale/old results to the cache |
| scan_result_guard.results = vec![types::ScanResult { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2Wpa3Personal, |
| entries: vec![], |
| compatibility: types::Compatibility::Supported, |
| }]; |
| drop(scan_result_guard); |
| |
| // Kick off scan |
| let scan_fut = network_selector.perform_scan(test_values.iface_manager); |
| pin_mut!(scan_fut); |
| assert_variant!(exec.run_until_stalled(&mut scan_fut), Poll::Pending); |
| |
| // Check that a scan request was sent to the sme and send back an error |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.sme_stream.next()), |
| Poll::Ready(Some(Ok(fidl_sme::ClientSmeRequest::Scan { |
| txn, .. |
| }))) => { |
| // Send failed scan response. |
| let (_stream, ctrl) = txn |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl.send_on_error(&mut fidl_sme::ScanError { |
| code: fidl_sme::ScanErrorCode::InternalError, |
| message: "Failed to scan".to_string() |
| }) |
| .expect("failed to send scan error"); |
| } |
| ); |
| // Process scan |
| exec.run_singlethreaded(&mut scan_fut); |
| |
| // Check there are no scan results presents for use |
| let scan_result_guard = exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| assert_eq!(scan_result_guard.results.len(), 0); |
| drop(scan_result_guard); |
| } |
| |
| #[fuchsia::test] |
| fn augment_bss_with_active_scan_doesnt_run_on_actively_found_networks() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let test_values = exec.run_singlethreaded(test_setup()); |
| |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -14, |
| channel: generate_channel(36), |
| ..generate_random_bss() |
| }; |
| let connect_req = types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: types::ScanObservation::Active, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| }; |
| |
| let fut = augment_bss_with_active_scan( |
| connect_req.clone(), |
| bss_1.channel, |
| bss_1.bssid, |
| test_values.iface_manager.clone(), |
| ); |
| pin_mut!(fut); |
| |
| // The connect_req comes out the other side with no change |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(req) => { |
| assert_eq!(req, connect_req)} |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn augment_bss_with_active_scan_runs_on_passively_found_networks() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let bss_1 = types::Bss { |
| compatible: true, |
| rssi: -14, |
| channel: generate_channel(36), |
| ..generate_random_bss() |
| }; |
| let connect_req = types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_1.bss_description.clone(), |
| observation: types::ScanObservation::Passive, |
| has_multiple_bss_candidates: true, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| }; |
| |
| let fut = augment_bss_with_active_scan( |
| connect_req.clone(), |
| bss_1.channel, |
| bss_1.bssid, |
| test_values.iface_manager.clone(), |
| ); |
| pin_mut!(fut); |
| |
| // Progress the future until a scan request is sent |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check that a scan request was sent to the sme and send back results |
| let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest { |
| ssids: vec![test_id_1.ssid.to_vec()], |
| channels: vec![36], |
| }); |
| let new_bss_desc = random_fidl_bss_description!( |
| Wpa3Enterprise, |
| bssid: bss_1.bssid.0, |
| ssid: test_id_1.ssid.clone(), |
| rssi_dbm: 0, |
| snr_db: 0, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| |
| let mock_scan_results = vec![ |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: random_fidl_bss_description!( |
| Wpa3Enterprise, |
| bssid: [0, 0, 0, 0, 0, 0], // Not the same BSSID |
| ssid: test_id_1.ssid.clone(), |
| rssi_dbm: 10, |
| snr_db: 10, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ), |
| }, |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: new_bss_desc.clone(), |
| }, |
| ]; |
| validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_scan_results, |
| ); |
| |
| // The connect_req comes out the other side with the new bss_description |
| assert_eq!( |
| exec.run_singlethreaded(fut), |
| types::ConnectionCandidate { |
| scanned: Some(types::ScannedCandidate { |
| bss_description: new_bss_desc, |
| // The network was observed in a passive scan prior to the directed active |
| // scan, so this should remain `Passive`. |
| observation: types::ScanObservation::Passive, |
| // Multiple BSSes were observed prior to the directed active scan, so this |
| // field should remain `true`. |
| has_multiple_bss_candidates: true, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| ..connect_req |
| } |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn find_best_connection_candidate_end_to_end() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| // create some identifiers |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| let bss_desc1 = random_fidl_bss_description!( |
| Wpa3, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: test_id_1.ssid.clone(), |
| rssi_dbm: 10, |
| snr_db: 10, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| let bss_desc1_active = random_fidl_bss_description!( |
| Wpa3, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: test_id_1.ssid.clone(), |
| rssi_dbm: 10, |
| snr_db: 10, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("bar").unwrap(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec()); |
| let bss_desc2 = random_fidl_bss_description!( |
| Wpa1, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: test_id_2.ssid.clone(), |
| rssi_dbm: 0, |
| snr_db: 0, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| let bss_desc2_active = random_fidl_bss_description!( |
| Wpa1, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: test_id_2.ssid.clone(), |
| rssi_dbm: 10, |
| snr_db: 10, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| |
| // insert some new saved networks |
| assert!(exec |
| .run_singlethreaded( |
| test_values |
| .saved_network_manager |
| .store(test_id_1.clone().into(), credential_1.clone()), |
| ) |
| .unwrap() |
| .is_none()); |
| assert!(exec |
| .run_singlethreaded( |
| test_values |
| .saved_network_manager |
| .store(test_id_2.clone().into(), credential_2.clone()), |
| ) |
| .unwrap() |
| .is_none()); |
| |
| // Mark them as having connected. Make connection passive so that no active scans are made. |
| exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result( |
| test_id_1.clone().into(), |
| &credential_1.clone(), |
| types::Bssid([0, 0, 0, 0, 0, 0]), |
| fake_successful_connect_result(), |
| Some(fidl_common::ScanType::Passive), |
| )); |
| exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result( |
| test_id_2.clone().into(), |
| &credential_2.clone(), |
| types::Bssid([0, 0, 0, 0, 0, 0]), |
| fake_successful_connect_result(), |
| Some(fidl_common::ScanType::Passive), |
| )); |
| |
| // Kick off network selection |
| let ignore_list = vec![]; |
| let network_selection_fut = network_selector |
| .find_best_connection_candidate(test_values.iface_manager.clone(), &ignore_list); |
| pin_mut!(network_selection_fut); |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // Check that a scan request was sent to the sme and send back results |
| let expected_scan_request = fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}); |
| let mock_scan_results = vec![ |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: bss_desc1.clone(), |
| }, |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: bss_desc2.clone(), |
| }, |
| ]; |
| validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_scan_results.clone(), |
| ); |
| |
| // Process scan results |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // An additional directed active scan should be made for the selected network |
| let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest { |
| ssids: vec![test_id_1.ssid.to_vec()], |
| channels: vec![1], |
| }); |
| let mock_active_scan_results = vec![fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: bss_desc1_active.clone(), |
| }]; |
| poll_for_and_validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut network_selection_fut, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_active_scan_results, |
| ); |
| |
| // Check that we pick a network |
| let results = exec.run_singlethreaded(&mut network_selection_fut); |
| assert_eq!( |
| results, |
| Some(types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_desc1_active.clone(), |
| observation: types::ScanObservation::Passive, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| }) |
| ); |
| |
| // Set the scan result cache's age so it is guaranteed to be old enough to trigger another |
| // passive scan. Without this manual adjustment, the test timing is such that sometimes the |
| // cache is fresh enough to use (therefore no new passive scan is performed). |
| let mut scan_result_guard = |
| exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| scan_result_guard.updated_at = |
| zx::Time::get_monotonic() - (STALE_SCAN_AGE + zx::Duration::from_millis(1)); |
| drop(scan_result_guard); |
| |
| // Ignore that network, check that we pick the other one |
| let ignore_list = vec![test_id_1.clone()]; |
| let network_selection_fut = network_selector |
| .find_best_connection_candidate(test_values.iface_manager.clone(), &ignore_list); |
| pin_mut!(network_selection_fut); |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // Check that a scan request was sent to the sme and send back results |
| let expected_scan_request = fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}); |
| validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_scan_results, |
| ); |
| |
| // Process scan results |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // An additional directed active scan should be made for the selected network |
| let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest { |
| ssids: vec![test_id_2.ssid.to_vec()], |
| channels: vec![1], |
| }); |
| let mock_active_scan_results = vec![fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: bss_desc2_active.clone(), |
| }]; |
| poll_for_and_validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut network_selection_fut, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_active_scan_results, |
| ); |
| |
| let results = exec.run_singlethreaded(&mut network_selection_fut); |
| assert_eq!( |
| results, |
| Some(types::ConnectionCandidate { |
| network: test_id_2.clone(), |
| credential: credential_2.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_desc2_active.clone(), |
| observation: types::ScanObservation::Passive, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa1, |
| }), |
| }) |
| ); |
| |
| // Check the network selections were logged |
| assert_data_tree!(test_values.inspector, root: { |
| net_select_test: { |
| network_selection: { |
| "0": { |
| "@time": inspect::testing::AnyProperty, |
| "candidates": { |
| "0": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| "1": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| }, |
| "selected": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| }, |
| "1": { |
| "@time": inspect::testing::AnyProperty, |
| "candidates": { |
| "0": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| "1": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| }, |
| "selected": contains { |
| bssid_hash: inspect::testing::AnyProperty, |
| score: inspect::testing::AnyProperty, |
| }, |
| } |
| } |
| }, |
| }); |
| |
| // Verify that NetworkSelectionDecision telemetry event is sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::NetworkSelectionDecision { |
| network_selection_type: telemetry::NetworkSelectionType::Undirected, |
| num_candidates: Ok(2), |
| selected_any: true, |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn find_best_connection_candidate_wpa_wpa2() { |
| // Check that if we see a WPA2 network and have WPA and WPA3 credentials saved for it, we |
| // could choose the WPA credential but not the WPA3 credential. In other words we can |
| // upgrade saved networks to higher security but not downgrade. |
| let mut exec = fasync::TestExecutor::new().expect("failed to create executor"); |
| let test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| |
| // Save networks with WPA and WPA3 security, same SSIDs, and different passwords. |
| let ssid = types::Ssid::try_from("foo").unwrap(); |
| let wpa_network_id = types::NetworkIdentifier { |
| ssid: ssid.clone(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| let credential = Credential::Password("foo_password".as_bytes().to_vec()); |
| assert!(exec |
| .run_singlethreaded( |
| test_values |
| .saved_network_manager |
| .store(wpa_network_id.clone().into(), credential.clone()), |
| ) |
| .expect("Failed to save network") |
| .is_none()); |
| let wpa3_network_id = types::NetworkIdentifier { |
| ssid: ssid.clone(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let wpa3_credential = Credential::Password("wpa3_only_password".as_bytes().to_vec()); |
| assert!(exec |
| .run_singlethreaded( |
| test_values |
| .saved_network_manager |
| .store(wpa3_network_id.clone().into(), wpa3_credential.clone()), |
| ) |
| .expect("Failed to save network") |
| .is_none()); |
| |
| // Record passive connects so that the test will not active scan. |
| exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result( |
| wpa_network_id.clone().into(), |
| &credential, |
| types::Bssid([0, 0, 0, 0, 0, 0]), |
| fake_successful_connect_result(), |
| Some(fidl_common::ScanType::Passive), |
| )); |
| exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result( |
| wpa3_network_id.clone().into(), |
| &wpa3_credential, |
| types::Bssid([0, 0, 0, 0, 0, 0]), |
| fake_successful_connect_result(), |
| Some(fidl_common::ScanType::Passive), |
| )); |
| |
| let wpa2_scan_result = types::ScanResult { |
| ssid: ssid, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal, |
| entries: vec![types::Bss { |
| compatible: true, |
| observation: types::ScanObservation::Active, // mark this as active, to avoid an additional scan |
| ..generate_random_bss() |
| }], |
| compatibility: types::Compatibility::Supported, |
| }; |
| let mut updater = network_selector.generate_scan_result_updater(); |
| exec.run_singlethreaded(updater.update_scan_results(&vec![wpa2_scan_result.clone()])); |
| |
| // Set the scan cache's "updated_at" field to the future so that a scan won't be triggered. |
| { |
| let mut cache_guard = |
| exec.run_singlethreaded(network_selector.scan_result_cache.lock()); |
| cache_guard.updated_at = zx::Time::INFINITE; |
| } |
| |
| // Check that we choose the config saved as WPA |
| let ignore_list = Vec::new(); |
| let network_selection_fut = network_selector |
| .find_best_connection_candidate(test_values.iface_manager.clone(), &ignore_list); |
| pin_mut!(network_selection_fut); |
| assert_variant!( |
| exec.run_until_stalled(&mut network_selection_fut), |
| Poll::Ready(Some(connection_candidate)) => { |
| let expected_candidate = types::ConnectionCandidate { |
| // The network ID should match network config for recording connect results. |
| network: wpa_network_id.clone(), |
| credential, |
| scanned: Some(types::ScannedCandidate { |
| bss_description: wpa2_scan_result.entries[0].bss_description.clone(), |
| observation: wpa2_scan_result.entries[0].observation, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2Personal, |
| }), |
| }; |
| assert_eq!(connection_candidate, expected_candidate); |
| } |
| ); |
| // If the best network ID is ignored, there is no best connection candidate. |
| let ignore_list = vec![wpa_network_id]; |
| let network_selection_fut = network_selector |
| .find_best_connection_candidate(test_values.iface_manager.clone(), &ignore_list); |
| pin_mut!(network_selection_fut); |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Ready(None)); |
| } |
| |
| #[fuchsia::test] |
| fn find_connection_candidate_for_network_end_to_end() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| // create identifiers |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec()); |
| |
| // insert saved networks |
| assert!(exec |
| .run_singlethreaded( |
| test_values |
| .saved_network_manager |
| .store(test_id_1.clone().into(), credential_1.clone()), |
| ) |
| .unwrap() |
| .is_none()); |
| |
| // get the sme proxy |
| let mut iface_manager_inner = exec.run_singlethreaded(test_values.iface_manager.lock()); |
| let sme_proxy = |
| exec.run_singlethreaded(iface_manager_inner.get_sme_proxy_for_scan()).unwrap(); |
| drop(iface_manager_inner); |
| |
| // Kick off network selection |
| let network_selection_fut = |
| network_selector.find_connection_candidate_for_network(sme_proxy, test_id_1.clone()); |
| pin_mut!(network_selection_fut); |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // Check that a scan request was sent to the sme and send back results |
| let expected_scan_request = fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest { |
| ssids: vec![test_id_1.ssid.to_vec()], |
| channels: vec![], |
| }); |
| let bss_desc_1 = random_fidl_bss_description!( |
| // This network is WPA3, but should still match against the desired WPA2 network |
| Wpa3, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: test_id_1.ssid.clone(), |
| rssi_dbm: 10, |
| snr_db: 10, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ); |
| let mock_scan_results = vec![ |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: bss_desc_1.clone(), |
| }, |
| fidl_sme::ScanResult { |
| compatible: true, |
| timestamp_nanos: zx::Time::get_monotonic().into_nanos(), |
| bss_description: random_fidl_bss_description!( |
| Wpa1, |
| bssid: [0, 0, 0, 0, 0, 0], |
| ssid: types::Ssid::try_from("other ssid").unwrap(), |
| rssi_dbm: 0, |
| snr_db: 0, |
| channel: types::WlanChan::new(1, types::Cbw::Cbw20), |
| ), |
| }, |
| ]; |
| validate_sme_scan_request_and_send_results( |
| &mut exec, |
| &mut test_values.sme_stream, |
| &expected_scan_request, |
| mock_scan_results, |
| ); |
| |
| // Check that we pick a network |
| let results = exec.run_singlethreaded(&mut network_selection_fut); |
| assert_eq!( |
| results, |
| Some(types::ConnectionCandidate { |
| network: test_id_1.clone(), |
| credential: credential_1.clone(), |
| scanned: Some(types::ScannedCandidate { |
| bss_description: bss_desc_1, |
| // A passive scan is never performed in the tested code path, so the |
| // observation mode cannot be known and this field should be `Unknown`. |
| observation: types::ScanObservation::Unknown, |
| has_multiple_bss_candidates: false, |
| security_type_detailed: types::SecurityTypeDetailed::Wpa3Personal, |
| }), |
| }) |
| ); |
| |
| // Verify that NetworkSelectionDecision telemetry event is sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::NetworkSelectionDecision { |
| network_selection_type: telemetry::NetworkSelectionType::Directed, |
| num_candidates: Ok(1), |
| selected_any: true, |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn find_connection_candidate_for_network_end_to_end_with_failure() { |
| let mut exec = fasync::TestExecutor::new().expect("failed to create an executor"); |
| let mut test_values = exec.run_singlethreaded(test_setup()); |
| let network_selector = test_values.network_selector; |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| // create identifiers |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: types::Ssid::try_from("foo").unwrap(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| |
| // get the sme proxy |
| let mut iface_manager_inner = exec.run_singlethreaded(test_values.iface_manager.lock()); |
| let sme_proxy = |
| exec.run_singlethreaded(iface_manager_inner.get_sme_proxy_for_scan()).unwrap(); |
| drop(iface_manager_inner); |
| |
| // Kick off network selection |
| let network_selection_fut = |
| network_selector.find_connection_candidate_for_network(sme_proxy, test_id_1); |
| pin_mut!(network_selection_fut); |
| assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending); |
| |
| // Return an error on the scan |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.sme_stream.next()), |
| Poll::Ready(Some(Ok(fidl_sme::ClientSmeRequest::Scan { |
| txn, req: _, control_handle: _ |
| }))) => { |
| // Send failed scan response. |
| let (_stream, ctrl) = txn |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl.send_on_error(&mut fidl_sme::ScanError { |
| code: fidl_sme::ScanErrorCode::InternalError, |
| message: "Failed to scan".to_string() |
| }).expect("failed to send scan error"); |
| } |
| ); |
| |
| // Check that nothing is returned |
| let results = exec.run_singlethreaded(&mut network_selection_fut); |
| assert_eq!(results, None); |
| |
| // Verify that NetworkSelectionDecision telemetry event is sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::NetworkSelectionDecision { |
| network_selection_type: telemetry::NetworkSelectionType::Directed, |
| num_candidates: Err(()), |
| selected_any: false, |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| async fn recorded_metrics_on_scan() { |
| let (mut cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver(); |
| |
| // create some identifiers |
| let test_ssid_1 = types::Ssid::try_from("foo").unwrap(); |
| let test_id_1 = types::NetworkIdentifier { |
| ssid: test_ssid_1.clone(), |
| security_type: types::SecurityType::Wpa3, |
| }; |
| let test_ssid_2 = types::Ssid::try_from("bar").unwrap(); |
| let test_id_2 = types::NetworkIdentifier { |
| ssid: test_ssid_2.clone(), |
| security_type: types::SecurityType::Wpa, |
| }; |
| |
| let hasher = WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()); |
| let mut mock_scan_results = vec![]; |
| |
| let test_network_info_1 = InternalSavedNetworkData { |
| network_id: test_id_1.clone(), |
| credential: Credential::Password("foo_pass".as_bytes().to_vec()), |
| has_ever_connected: false, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }; |
| let test_bss_1 = |
| types::Bss { observation: types::ScanObservation::Passive, ..generate_random_bss() }; |
| mock_scan_results.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: test_network_info_1.clone(), |
| scanned_bss: &test_bss_1, |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }); |
| |
| let test_bss_2 = |
| types::Bss { observation: types::ScanObservation::Passive, ..generate_random_bss() }; |
| mock_scan_results.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: test_network_info_1.clone(), |
| scanned_bss: &test_bss_2, |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }); |
| |
| // mark one BSS as found in active scan |
| let test_bss_3 = |
| types::Bss { observation: types::ScanObservation::Active, ..generate_random_bss() }; |
| mock_scan_results.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: test_network_info_1.clone(), |
| scanned_bss: &test_bss_3, |
| multiple_bss_candidates: true, |
| hasher: hasher.clone(), |
| }); |
| |
| let test_bss_4 = |
| types::Bss { observation: types::ScanObservation::Passive, ..generate_random_bss() }; |
| mock_scan_results.push(InternalBss { |
| security_type_detailed: types::SecurityTypeDetailed::Wpa2PersonalTkipOnly, |
| saved_network_info: InternalSavedNetworkData { |
| network_id: test_id_2.clone(), |
| credential: Credential::Password("bar_pass".as_bytes().to_vec()), |
| has_ever_connected: false, |
| recent_failures: Vec::new(), |
| past_connections: PastConnectionsByBssid::new(), |
| }, |
| scanned_bss: &test_bss_4, |
| multiple_bss_candidates: false, |
| hasher: hasher.clone(), |
| }); |
| |
| record_metrics_on_scan(mock_scan_results, &mut cobalt_api); |
| |
| // The order of the first two cobalt events is not deterministic, so extract them into |
| // a vector that we're search through |
| let cobalt_events_vec = |
| vec![cobalt_events.try_next().unwrap(), cobalt_events.try_next().unwrap()]; |
| |
| // Three BSSs present for network 1 in scan results |
| assert!(cobalt_events_vec |
| .iter() |
| .find(|&event| event |
| == &Some( |
| CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID) |
| .with_event_code( |
| SavedNetworkInScanResultMetricDimensionBssCount::TwoToFour |
| .as_event_code() |
| ) |
| .as_event() |
| )) |
| .is_some()); |
| |
| // One BSS present for network 2 in scan results |
| assert!(cobalt_events_vec |
| .iter() |
| .find(|&event| event |
| == &Some( |
| CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID) |
| .with_event_code( |
| SavedNetworkInScanResultMetricDimensionBssCount::One.as_event_code() |
| ) |
| .as_event() |
| )) |
| .is_some()); |
| |
| // Total of two saved networks in the scan results |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SCAN_RESULTS_RECEIVED_METRIC_ID) |
| .with_event_code( |
| ScanResultsReceivedMetricDimensionSavedNetworksCount::TwoToFour |
| .as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| // One saved networks that was discovered via active scan |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_METRIC_ID) |
| .with_event_code(ActiveScanSsidsObserved::One.as_event_code()) |
| .as_event() |
| ) |
| ); |
| // No more metrics |
| assert!(cobalt_events.try_next().is_err()); |
| } |
| |
| #[fuchsia::test] |
| async fn recorded_metrics_on_scan_no_saved_networks() { |
| let (mut cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver(); |
| let mock_scan_results = vec![]; |
| |
| record_metrics_on_scan(mock_scan_results, &mut cobalt_api); |
| |
| // No saved networks in scan results |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SCAN_RESULTS_RECEIVED_METRIC_ID) |
| .with_event_code( |
| ScanResultsReceivedMetricDimensionSavedNetworksCount::Zero.as_event_code() |
| ) |
| .as_event() |
| ) |
| ); |
| // Also no saved networks that were discovered via active scan |
| assert_eq!( |
| cobalt_events.try_next().unwrap(), |
| Some( |
| CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_METRIC_ID) |
| .with_event_code(ActiveScanSsidsObserved::Zero.as_event_code()) |
| .as_event() |
| ) |
| ); |
| // No more metrics |
| assert!(cobalt_events.try_next().is_err()); |
| } |
| |
| fn connect_failure_with_bssid(bssid: types::Bssid) -> ConnectFailure { |
| ConnectFailure { |
| reason: FailureReason::GeneralFailure, |
| time: fasync::Time::INFINITE, |
| bssid, |
| } |
| } |
| |
| fn past_connection_with_bssid_uptime( |
| bssid: types::Bssid, |
| uptime: zx::Duration, |
| ) -> PastConnectionData { |
| PastConnectionData { |
| bssid, |
| connection_uptime: uptime, |
| disconnect_time: fasync::Time::INFINITE, // disconnect will always be considered recent |
| ..random_connection_data() |
| } |
| } |
| |
| fn fake_successful_connect_result() -> fidl_sme::ConnectResult { |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Success, |
| is_credential_rejected: false, |
| is_reconnect: false, |
| } |
| } |
| } |