blob: b38299af93e03a42a0a0262f961fdfdcb0dd08f7 [file] [log] [blame]
// Copyright 2020 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, Credential, SavedNetworksManager},
mode_management::iface_manager_api::IfaceManagerApi,
},
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_zircon as zx,
futures::lock::Mutex,
log::{debug, error, info, trace},
rand::Rng,
std::{cmp::Ordering, collections::HashMap, convert::TryInto, sync::Arc},
wlan_common::{channel::Channel, hasher::WlanHasher},
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
// 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;
pub struct NetworkSelector {
saved_network_manager: Arc<SavedNetworksManager>,
scan_result_cache: Arc<Mutex<ScanResultCache>>,
cobalt_api: Arc<Mutex<CobaltSender>>,
hasher: WlanHasher,
}
struct ScanResultCache {
updated_at: zx::Time,
results: Vec<types::ScanResult>,
}
#[derive(Debug, PartialEq, Clone)]
struct InternalSavedNetworkData {
credential: Credential,
has_ever_connected: bool,
recent_failure_count: u8,
}
#[derive(Debug, Clone, PartialEq)]
struct InternalBss<'a> {
network_id: types::NetworkIdentifier,
network_info: InternalSavedNetworkData,
bss_info: &'a types::Bss,
multiple_bss_candidates: bool,
}
impl InternalBss<'_> {
fn score(&self) -> i8 {
let rssi = self.bss_info.rssi;
let channel = Channel::from_fidl(self.bss_info.channel);
// If the network is 5G and has a strong enough RSSI, give it a bonus
if channel.is_5ghz() && rssi >= RSSI_CUTOFF_5G_PREFERENCE {
return rssi.saturating_add(RSSI_5G_PREFERENCE_BOOST);
}
return rssi;
}
fn print_without_pii(&self, hasher: &WlanHasher) {
let channel = Channel::from_fidl(self.bss_info.channel);
let rssi = self.bss_info.rssi;
let recent_failure_count = self.network_info.recent_failure_count;
let security_type = match self.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",
};
info!(
"{}({:4}), {}, {:>4}dBm, chan {:8}{}{}{}",
hasher.hash_ssid(&self.network_id.ssid),
security_type,
hasher.hash_mac_addr(&self.bss_info.bssid),
rssi,
channel,
if !self.bss_info.compatible { ", NOT compatible" } else { "" },
if recent_failure_count > 0 {
format!(", {} recent failures", recent_failure_count)
} else {
"".to_string()
},
if !self.network_info.has_ever_connected { ", never used yet" } else { "" },
)
}
}
impl NetworkSelector {
pub fn new(saved_network_manager: Arc<SavedNetworksManager>, cobalt_api: CobaltSender) -> Self {
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()),
}
}
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),
}
}
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());
}
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,
);
scan::perform_scan(
iface_manager,
self.saved_network_manager.clone(),
None,
self.generate_scan_result_updater(),
scan::LocationSensorUpdater {},
|_| {
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.perform_scan(iface_manager.clone()).await;
let saved_networks = load_saved_networks(Arc::clone(&self.saved_network_manager)).await;
let scan_result_guard = self.scan_result_cache.lock().await;
let networks =
merge_saved_networks_and_scan_data(saved_networks, &scan_result_guard.results).await;
match select_best_connection_candidate(networks, ignore_list, &self.hasher) {
Some((selected, channel, bssid)) => {
Some(augment_bss_with_active_scan(selected, channel, bssid, iface_manager).await)
}
None => None,
}
}
/// 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;
match scan_results {
Err(()) => None,
Ok(scan_results) => {
let saved_networks =
load_saved_networks(Arc::clone(&self.saved_network_manager)).await;
let networks =
merge_saved_networks_and_scan_data(saved_networks, &scan_results).await;
let ignore_list = vec![];
select_best_connection_candidate(networks, &ignore_list, &self.hasher).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 }
},
)
}
}
}
}
/// 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_networks: HashMap<types::NetworkIdentifier, InternalSavedNetworkData>,
scan_results: &'a Vec<types::ScanResult>,
) -> Vec<InternalBss<'a>> {
let mut merged_networks = vec![];
for scan_result in scan_results {
if let Some(saved_network_info) = saved_networks.get(&scan_result.id) {
let multiple_bss_candidates = scan_result.entries.len() > 1;
for bss in &scan_result.entries {
merged_networks.push(InternalBss {
bss_info: bss,
multiple_bss_candidates,
network_id: scan_result.id.clone(),
network_info: saved_network_info.clone(),
});
}
}
}
merged_networks
}
/// Insert all saved networks into a hashmap with this module's internal data representation
async fn load_saved_networks(
saved_network_manager: Arc<SavedNetworksManager>,
) -> HashMap<types::NetworkIdentifier, InternalSavedNetworkData> {
let mut networks: HashMap<types::NetworkIdentifier, InternalSavedNetworkData> = HashMap::new();
for saved_network in saved_network_manager.get_networks().await.into_iter() {
let recent_failure_count = saved_network
.perf_stats
.failure_list
.get_recent(zx::Time::get_monotonic() - RECENT_FAILURE_WINDOW)
.len()
.try_into()
.unwrap_or_else(|e| {
error!("Failed to convert failure count: {:?}", e);
u8::MAX
});
trace!(
"Adding saved network to hashmap{}",
if recent_failure_count > 0 { " with some failures" } else { "" }
);
// We allow networks saved as WPA to be also used as WPA2 or WPA2 to be used for WPA3
if let Some(security_type) = upgrade_security(&saved_network.security_type) {
networks.insert(
types::NetworkIdentifier { ssid: saved_network.ssid.clone(), type_: security_type },
InternalSavedNetworkData {
credential: saved_network.credential.clone(),
has_ever_connected: saved_network.has_ever_connected,
recent_failure_count: recent_failure_count,
},
);
};
networks.insert(
types::NetworkIdentifier {
ssid: saved_network.ssid,
type_: saved_network.security_type.into(),
},
InternalSavedNetworkData {
credential: saved_network.credential,
has_ever_connected: saved_network.has_ever_connected,
recent_failure_count: recent_failure_count,
},
);
}
networks
}
pub fn upgrade_security(security: &config_management::SecurityType) -> Option<types::SecurityType> {
match security {
config_management::SecurityType::Wpa => Some(types::SecurityType::Wpa2),
config_management::SecurityType::Wpa2 => Some(types::SecurityType::Wpa3),
_ => None,
}
}
pub struct NetworkSelectorScanUpdater {
scan_result_cache: Arc<Mutex<ScanResultCache>>,
saved_network_manager: Arc<SavedNetworksManager>,
cobalt_api: Arc<Mutex<CobaltSender>>,
}
#[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 saved_networks = load_saved_networks(Arc::clone(&self.saved_network_manager)).await;
let mut cobalt_api_guard = self.cobalt_api.lock().await;
let cobalt_api = &mut *cobalt_api_guard;
record_metrics_on_scan(scan_results, saved_networks, cobalt_api);
drop(cobalt_api_guard);
}
}
fn select_best_connection_candidate<'a>(
bss_list: Vec<InternalBss<'a>>,
ignore_list: &Vec<types::NetworkIdentifier>,
hasher: &WlanHasher,
) -> Option<(types::ConnectionCandidate, types::WlanChan, types::Bssid)> {
info!("Selecting from {} BSSs found for saved networks", bss_list.len());
bss_list
.into_iter()
.inspect(|bss| {
bss.print_without_pii(hasher);
})
.filter(|bss| {
// Filter out incompatible BSSs
if !bss.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.network_id) {
trace!("Network is ignored, filtering: {:?}", bss);
return false;
}
true
})
.max_by(|bss_a, bss_b| {
// If only one network has failures, prefer the other one
if bss_a.network_info.recent_failure_count > 0
&& bss_b.network_info.recent_failure_count == 0
{
return Ordering::Less;
}
if bss_a.network_info.recent_failure_count == 0
&& bss_b.network_info.recent_failure_count > 0
{
return Ordering::Greater;
}
// Both networks have failures, compare their scores
bss_a.score().partial_cmp(&bss_b.score()).unwrap()
})
.map(|bss| {
info!("Selected BSS:");
bss.print_without_pii(hasher);
(
types::ConnectionCandidate {
network: bss.network_id,
credential: bss.network_info.credential,
observed_in_passive_scan: Some(bss.bss_info.observed_in_passive_scan),
bss: bss.bss_info.bss_desc.clone(),
multiple_bss_candidates: Some(bss.multiple_bss_candidates),
},
bss.bss_info.channel,
bss.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<Option<Box<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 sme_proxy = iface_manager.lock().await.get_sme_proxy_for_scan().await.map_err(|e| {
info!("Failed to get an SME proxy for scan: {:?}", e);
})?;
// 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 network in the results
let mut network = directed_scan_result
.drain(..)
.find(|n| n.id == selected_network.network)
.ok_or_else(|| {
info!("BSS info will lack active scan augmentation, proceeding anyway.");
})?;
// Find the BSS in the network's list of BSSs
let bss = network.entries.drain(..).find(|bss| bss.bssid == bssid).ok_or_else(|| {
info!("BSS info will lack active scan augmentation, proceeding anyway.");
})?;
Ok(bss.bss_desc)
}
match get_enhanced_bss_description(&selected_network, channel, bssid, iface_manager).await {
Ok(new_bss_desc) => types::ConnectionCandidate { bss: new_bss_desc, ..selected_network },
Err(()) => selected_network,
}
}
fn record_metrics_on_scan(
scan_results: &Vec<types::ScanResult>,
saved_networks: HashMap<types::NetworkIdentifier, InternalSavedNetworkData>,
cobalt_api: &mut CobaltSender,
) {
let mut num_saved_networks_observed = 0;
let mut num_actively_scanned_networks = 0;
for scan_result in scan_results {
if let Some(_) = saved_networks.get(&scan_result.id) {
// This saved network was present in scan results.
num_saved_networks_observed += 1;
// Check if the network was found via active scan.
if scan_result.entries.iter().any(|bss| bss.observed_in_passive_scan == false) {
num_actively_scanned_networks += 1;
};
// Record how many BSSs are visible in the scan results for this saved network.
let num_bss = match scan_result.entries.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);
}
}
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,
util::{
logger::set_logger_for_test,
testing::{
create_mock_cobalt_sender_and_receiver, generate_channel,
generate_random_bss_desc, generate_random_channel,
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::{self as fasync, DurationExt},
fuchsia_cobalt::cobalt_event_builder::CobaltEventExt,
futures::{
channel::{mpsc, oneshot},
prelude::*,
task::Poll,
},
pin_utils::pin_mut,
std::sync::Arc,
test_case::test_case,
wlan_common::assert_variant,
};
struct TestValues {
network_selector: Arc<NetworkSelector>,
saved_network_manager: Arc<SavedNetworksManager>,
cobalt_events: mpsc::Receiver<CobaltEvent>,
iface_manager: Arc<Mutex<FakeIfaceManager>>,
sme_stream: fidl_sme::ClientSmeRequestStream,
}
async fn test_setup() -> TestValues {
set_logger_for_test();
// 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 network_selector =
Arc::new(NetworkSelector::new(Arc::clone(&saved_network_manager), cobalt_api));
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"),
}
}
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!()
}
}
#[fasync::run_singlethreaded(test)]
async fn saved_networks_are_loaded() {
let test_values = test_setup().await;
// check there are 0 saved networks to start with
let networks = load_saved_networks(Arc::clone(&test_values.saved_network_manager)).await;
assert_eq!(networks.len(), 0);
// 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 ssid_2 = "bar".as_bytes().to_vec();
let test_id_2 =
types::NetworkIdentifier { ssid: ssid_2.clone(), type_: types::SecurityType::Wpa };
let credential_2 = Credential::Password("bar_pass".as_bytes().to_vec());
// insert some new saved networks
test_values
.saved_network_manager
.store(test_id_1.clone().into(), credential_1.clone())
.await
.unwrap();
test_values
.saved_network_manager
.store(test_id_2.clone().into(), credential_2.clone())
.await
.unwrap();
// mark the first one as having connected
test_values
.saved_network_manager
.record_connect_result(
test_id_1.clone().into(),
&credential_1.clone(),
fidl_sme::ConnectResultCode::Success,
None,
)
.await;
// mark the second one as having a failure
test_values
.saved_network_manager
.record_connect_result(
test_id_2.clone().into(),
&credential_2.clone(),
fidl_sme::ConnectResultCode::CredentialRejected,
None,
)
.await;
// check these networks were loaded
let mut expected_hashmap = HashMap::new();
expected_hashmap.insert(
test_id_1,
InternalSavedNetworkData {
credential: credential_1,
has_ever_connected: true,
recent_failure_count: 0,
},
);
expected_hashmap.insert(
test_id_2,
InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: false,
recent_failure_count: 1,
},
);
// Networks saved as WPA can be used to auto connect to WPA2 networks
expected_hashmap.insert(
types::NetworkIdentifier { ssid: ssid_2, type_: types::SecurityType::Wpa2 },
InternalSavedNetworkData {
credential: credential_2,
has_ever_connected: false,
recent_failure_count: 1,
},
);
let networks = load_saved_networks(Arc::clone(&test_values.saved_network_manager)).await;
assert_eq!(networks, expected_hashmap);
}
#[fasync::run_singlethreaded(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);
// create some identifiers
let test_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
// provide some new scan results
let mock_scan_results = vec![
types::ScanResult {
id: test_id_1.clone(),
entries: vec![generate_random_bss(), generate_random_bss(), generate_random_bss()],
compatibility: types::Compatibility::Supported,
},
types::ScanResult {
id: test_id_2.clone(),
entries: vec![generate_random_bss()],
compatibility: types::Compatibility::DisallowedNotSupported,
},
];
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());
}
#[fasync::run_singlethreaded(test)]
async fn scan_results_merged_with_saved_networks() {
// 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 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());
// create the saved networks hashmap
let mut saved_networks = HashMap::new();
saved_networks.insert(
test_id_1.clone(),
InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
);
saved_networks.insert(
test_id_2.clone(),
InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: false,
recent_failure_count: 0,
},
);
// build some scan results
let mock_scan_results = vec![
types::ScanResult {
id: test_id_1.clone(),
entries: vec![generate_random_bss(), generate_random_bss(), generate_random_bss()],
compatibility: types::Compatibility::Supported,
},
types::ScanResult {
id: test_id_2.clone(),
entries: vec![generate_random_bss()],
compatibility: types::Compatibility::DisallowedNotSupported,
},
];
// build our expected result
let expected_result = vec![
InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &mock_scan_results[0].entries[0],
multiple_bss_candidates: true,
},
InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &mock_scan_results[0].entries[1],
multiple_bss_candidates: true,
},
InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &mock_scan_results[0].entries[2],
multiple_bss_candidates: true,
},
InternalBss {
network_id: test_id_2.clone(),
network_info: InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: false,
recent_failure_count: 0,
},
bss_info: &mock_scan_results[1].entries[0],
multiple_bss_candidates: false,
},
];
// validate the function works
let result = merge_saved_networks_and_scan_data(saved_networks, &mock_scan_results).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")]
fn scoring_test(bss: types::Bss, expected_score: i8) {
let mut rng = rand::thread_rng();
let internal_bss = InternalBss {
network_id: types::NetworkIdentifier {
ssid: "test".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
},
network_info: InternalSavedNetworkData {
credential: Credential::None,
has_ever_connected: rng.gen::<bool>(),
recent_failure_count: rng.gen_range(0, 20),
},
bss_info: &bss,
multiple_bss_candidates: false,
};
assert_eq!(internal_bss.score(), expected_score)
}
#[test]
fn select_best_connection_candidate_sorts_by_score() {
// 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 {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info1,
multiple_bss_candidates: true,
});
let bss_info2 = types::Bss {
compatible: true,
rssi: -10,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info2,
multiple_bss_candidates: true,
});
let bss_info3 = types::Bss {
compatible: true,
rssi: -8,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
network_id: test_id_2.clone(),
network_info: InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info3,
multiple_bss_candidates: false,
});
// there's a network on 5G, it should get a boost and be selected
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_1.clone(),
credential: credential_1.clone(),
bss: 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.bss_info.clone() };
modified_network.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![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_2.clone(),
credential: credential_2.clone(),
bss: networks[2].bss_info.bss_desc.clone(),
observed_in_passive_scan: Some(networks[2].bss_info.observed_in_passive_scan),
multiple_bss_candidates: Some(false),
},
networks[2].bss_info.channel,
networks[2].bss_info.bssid
))
);
}
#[test]
fn select_best_connection_candidate_sorts_by_failure_count() {
// 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, ..generate_random_bss() };
networks.push(InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info1,
multiple_bss_candidates: false,
});
let bss_info2 = types::Bss { compatible: true, rssi: -100, ..generate_random_bss() };
networks.push(InternalBss {
network_id: test_id_2.clone(),
network_info: InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info2,
multiple_bss_candidates: false,
});
// stronger network returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_1.clone(),
credential: credential_1.clone(),
bss: bss_info1.bss_desc.clone(),
observed_in_passive_scan: Some(networks[0].bss_info.observed_in_passive_scan),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.bssid
))
);
// mark the stronger network as having a failure
let mut modified_network = networks[0].clone();
modified_network.network_info.recent_failure_count = 2;
networks[0] = modified_network;
// weaker network (with no failures) returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_2.clone(),
credential: credential_2.clone(),
bss: bss_info2.bss_desc.clone(),
observed_in_passive_scan: Some(networks[1].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
let mut modified_network = networks[1].clone();
modified_network.network_info.recent_failure_count = 2;
networks[1] = modified_network;
// stronger network returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_1.clone(),
credential: credential_1.clone(),
bss: bss_info1.bss_desc.clone(),
observed_in_passive_scan: Some(networks[0].bss_info.observed_in_passive_scan),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.bssid
))
);
}
#[test]
fn select_best_connection_candidate_incompatible() {
// 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 {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info1,
multiple_bss_candidates: true,
});
let bss_info2 = types::Bss {
compatible: false,
rssi: -10,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info2,
multiple_bss_candidates: true,
});
let bss_info3 = types::Bss {
compatible: true,
rssi: -12,
channel: generate_channel(1),
..generate_random_bss()
};
networks.push(InternalBss {
network_id: test_id_2.clone(),
network_info: InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info3,
multiple_bss_candidates: false,
});
// stronger network returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_2.clone(),
credential: credential_2.clone(),
bss: bss_info3.bss_desc.clone(),
observed_in_passive_scan: Some(networks[2].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.bss_info.clone() };
modified_network.bss_info = &modified_bss_info;
networks[2] = modified_network;
// other network returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_1.clone(),
credential: credential_1.clone(),
bss: networks[0].bss_info.bss_desc.clone(),
observed_in_passive_scan: Some(networks[0].bss_info.observed_in_passive_scan),
multiple_bss_candidates: Some(true),
},
networks[0].bss_info.channel,
networks[0].bss_info.bssid
))
);
}
#[test]
fn select_best_connection_candidate_ignore_list() {
// 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 {
network_id: test_id_1.clone(),
network_info: InternalSavedNetworkData {
credential: credential_1.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info1,
multiple_bss_candidates: false,
});
let bss_info2 = types::Bss { compatible: true, rssi: -12, ..generate_random_bss() };
networks.push(InternalBss {
network_id: test_id_2.clone(),
network_info: InternalSavedNetworkData {
credential: credential_2.clone(),
has_ever_connected: true,
recent_failure_count: 0,
},
bss_info: &bss_info2,
multiple_bss_candidates: false,
});
// stronger network returned
assert_eq!(
select_best_connection_candidate(
networks.clone(),
&vec![],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_2.clone(),
credential: credential_2.clone(),
bss: bss_info2.bss_desc.clone(),
observed_in_passive_scan: Some(networks[1].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()],
&WlanHasher::new(rand::thread_rng().gen::<u64>().to_le_bytes())
),
Some((
types::ConnectionCandidate {
network: test_id_1.clone(),
credential: credential_1.clone(),
bss: bss_info1.bss_desc.clone(),
observed_in_passive_scan: Some(networks[0].bss_info.observed_in_passive_scan),
multiple_bss_candidates: Some(false),
},
bss_info1.channel,
bss_info1.bssid
))
);
}
#[fasync::run_singlethreaded(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());
}
#[test]
fn perform_scan_cache_is_stale() {
let mut exec = fasync::Executor::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);
}
#[test]
fn augment_bss_with_active_scan_doesnt_run_on_actively_found_networks() {
let mut exec = fasync::Executor::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: 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)}
);
}
#[test]
fn augment_bss_with_active_scan_runs_on_passively_found_networks() {
let mut exec = fasync::Executor::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: 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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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: 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
}
);
}
#[test]
fn find_best_connection_candidate_end_to_end() {
let mut exec = fasync::Executor::new().expect("failed to create an executor");
let mut test_values = exec.run_singlethreaded(test_setup());
let network_selector = test_values.network_selector;
// 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
exec.run_singlethreaded(
test_values.saved_network_manager.store(test_id_1.clone().into(), credential_1.clone()),
)
.unwrap();
exec.run_singlethreaded(
test_values.saved_network_manager.store(test_id_2.clone().into(), credential_2.clone()),
)
.unwrap();
// 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(),
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(),
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);
// 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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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,
);
// Process scan results
assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending);
// It takes an indeterminate amount of time for the scan module to either send the results
// to the location sensor, or be notified by the component framework that the location
// sensor's channel is closed / non-existent. Keep trying to advance the future until the
// next expected event happens (i.e. an event is present on the sme stream for the expected
// active scan).
let mut counter = 0;
let sme_stream_result = loop {
counter += 1;
if counter > 1000 {
panic!("Failed to progress network selection future until active scan");
};
let sleep_duration = zx::Duration::from_millis(2);
exec.run_singlethreaded(fasync::Timer::new(sleep_duration.after_now()));
assert_variant!(exec.run_until_stalled(&mut network_selection_fut), Poll::Pending);
match exec.run_until_stalled(&mut test_values.sme_stream.next()) {
Poll::Pending => continue,
other_result => {
debug!("Required {} iterations to get an SME stream message", counter);
break other_result;
}
}
};
// 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 mut 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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa3Enterprise,
compatible: true,
bss_desc: bss_desc1_active.clone(),
}];
assert_variant!(
sme_stream_result,
Poll::Ready(Some(Ok(fidl_sme::ClientSmeRequest::Scan {
txn, req, control_handle: _
}))) => {
// Validate the request
assert_eq!(req, expected_scan_request);
// Send all the APs
let (_stream, ctrl) = txn
.into_stream_and_control_handle().expect("error accessing control handle");
ctrl.send_on_result(&mut mock_scan_results.iter_mut())
.expect("failed to send scan data");
// Send the end of data
ctrl.send_on_finished()
.expect("failed to send scan data");
}
);
// 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: bss_desc1_active.clone(),
observed_in_passive_scan: Some(true),
multiple_bss_candidates: Some(false)
})
);
// 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);
// 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_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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::Cbw20,
secondary80: 0,
},
protection: fidl_sme::Protection::Wpa1,
compatible: true,
bss_desc: bss_desc2_active.clone(),
}];
validate_sme_scan_request_and_send_results(
&mut exec,
&mut test_values.sme_stream,
&expected_scan_request,
mock_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: bss_desc2_active.clone(),
observed_in_passive_scan: Some(true),
multiple_bss_candidates: Some(false)
})
);
}
#[fasync::run_singlethreaded(test)]
async 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 test_values = test_setup().await;
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());
test_values
.saved_network_manager
.store(wpa_network_id.clone().into(), credential.clone())
.await
.expect("Failed to save network");
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());
test_values
.saved_network_manager
.store(wpa3_network_id.clone().into(), wpa3_credential.clone())
.await
.expect("Failed to save network");
// Record passive connects so that the test will not active scan.
test_values
.saved_network_manager
.record_connect_result(
wpa_network_id.clone().into(),
&credential,
fidl_sme::ConnectResultCode::Success,
Some(fidl_common::ScanType::Passive),
)
.await;
test_values
.saved_network_manager
.record_connect_result(
wpa3_network_id.clone().into(),
&wpa3_credential,
fidl_sme::ConnectResultCode::Success,
Some(fidl_common::ScanType::Passive),
)
.await;
// Feed scans with WPA2 and WPA3 results to network selector, as we should get if a
// WPA2/WPA3 network was seen.
let id = types::NetworkIdentifier { ssid: ssid, type_: types::SecurityType::Wpa2 };
let mixed_scan_results = vec![types::ScanResult {
id: id.clone(),
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();
updater.update_scan_results(&mixed_scan_results).await;
// Check that we choose the config saved as WPA2
assert_eq!(
network_selector
.find_best_connection_candidate(test_values.iface_manager.clone(), &vec![])
.await,
Some(types::ConnectionCandidate {
network: id.clone(),
credential,
bss: mixed_scan_results[0].entries[0].bss_desc.clone(),
observed_in_passive_scan: Some(
mixed_scan_results[0].entries[0].observed_in_passive_scan
),
multiple_bss_candidates: Some(false),
})
);
assert_eq!(
network_selector
.find_best_connection_candidate(test_values.iface_manager, &vec![id])
.await,
None
);
}
#[test]
fn find_connection_candidate_for_network_end_to_end() {
let mut exec = fasync::Executor::new().expect("failed to create an executor");
let mut test_values = exec.run_singlethreaded(test_setup());
let network_selector = test_values.network_selector;
// 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
exec.run_singlethreaded(
test_values.saved_network_manager.store(test_id_1.clone().into(), credential_1.clone()),
)
.unwrap();
// 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.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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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::WlanChan {
primary: 1,
cbw: fidl_common::Cbw::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: 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),
})
);
}
#[test]
fn find_connection_candidate_for_network_end_to_end_with_failure() {
let mut exec = fasync::Executor::new().expect("failed to create an executor");
let mut test_values = exec.run_singlethreaded(test_setup());
let network_selector = test_values.network_selector;
// 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);
// 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);
}
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();
types::ScanResult {
id: types::NetworkIdentifier {
ssid: format!("scan result rand {}", rng.gen::<i32>()).as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
},
entries: vec![generate_random_bss(), generate_random_bss()],
compatibility: types::Compatibility::Supported,
}
}
fn generate_random_saved_network() -> (types::NetworkIdentifier, InternalSavedNetworkData) {
let mut rng = rand::thread_rng();
(
types::NetworkIdentifier {
ssid: format!("saved network rand {}", rng.gen::<i32>()).as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
},
InternalSavedNetworkData {
credential: Credential::Password(
format!("password {}", rng.gen::<i32>()).as_bytes().to_vec(),
),
has_ever_connected: false,
recent_failure_count: 0,
},
)
}
#[fasync::run_singlethreaded(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_id_1 = types::NetworkIdentifier {
ssid: "foo".as_bytes().to_vec(),
type_: types::SecurityType::Wpa3,
};
let test_id_2 = types::NetworkIdentifier {
ssid: "bar".as_bytes().to_vec(),
type_: types::SecurityType::Wpa,
};
let mock_scan_results = vec![
types::ScanResult {
id: test_id_1.clone(),
entries: vec![
types::Bss { observed_in_passive_scan: true, ..generate_random_bss() },
types::Bss { observed_in_passive_scan: true, ..generate_random_bss() },
types::Bss { observed_in_passive_scan: false, ..generate_random_bss() },
],
compatibility: types::Compatibility::Supported,
},
types::ScanResult {
id: test_id_2.clone(),
entries: vec![types::Bss {
observed_in_passive_scan: true,
..generate_random_bss()
}],
compatibility: types::Compatibility::Supported,
},
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
];
let mut mock_saved_networks = HashMap::new();
mock_saved_networks.insert(
test_id_1.clone(),
InternalSavedNetworkData {
credential: Credential::Password("foo_pass".as_bytes().to_vec()),
has_ever_connected: false,
recent_failure_count: 0,
},
);
mock_saved_networks.insert(
test_id_2.clone(),
InternalSavedNetworkData {
credential: Credential::Password("bar_pass".as_bytes().to_vec()),
has_ever_connected: false,
recent_failure_count: 0,
},
);
let random_saved_net = generate_random_saved_network();
mock_saved_networks.insert(random_saved_net.0, random_saved_net.1);
let random_saved_net = generate_random_saved_network();
mock_saved_networks.insert(random_saved_net.0, random_saved_net.1);
let random_saved_net = generate_random_saved_network();
mock_saved_networks.insert(random_saved_net.0, random_saved_net.1);
record_metrics_on_scan(&mock_scan_results, mock_saved_networks, &mut cobalt_api);
// Three BSSs present for network 1 in scan results
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID)
.with_event_code(
SavedNetworkInScanResultMetricDimensionBssCount::TwoToFour.as_event_code()
)
.as_event()
)
);
// One BSS present for network 2 in scan results
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_NETWORK_IN_SCAN_RESULT_METRIC_ID)
.with_event_code(
SavedNetworkInScanResultMetricDimensionBssCount::One.as_event_code()
)
.as_event()
)
);
// 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());
}
#[fasync::run_singlethreaded(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![
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
generate_random_scan_result(),
];
let mock_saved_networks = HashMap::new();
record_metrics_on_scan(&mock_scan_results, mock_saved_networks, &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());
}
}