blob: bfa8d39697e839ebfe680e60a6b152e29046c79f [file] [log] [blame]
// 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, ScanResultUpdate},
types,
},
config_management::{
self, ConnectFailure, Credential, Disconnect, 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_policy as fidl_policy,
fidl_fuchsia_wlan_sme as fidl_sme,
fuchsia_cobalt::CobaltSender,
fuchsia_inspect::Node as InspectNode,
fuchsia_inspect_contrib::{
inspect_insert, inspect_log,
log::{InspectList, WriteInspect},
nodes::BoundedListNode as InspectBoundedListNode,
},
fuchsia_zircon as zx,
futures::lock::Mutex,
log::{debug, error, info, trace},
rand::Rng,
std::{collections::HashMap, convert::TryInto as _, sync::Arc},
wlan_common::{channel::Channel, hasher::WlanHasher},
wlan_inspect::wrappers::InspectWlanChan,
wlan_metrics_registry::{
ActiveScanRequestedForNetworkSelectionMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested,
SavedNetworkInScanResultMetricDimensionBssCount,
SavedNetworkInScanResultWithActiveScanMetricDimensionActiveScanSsidsObserved as ActiveScanSsidsObserved,
ScanResultsReceivedMetricDimensionSavedNetworksCount,
ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_METRIC_ID,
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: i8 = -64;
/// The score boost for 5G networks that we are giving preference to.
const RSSI_5G_PREFERENCE_BOOST: i8 = 20;
/// The amount to decrease the score by for each failed connection attempt.
const SCORE_PENALTY_FOR_RECENT_FAILURE: i8 = 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: i8 = 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: i8 = 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<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>,
recent_disconnects: Vec<Disconnect>,
}
#[derive(Debug, Clone, PartialEq)]
struct InternalBss<'a> {
saved_network_info: InternalSavedNetworkData,
scanned_bss_info: &'a types::Bss,
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) -> i8 {
let mut score = self.scanned_bss_info.rssi;
let channel = Channel::from_fidl(self.scanned_bss_info.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);
}
// Count failures for rejected credentials higher since we probably won't succeed another
// try with the same credentials.
let failure_score: i8 = self
.saved_network_info
.recent_failures
.iter()
.filter(|failure| failure.bssid == self.scanned_bss_info.bssid)
.map(|failure| {
if failure.reason == FailureReason::CredentialRejected {
SCORE_PENALTY_FOR_RECENT_CREDENTIAL_REJECTED
} else {
SCORE_PENALTY_FOR_RECENT_FAILURE
}
})
.sum();
let short_connection_score: i8 = self
.recent_short_connections()
.try_into()
.unwrap_or_else(|_| i8::MAX)
.saturating_mul(SCORE_PENALTY_FOR_SHORT_CONNECTION);
return score.saturating_sub(failure_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_info.bssid)
.count()
.try_into()
.unwrap_or_else(|e| {
error!("{}", e);
u64::MAX
})
}
fn recent_short_connections(&self) -> usize {
self.saved_network_info
.recent_disconnects
.iter()
.filter(|d| d.bssid == self.scanned_bss_info.bssid && d.uptime < SHORT_CONNECT_DURATION)
.collect::<Vec<_>>()
.len()
}
fn saved_security_type_to_string(&self) -> String {
match self.saved_network_info.network_id.type_ {
fidl_policy::SecurityType::None => "open",
fidl_policy::SecurityType::Wep => "WEP",
fidl_policy::SecurityType::Wpa => "WPA",
fidl_policy::SecurityType::Wpa2 => "WPA2",
fidl_policy::SecurityType::Wpa3 => "WPA3",
}
.to_string()
}
fn to_string_without_pii(&self) -> String {
let channel = Channel::from_fidl(self.scanned_bss_info.channel);
let rssi = self.scanned_bss_info.rssi;
let recent_failure_count = self.recent_failure_count();
let recent_short_connection_count = self.recent_short_connections();
format!(
"{}({:4}), {}, {:>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_info.bssid),
rssi,
channel,
self.score(),
if !self.scanned_bss_info.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(&self, writer: &InspectNode, key: &str) {
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_info.bssid),
rssi: self.scanned_bss_info.rssi,
score: self.score(),
security_type_saved: self.saved_security_type_to_string(),
channel: InspectWlanChan(&self.scanned_bss_info.channel),
compatible: self.scanned_bss_info.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,
inspect_node: InspectNode,
telemetry_sender: TelemetrySender,
) -> Self {
let inspect_node_for_network_selection = InspectBoundedListNode::new(
inspect_node.create_child("network_selection"),
INSPECT_EVENT_LIMIT_FOR_NETWORK_SELECTIONS,
);
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: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
_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 mut cobalt_api_clone = self.cobalt_api.lock().await.clone();
let potentially_hidden_saved_networks =
config_management::select_subset_potentially_hidden_networks(
self.saved_network_manager.get_networks().await,
);
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 },
|_| {
let active_scan_request_count_metric =
match potentially_hidden_saved_networks.len() {
0 => ActiveScanSsidsRequested::Zero,
1 => ActiveScanSsidsRequested::One,
2..=4 => ActiveScanSsidsRequested::TwoToFour,
5..=10 => ActiveScanSsidsRequested::FiveToTen,
11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
101..=usize::MAX => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
_ => unreachable!(),
};
cobalt_api_clone.log_event(
ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_METRIC_ID,
active_scan_request_count_metric,
);
if potentially_hidden_saved_networks.is_empty() {
None
} else {
Some(potentially_hidden_saved_networks)
}
},
)
.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.telemetry_sender.send(TelemetryEvent::StartNetworkSelection {
network_selection_type: telemetry::NetworkSelectionType::Undirected,
});
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 {
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> {
self.telemetry_sender.send(TelemetryEvent::StartNetworkSelection {
network_selection_type: telemetry::NetworkSelectionType::Directed,
});
// 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(|(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).
types::ConnectionCandidate {
observed_in_passive_scan: None,
..candidate
}
});
(result, num_candidates)
}
};
self.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
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_info: bss,
multiple_bss_candidates,
saved_network_info: InternalSavedNetworkData {
network_id: types::NetworkIdentifier {
ssid: saved_config.ssid.clone(),
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
.failure_list
.get_recent(zx::Time::get_monotonic() - RECENT_FAILURE_WINDOW),
recent_disconnects: saved_config
.perf_stats
.disconnect_list
.get_recent(zx::Time::get_monotonic() - RECENT_DISCONNECT_WINDOW),
},
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 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_info.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(|bss_a, bss_b| bss_a.score().partial_cmp(&bss_b.score()).unwrap());
// Log the candidates into Inspect
inspect_log!(inspect_node, 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(),
observed_in_passive_scan: Some(bss.scanned_bss_info.observed_in_passive_scan),
bss: Some(bss.scanned_bss_info.bss_desc.clone()),
multiple_bss_candidates: Some(bss.multiple_bss_candidates),
},
bss.scanned_bss_info.channel,
bss.scanned_bss_info.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(
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, ()> {
// Make sure the scan is needed
match selected_network.observed_in_passive_scan {
Some(true) => info!("Performing directed active scan on selected network"),
Some(false) => {
debug!("Network already discovered via active scan.");
return Err(());
}
None => {
error!("Unexpected 'None' value for 'observed_in_passive_scan'.");
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_desc = 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_desc);
}
}
}
None
})
.ok_or_else(|| {
info!("BSS info will lack active scan augmentation, proceeding anyway.");
})?;
Ok(bss_desc)
}
match get_enhanced_bss_description(&selected_network, channel, bssid, iface_manager).await {
Ok(new_bss_desc) => {
types::ConnectionCandidate { bss: Some(new_bss_desc), ..selected_network }
}
Err(()) => selected_network,
}
}
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| bss.scanned_bss_info.observed_in_passive_scan == false) {
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::SavedNetworksManager,
util::testing::{
create_mock_cobalt_sender_and_receiver, generate_channel, generate_random_bss_desc,
generate_random_channel, poll_for_and_validate_sme_scan_request_and_send_results,
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_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::TryInto, sync::Arc},
test_case::test_case,
wlan_common::assert_variant,
};
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 (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
let network_selector = Arc::new(NetworkSelector::new(
saved_network_manager.clone(),
cobalt_api,
inspect_node,
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: fidl_fuchsia_wlan_policy::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: Vec<u8>, _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!()
}
}
#[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 = "foo".as_bytes().to_vec();
let test_security_1 = types::SecurityTypeDetailed::Wpa3Personal;
let test_id_1 = types::NetworkIdentifier {
ssid: test_ssid_1.clone(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_ssid_2 = "bar".as_bytes().to_vec();
let test_security_2 = types::SecurityTypeDetailed::Wpa1;
let test_id_2 =
types::NetworkIdentifier { ssid: test_ssid_2.clone(), 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,
fidl_sme::ConnectResultCode::Success,
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::ConnectResultCode::CredentialRejected,
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
.failure_list
.get_recent(zx::Time::get_monotonic() - 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(),
recent_disconnects: Vec::new(),
};
let expected_result = vec![
InternalBss {
saved_network_info: expected_internal_data_1.clone(),
scanned_bss_info: &mock_scan_results[0].entries[0],
multiple_bss_candidates: true,
hasher: hasher.clone(),
},
InternalBss {
saved_network_info: expected_internal_data_1.clone(),
scanned_bss_info: &mock_scan_results[0].entries[1],
multiple_bss_candidates: true,
hasher: hasher.clone(),
},
InternalBss {
saved_network_info: expected_internal_data_1,
scanned_bss_info: &mock_scan_results[0].entries[2],
multiple_bss_candidates: true,
hasher: hasher.clone(),
},
InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: false,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &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: i8) {
let mut rng = rand::thread_rng();
let network_id = types::NetworkIdentifier {
ssid: "test".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let internal_bss = InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: network_id,
credential: Credential::None,
has_ever_connected: rng.gen::<bool>(),
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss,
multiple_bss_candidates: false,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
assert_eq!(internal_bss.score(), expected_score)
}
#[fuchsia::test]
fn test_score_bss_prefers_less_short_connections() {
let bss_info_worse =
types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() };
let bss_info_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 mut disconnects =
vec![disconnect_with_bssid_uptime(bss_info_worse.bssid, short_uptime)];
disconnects.push(disconnect_with_bssid_uptime(bss_info_better.bssid, okay_uptime));
internal_data.recent_disconnects = disconnects;
let bss_worse = InternalBss {
saved_network_info: internal_data.clone(),
scanned_bss_info: &bss_info_worse,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
let bss_better = InternalBss {
saved_network_info: internal_data,
scanned_bss_info: &bss_info_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());
}
#[fuchsia::test]
fn test_score_bss_prefers_less_failures() {
let bss_info_worse =
types::Bss { rssi: -60, channel: generate_channel(3), ..generate_random_bss() };
let bss_info_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_info_worse.bssid); 12];
failures.push(connect_failure_with_bssid(bss_info_better.bssid));
internal_data.recent_failures = failures;
let bss_worse = InternalBss {
saved_network_info: internal_data.clone(),
scanned_bss_info: &bss_info_worse,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
let bss_better = InternalBss {
saved_network_info: internal_data,
scanned_bss_info: &bss_info_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());
}
#[fuchsia::test]
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_info_worse =
types::Bss { rssi: -35, channel: generate_channel(3), ..generate_random_bss() };
let bss_info_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_info_better.bssid); 2];
let bss_worse = InternalBss {
saved_network_info: internal_data.clone(),
scanned_bss_info: &bss_info_worse,
multiple_bss_candidates: false,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
let bss_better = InternalBss {
saved_network_info: internal_data,
scanned_bss_info: &bss_info_better,
multiple_bss_candidates: false,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
assert!(bss_better.score() > bss_worse.score());
}
#[fuchsia::test]
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_info_worse =
types::Bss { rssi: -30, channel: generate_channel(44), ..generate_random_bss() };
let bss_info_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_info_better.bssid); 4];
failures.push(ConnectFailure {
bssid: bss_info_worse.bssid,
time: zx::Time::get_monotonic(),
reason: FailureReason::CredentialRejected,
});
internal_data.recent_failures = failures;
let bss_worse = InternalBss {
saved_network_info: internal_data.clone(),
scanned_bss_info: &bss_info_worse,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
let bss_better = InternalBss {
saved_network_info: internal_data,
scanned_bss_info: &bss_info_better,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
};
assert!(bss_better.score() > bss_worse.score());
}
#[fuchsia::test]
fn select_best_connection_candidate_sorts_by_score() {
// generate Inspect nodes
let inspector = inspect::Inspector::new();
let mut inspect_node =
InspectBoundedListNode::new(inspector.root().create_child("test"), 10);
// build networks list
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let mut networks = vec![];
let bss_info1 = types::Bss {
compatible: true,
rssi: -14,
channel: generate_channel(36),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info1,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info2 = types::Bss {
compatible: true,
rssi: -10,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info2,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info3 = types::Bss {
compatible: true,
rssi: -8,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info3,
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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(bss_info1.observed_in_passive_scan),
multiple_bss_candidates: Some(true),
},
bss_info1.channel,
bss_info1.bssid
))
);
// make the 5GHz network into a 2.4GHz network
let mut modified_network = networks[0].clone();
let modified_bss_info = types::Bss {
channel: generate_channel(6),
..modified_network.scanned_bss_info.clone()
};
modified_network.scanned_bss_info = &modified_bss_info;
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(),
bss: Some(networks[2].scanned_bss_info.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[2].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
networks[2].scanned_bss_info.channel,
networks[2].scanned_bss_info.bssid
))
);
}
#[fuchsia::test]
fn select_best_connection_candidate_sorts_by_failure_count() {
// generate Inspect nodes
let inspector = inspect::Inspector::new();
let mut inspect_node =
InspectBoundedListNode::new(inspector.root().create_child("test"), 10);
// build networks list
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let mut networks = vec![];
let bss_info1 = types::Bss {
compatible: true,
rssi: -34,
channel: generate_channel(3),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info1,
multiple_bss_candidates: false,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info2 = types::Bss {
compatible: true,
rssi: -50,
channel: generate_channel(3),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info2,
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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[0].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.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_info1.bssid); num_failures];
networks[1].saved_network_info.recent_failures =
vec![connect_failure_with_bssid(bss_info1.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(),
bss: Some(bss_info2.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[1].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info2.channel,
bss_info2.bssid
))
);
// give them both the same number of failures
networks[1].saved_network_info.recent_failures =
vec![connect_failure_with_bssid(bss_info2.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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[0].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.bssid
))
);
}
#[fuchsia::test]
fn select_best_connection_candidate_incompatible() {
// generate Inspect nodes
let inspector = inspect::Inspector::new();
let mut inspect_node =
InspectBoundedListNode::new(inspector.root().create_child("test"), 10);
// build networks list
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let mut networks = vec![];
let bss_info1 = types::Bss {
compatible: true,
rssi: -14,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info1,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info2 = types::Bss {
compatible: false,
rssi: -10,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info2,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info3 = types::Bss {
compatible: true,
rssi: -12,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info3,
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(),
bss: Some(bss_info3.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[2].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info3.channel,
bss_info3.bssid
))
);
// mark the stronger network as incompatible
let mut modified_network = networks[2].clone();
let modified_bss_info =
types::Bss { compatible: false, ..modified_network.scanned_bss_info.clone() };
modified_network.scanned_bss_info = &modified_bss_info;
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(),
bss: Some(networks[0].scanned_bss_info.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[0].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(true),
},
networks[0].scanned_bss_info.channel,
networks[0].scanned_bss_info.bssid
))
);
}
#[fuchsia::test]
fn select_best_connection_candidate_ignore_list() {
// generate Inspect nodes
let inspector = inspect::Inspector::new();
let mut inspect_node =
InspectBoundedListNode::new(inspector.root().create_child("test"), 10);
// build networks list
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let mut networks = vec![];
let bss_info1 = types::Bss { compatible: true, rssi: -100, ..generate_random_bss() };
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info1,
multiple_bss_candidates: false,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info2 = types::Bss { compatible: true, rssi: -12, ..generate_random_bss() };
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info2,
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(),
bss: Some(bss_info2.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[1].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info2.channel,
bss_info2.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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[0].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.bssid
))
);
}
#[fuchsia::test]
fn select_best_connection_candidate_logs_to_inspect() {
// generate Inspect nodes
let inspector = inspect::Inspector::new();
let mut inspect_node =
InspectBoundedListNode::new(inspector.root().create_child("test"), 10);
// build networks list
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let mut networks = vec![];
let bss_info1 = types::Bss {
compatible: true,
rssi: -14,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info1,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info2 = types::Bss {
compatible: false,
rssi: -10,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_1.clone(),
credential: credential_1.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info2,
multiple_bss_candidates: true,
hasher: WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes()),
});
let bss_info3 = types::Bss {
compatible: true,
rssi: -12,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
saved_network_info: InternalSavedNetworkData {
network_id: test_id_2.clone(),
credential: credential_2.clone(),
has_ever_connected: true,
recent_failures: Vec::new(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &bss_info3,
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(),
bss: Some(bss_info3.bss_desc.clone()),
observed_in_passive_scan: Some(
networks[2].scanned_bss_info.observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
},
bss_info3.channel,
bss_info3.bssid
))
);
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_info.bssid),
rssi: i64::from(networks[2].scanned_bss_info.rssi),
score: i64::from(networks[2].score()),
security_type_saved: networks[2].saved_security_type_to_string(),
channel: {
cbw: inspect::testing::AnyProperty,
primary: u64::from(networks[2].scanned_bss_info.channel.primary),
secondary80: u64::from(networks[2].scanned_bss_info.channel.secondary80),
},
compatible: networks[2].scanned_bss_info.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: "foo".as_bytes().to_vec(),
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: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let bss_info1 = 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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(false), // was actively scanned
multiple_bss_candidates: Some(false),
};
let fut = augment_bss_with_active_scan(
connect_req.clone(),
bss_info1.channel,
bss_info1.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: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let bss_info1 = 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(),
bss: Some(bss_info1.bss_desc.clone()),
observed_in_passive_scan: Some(true), // was passively scanned
multiple_bss_candidates: Some(true),
};
let fut = augment_bss_with_active_scan(
connect_req.clone(),
bss_info1.channel,
bss_info1.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.clone()],
channels: vec![36],
});
let new_bss_desc = generate_random_bss_desc();
let mock_scan_results = vec![
fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0], // Not the same BSSID
ssid: test_id_1.ssid.clone(),
rssi_dbm: 10,
snr_db: 10,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa3Enterprise,
compatible: true,
bss_desc: generate_random_bss_desc(),
},
fidl_sme::BssInfo {
bssid: bss_info1.bssid.clone(),
ssid: test_id_1.ssid.clone(),
rssi_dbm: 0,
snr_db: 0,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa3Enterprise,
compatible: true,
bss_desc: 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_desc
assert_eq!(
exec.run_singlethreaded(fut),
types::ConnectionCandidate {
bss: Some(new_bss_desc),
// observed_in_passive_scan should still be true, since the network was found in a
// passive scan prior to the directed active scan augmentation.
observed_in_passive_scan: Some(true),
// multiple_bss_candidates should still be true, even if only one bss was found in
// the active scan, because we had found multiple BSSs prior to the active scan.
multiple_bss_candidates: Some(true),
..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: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let bss_desc1 = generate_random_bss_desc();
let bss_desc1_active = generate_random_bss_desc();
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
let bss_desc2 = generate_random_bss_desc();
let bss_desc2_active = generate_random_bss_desc();
// 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(),
[0, 0, 0, 0, 0, 0],
fidl_sme::ConnectResultCode::Success,
Some(fidl_common::ScanType::Passive),
));
exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result(
test_id_2.clone().into(),
&credential_2.clone(),
[0, 0, 0, 0, 0, 0],
fidl_sme::ConnectResultCode::Success,
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);
// Verify that StartNetworkSelection telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event, TelemetryEvent::StartNetworkSelection {
network_selection_type: telemetry::NetworkSelectionType::Undirected,
});
});
// 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::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: test_id_1.ssid.clone(),
rssi_dbm: 10,
snr_db: 10,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa3Enterprise,
compatible: true,
bss_desc: bss_desc1.clone(),
},
fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: test_id_2.ssid.clone(),
rssi_dbm: 0,
snr_db: 0,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa1,
compatible: true,
bss_desc: 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.clone()],
channels: vec![1],
});
let mock_active_scan_results = vec![fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: test_id_1.ssid.clone(),
rssi_dbm: 10,
snr_db: 10,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa3Enterprise,
compatible: true,
bss_desc: 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(),
bss: Some(bss_desc1_active.clone()),
observed_in_passive_scan: Some(true),
multiple_bss_candidates: Some(false)
})
);
// 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.clone()],
channels: vec![1],
});
let mock_active_scan_results = vec![fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: test_id_2.ssid.clone(),
rssi_dbm: 10,
snr_db: 10,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa1,
compatible: true,
bss_desc: 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(),
bss: Some(bss_desc2_active.clone()),
observed_in_passive_scan: Some(true),
multiple_bss_candidates: Some(false)
})
);
// 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_eq!(event, TelemetryEvent::NetworkSelectionDecision {
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 = "foo".as_bytes().to_vec();
let wpa_network_id =
types::NetworkIdentifier { ssid: ssid.clone(), 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(), 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,
[0, 0, 0, 0, 0, 0],
fidl_sme::ConnectResultCode::Success,
Some(fidl_common::ScanType::Passive),
));
exec.run_singlethreaded(test_values.saved_network_manager.record_connect_result(
wpa3_network_id.clone().into(),
&wpa3_credential,
[0, 0, 0, 0, 0, 0],
fidl_sme::ConnectResultCode::Success,
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,
observed_in_passive_scan: false, // 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,
bss: Some(wpa2_scan_result.entries[0].bss_desc.clone()),
observed_in_passive_scan: Some(
wpa2_scan_result.entries[0].observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
};
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: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let credential_1 = Credential::Password("foo_pass".as_bytes().to_vec());
let bss_desc_1 = generate_random_bss_desc();
// 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);
// Verify that StartNetworkSelection telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event, TelemetryEvent::StartNetworkSelection {
network_selection_type: telemetry::NetworkSelectionType::Directed,
});
});
// 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.clone()],
channels: vec![],
});
let mock_scan_results = vec![
fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: test_id_1.ssid.clone(),
rssi_dbm: 10,
snr_db: 10,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
// This network is WPA3, but should still match against the desired WPA2 network
protection: fidl_sme::Protection::Wpa3Personal,
compatible: true,
bss_desc: bss_desc_1.clone(),
},
fidl_sme::BssInfo {
bssid: [0, 0, 0, 0, 0, 0],
ssid: "other ssid".as_bytes().to_vec(),
rssi_dbm: 0,
snr_db: 0,
channel: fidl_common::WlanChannel {
primary: 1,
cbw: fidl_common::ChannelBandwidth::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa1,
compatible: true,
bss_desc: generate_random_bss_desc(),
},
];
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(),
bss: Some(bss_desc_1),
// This code path can't know if the network would have been observed in a passive
// scan, since it never performs a passive scan.
observed_in_passive_scan: None,
multiple_bss_candidates: Some(false),
})
);
// Verify that NetworkSelectionDecision telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event, TelemetryEvent::NetworkSelectionDecision {
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: "foo".as_bytes().to_vec(),
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);
// Verify that StartNetworkSelection telemetry event is sent
assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => {
assert_eq!(event, TelemetryEvent::StartNetworkSelection {
network_selection_type: telemetry::NetworkSelectionType::Directed,
});
});
// 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_eq!(event, TelemetryEvent::NetworkSelectionDecision {
num_candidates: Err(()),
selected_any: false,
});
});
}
fn generate_random_bss() -> types::Bss {
let mut rng = rand::thread_rng();
let bss = (0..6).map(|_| rng.gen::<u8>()).collect::<Vec<u8>>();
types::Bss {
bssid: bss.as_slice().try_into().unwrap(),
rssi: rng.gen_range(-100, 20),
channel: generate_random_channel(),
timestamp_nanos: 0,
snr_db: rng.gen_range(-20, 50),
observed_in_passive_scan: rng.gen::<bool>(),
compatible: rng.gen::<bool>(),
bss_desc: generate_random_bss_desc(),
}
}
fn generate_random_scan_result() -> types::ScanResult {
let mut rng = rand::thread_rng();
let ssid = format!("scan result rand {}", rng.gen::<i32>()).as_bytes().to_vec();
types::ScanResult {
ssid: ssid,
security_type_detailed: types::SecurityTypeDetailed::Wpa1,
entries: vec![generate_random_bss(), generate_random_bss()],
compatibility: match rng.gen_range(0, 2) {
0 => types::Compatibility::Supported,
1 => types::Compatibility::DisallowedNotSupported,
2 => types::Compatibility::DisallowedInsecure,
_ => panic!(),
},
}
}
fn generate_random_saved_network() -> (types::NetworkIdentifier, InternalSavedNetworkData) {
let mut rng = rand::thread_rng();
let net_id = types::NetworkIdentifier {
ssid: format!("saved network rand {}", rng.gen::<i32>()).as_bytes().to_vec(),
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(),
recent_disconnects: Vec::new(),
},
)
}
#[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 = "foo".as_bytes().to_vec();
let test_id_1 = types::NetworkIdentifier {
ssid: test_ssid_1.clone(),
type_: types::SecurityType::Wpa3,
};
let test_ssid_2 = "bar".as_bytes().to_vec();
let test_id_2 =
types::NetworkIdentifier { ssid: test_ssid_2.clone(), 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(),
recent_disconnects: Vec::new(),
};
let test_bss_1 = types::Bss { observed_in_passive_scan: true, ..generate_random_bss() };
mock_scan_results.push(InternalBss {
saved_network_info: test_network_info_1.clone(),
scanned_bss_info: &test_bss_1,
multiple_bss_candidates: true,
hasher: hasher.clone(),
});
let test_bss_2 = types::Bss { observed_in_passive_scan: true, ..generate_random_bss() };
mock_scan_results.push(InternalBss {
saved_network_info: test_network_info_1.clone(),
scanned_bss_info: &test_bss_2,
multiple_bss_candidates: true,
hasher: hasher.clone(),
});
// mark one BSS as found in active scan
let test_bss_3 = types::Bss { observed_in_passive_scan: false, ..generate_random_bss() };
mock_scan_results.push(InternalBss {
saved_network_info: test_network_info_1.clone(),
scanned_bss_info: &test_bss_3,
multiple_bss_candidates: true,
hasher: hasher.clone(),
});
let test_bss_4 = types::Bss { observed_in_passive_scan: true, ..generate_random_bss() };
mock_scan_results.push(InternalBss {
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(),
recent_disconnects: Vec::new(),
},
scanned_bss_info: &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: zx::Time::INFINITE, bssid }
}
fn disconnect_with_bssid_uptime(bssid: types::Bssid, uptime: zx::Duration) -> Disconnect {
Disconnect {
time: zx::Time::INFINITE, // disconnect never expires
bssid,
uptime,
}
}
}