blob: 5de7a0b50b0d8b29c03d2651cf8502ca4e919112 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
super::{
network_config::{
Credential, FailureReason, HiddenProbEvent, NetworkConfig, NetworkConfigError,
NetworkIdentifier, SecurityType,
},
stash_conversion::*,
},
crate::{
client::{network_selection::upgrade_security, types},
legacy::known_ess_store::{self, EssJsonRead, KnownEss, KnownEssStore},
},
anyhow::format_err,
fidl_fuchsia_wlan_common::ScanType,
fidl_fuchsia_wlan_sme as fidl_sme,
fuchsia_cobalt::CobaltSender,
futures::lock::Mutex,
log::{error, info},
rand::Rng,
std::{
clone::Clone,
collections::{hash_map::Entry, HashMap},
fs, io,
path::Path,
},
wlan_metrics_registry::{
SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations,
SavedNetworksMetricDimensionSavedNetworks,
SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID, SAVED_NETWORKS_METRIC_ID,
},
wlan_stash::policy::{PolicyStash as Stash, POLICY_STASH_ID},
};
const MAX_CONFIGS_PER_SSID: usize = 1;
/// The Saved Network Manager keeps track of saved networks and provides thread-safe access to
/// saved networks. Networks are saved by NetworkConfig and accessed by their NetworkIdentifier
/// (SSID and security protocol). Network configs are saved in-memory, and part of each network
/// data is saved persistently. Futures aware locks are used in order to wait for the stash flush
/// operations to complete when data changes.
pub struct SavedNetworksManager {
saved_networks: Mutex<NetworkConfigMap>,
stash: Mutex<Stash>,
legacy_store: KnownEssStore,
cobalt_api: Mutex<CobaltSender>,
}
/// Save multiple network configs per SSID in able to store multiple connections with different
/// credentials, for different authentication credentials on the same network or for different
/// networks with the same name.
type NetworkConfigMap = HashMap<NetworkIdentifier, Vec<NetworkConfig>>;
pub enum ScanResultType {
#[allow(dead_code)]
Undirected,
Directed(Vec<types::NetworkIdentifier>), // Contains list of target SSIDs
}
impl SavedNetworksManager {
/// Initializes a new Saved Network Manager by reading saved networks from a secure storage
/// (stash) or from a legacy storage file (from KnownEssStore) if stash is empty. In either
/// case it initializes in-memory storage and persistent storage with stash to remember
/// networks.
pub async fn new(cobalt_api: CobaltSender) -> Result<Self, anyhow::Error> {
let path = known_ess_store::KNOWN_NETWORKS_PATH;
let tmp_path = known_ess_store::TMP_KNOWN_NETWORKS_PATH;
Self::new_with_stash_or_paths(
POLICY_STASH_ID,
Path::new(path),
Path::new(tmp_path),
cobalt_api,
)
.await
}
/// Load from persistent data from 1 of 2 places: stash or the file created by KnownEssStore.
/// For now we need to support reading from the the legacy version (KnownEssStore) as well
/// from stash. And we need to keep the legacy version temporarily so we will decide where to
/// read based on whether stash has been used.
/// TODO(fxbug.dev/44184) Eventually delete logic for handling legacy storage from KnownEssStore and
/// update comments once all users have migrated.
pub async fn new_with_stash_or_paths(
stash_id: impl AsRef<str>,
legacy_path: impl AsRef<Path>,
legacy_tmp_path: impl AsRef<Path>,
cobalt_api: CobaltSender,
) -> Result<Self, anyhow::Error> {
let mut stash = Stash::new_with_id(stash_id.as_ref())?;
let stashed_networks = stash.load().await?;
let mut saved_networks: HashMap<NetworkIdentifier, Vec<NetworkConfig>> = stashed_networks
.iter()
.map(|(network_id, persistent_data)| {
(
NetworkIdentifier::from(network_id.clone()),
persistent_data
.iter()
.filter_map(|data| {
NetworkConfig::new(
NetworkIdentifier::from(network_id.clone()),
data.credential.clone().into(),
data.has_ever_connected,
)
.ok()
})
.collect(),
)
})
.collect();
// Don't read legacy if stash is not empty; we have already migrated.
if saved_networks.is_empty() {
Self::migrate_legacy(legacy_path.as_ref(), &mut stash, &mut saved_networks).await?;
}
// KnownEssStore will internally load from the correct path.
let legacy_store = KnownEssStore::new_with_paths(
legacy_path.as_ref().to_path_buf(),
legacy_tmp_path.as_ref().to_path_buf(),
)?;
Ok(Self {
saved_networks: Mutex::new(saved_networks),
stash: Mutex::new(stash),
legacy_store,
cobalt_api: Mutex::new(cobalt_api),
})
}
/// Creates a new config at a random path, ensuring a clean environment for an individual test
#[cfg(test)]
pub async fn new_for_test() -> Result<Self, anyhow::Error> {
use crate::util::testing::cobalt::create_mock_cobalt_sender;
use rand::{distributions::Alphanumeric, thread_rng};
let stash_id: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
let path: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
let tmp_path: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
Self::new_with_stash_or_paths(
stash_id,
Path::new(&path),
Path::new(&tmp_path),
create_mock_cobalt_sender(),
)
.await
}
/// Creates a new SavedNetworksManager and hands back the other end of the stash proxy used.
/// This should be used when a test does something that will interact with stash and uses the
/// executor to step through futures.
#[cfg(test)]
pub async fn new_and_stash_server(
legacy_path: impl AsRef<Path>,
legacy_tmp_path: impl AsRef<Path>,
) -> (Self, fidl_fuchsia_stash::StoreAccessorRequestStream) {
use crate::util::testing::cobalt::create_mock_cobalt_sender;
use rand::{distributions::Alphanumeric, thread_rng};
let id: String = thread_rng().sample_iter(&Alphanumeric).take(20).collect();
use fidl::endpoints::create_proxy;
let (store_client, _stash_server) = create_proxy::<fidl_fuchsia_stash::StoreMarker>()
.expect("failed to create stash proxy");
store_client.identify(id.as_ref()).expect("failed to identify client to store");
let (store, accessor_server) = create_proxy::<fidl_fuchsia_stash::StoreAccessorMarker>()
.expect("failed to create accessor proxy");
let stash = Stash::new_with_stash(store);
let legacy_store = KnownEssStore::new_with_paths(
legacy_path.as_ref().to_path_buf(),
legacy_tmp_path.as_ref().to_path_buf(),
)
.expect("failed to create legacy store");
(
Self {
saved_networks: Mutex::new(NetworkConfigMap::new()),
stash: Mutex::new(stash),
legacy_store,
cobalt_api: Mutex::new(create_mock_cobalt_sender()),
},
accessor_server.into_stream().expect("failed to create stash request stream"),
)
}
/// Read from the old persistent storage of network configs, then write them into the new
/// storage, both in the hashmap and the stash that stores them persistently.
async fn migrate_legacy(
legacy_storage_path: impl AsRef<Path>,
stash: &mut Stash,
saved_networks: &mut NetworkConfigMap,
) -> Result<(), anyhow::Error> {
info!("Attempting to migrate saved networks from legacy implementation to stash");
match Self::load_from_path(&legacy_storage_path) {
Ok(legacy_saved_networks) => {
for (net_id, configs) in legacy_saved_networks {
stash
.write(
&net_id.clone().into(),
&network_config_vec_to_persistent_data(&configs),
)
.await?;
saved_networks.insert(net_id, configs);
}
}
Err(e) => {
format_err!("Failed to load legacy networks: {}", e);
}
}
Ok(())
}
/// Handles reading networks persisted by the previous version of network storage
/// (KnownEssStore) for the new store.
fn load_from_path(storage_path: impl AsRef<Path>) -> Result<NetworkConfigMap, anyhow::Error> {
// Temporarily read from memory the same way EssStore does.
let config_list: Vec<EssJsonRead> = match fs::File::open(&storage_path) {
Ok(file) => match serde_json::from_reader(io::BufReader::new(file)) {
Ok(list) => list,
Err(e) => {
error!(
"Failed to parse the list of known wireless networks from JSONin {}: {}. \
Starting with an empty list.",
storage_path.as_ref().display(),
e
);
fs::remove_file(&storage_path).map_err(|e| {
format_err!("Failed to delete {}: {}", storage_path.as_ref().display(), e)
})?;
Vec::new()
}
},
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Vec::new(),
_ => {
return Err(format_err!(
"Failed to open {}: {}",
storage_path.as_ref().display(),
e
))
}
},
};
let mut saved_networks = HashMap::<NetworkIdentifier, Vec<NetworkConfig>>::new();
for config in config_list {
// Choose appropriate unknown values based on password and how known ESS have been used
// for connections. Credential cannot be read in as PSK - PSK's were not supported by
// before the new storage was in place.
let credential = Credential::from_bytes(config.password);
let network_id =
NetworkIdentifier::new(config.ssid, credential.derived_security_type());
if let Ok(network_config) = NetworkConfig::new(network_id.clone(), credential, false) {
saved_networks.entry(network_id).or_default().push(network_config);
} else {
error!(
"Error creating network config from loaded data for SSID {}",
String::from_utf8_lossy(&network_id.ssid.clone())
);
}
}
Ok(saved_networks)
}
/// Attempt to remove the NetworkConfig described by the specified NetworkIdentifier and
/// Credential. Return true if a NetworkConfig is remove and false otherwise.
pub async fn remove(
&self,
network_id: NetworkIdentifier,
credential: Credential,
) -> Result<bool, NetworkConfigError> {
// Find any matching NetworkConfig and remove it.
let mut saved_networks = self.saved_networks.lock().await;
if let Some(network_configs) = saved_networks.get_mut(&network_id) {
let original_len = network_configs.len();
// Keep the configs that don't match provided NetworkIdentifier and Credential.
network_configs.retain(|cfg| cfg.credential != credential);
// Update stash and legacy storage if there was a change
if original_len != network_configs.len() {
self.stash
.lock()
.await
.write(
&network_id.clone().into(),
&network_config_vec_to_persistent_data(&network_configs),
)
.await
.map_err(|_| NetworkConfigError::StashWriteError)?;
self.legacy_store
.remove(network_id.ssid, credential.into_bytes())
.map_err(|_| NetworkConfigError::LegacyWriteError)?;
return Ok(true);
} else {
info!("No matching network with the provided credential was found to remove.");
}
} else {
info!("No network was found to remove with the provided SSID and security.");
}
Ok(false)
}
/// Clear the in memory storage and the persistent storage. Also clear the legacy storage.
pub async fn clear(&self) -> Result<(), anyhow::Error> {
self.saved_networks.lock().await.clear();
self.stash.lock().await.clear().await?;
self.legacy_store.clear()
}
/// Get the count of networks in store, including multiple values with same SSID
pub async fn known_network_count(&self) -> usize {
self.saved_networks.lock().await.values().into_iter().flatten().count()
}
/// Return a list of network configs that match the given SSID.
pub async fn lookup(&self, id: NetworkIdentifier) -> Vec<NetworkConfig> {
self.saved_networks.lock().await.entry(id).or_default().iter().map(Clone::clone).collect()
}
/// Save a network by SSID and password. If the SSID and password have been saved together
/// before, do not modify the saved config. Update the legacy storage to keep it consistent
/// with what it did before the new version. If a network is pushed out because of the newly
/// saved network, this will return the removed config.
pub async fn store(
&self,
network_id: NetworkIdentifier,
credential: Credential,
) -> Result<Option<NetworkConfig>, NetworkConfigError> {
let mut saved_networks = self.saved_networks.lock().await;
let network_entry = saved_networks.entry(network_id.clone());
if let Entry::Occupied(network_configs) = &network_entry {
if network_configs.get().iter().any(|cfg| cfg.credential == credential) {
info!("Saving a previously saved network with same password.");
return Ok(None);
}
}
let network_config = NetworkConfig::new(network_id.clone(), credential.clone(), false)?;
let network_configs = network_entry.or_default();
let evicted_config = evict_if_needed(network_configs);
network_configs.push(network_config);
self.stash
.lock()
.await
.write(
&network_id.clone().into(),
&network_config_vec_to_persistent_data(&network_configs),
)
.await
.map_err(|_| NetworkConfigError::StashWriteError)?;
// Write saved networks to the legacy store only if they are WPA2 or Open, as legacy store
// does not support more types. Do not write PSK to legacy storage.
if let Credential::Psk(_) = credential {
return Ok(evicted_config);
}
if network_id.security_type == SecurityType::Wpa2
|| network_id.security_type == SecurityType::None
{
let ess = KnownEss { password: credential.into_bytes() };
self.legacy_store
.store(network_id.ssid, ess)
.map_err(|_| NetworkConfigError::LegacyWriteError)?;
}
Ok(evicted_config)
}
/// Update the specified saved network with the result of an attempted connect. If the
/// specified network could have been connected to with a different security type and we
/// do not find the specified config, we will check the other possible security type. For
/// example if a WPA3 network is specified, we will check WPA2 if it isn't found. If the
/// specified network is not saved, this function does not save it.
pub async fn record_connect_result(
&self,
id: NetworkIdentifier,
credential: &Credential,
connect_result: fidl_sme::ConnectResultCode,
discovered_in_scan: Option<ScanType>,
) {
let mut saved_networks = self.saved_networks.lock().await;
let mut ids = vec![id.clone()];
// This alternate possible network identifier will be checked only if the specified id is
// not found, as it is pushed to the end of the list.
if let Some(security_type) = lower_valid_security(&id.security_type) {
ids.push(NetworkIdentifier::new(id.ssid.clone(), security_type));
}
for id in ids.into_iter() {
let networks = match saved_networks.get_mut(&id) {
Some(networks) => networks,
None => {
continue;
}
};
for network in networks.iter_mut() {
if &network.credential == credential {
match connect_result {
fidl_sme::ConnectResultCode::Success => {
let mut has_change = false;
if !network.has_ever_connected {
network.has_ever_connected = true;
has_change = true;
}
if let Some(scan_type) = discovered_in_scan {
let connect_event = match scan_type {
ScanType::Passive => HiddenProbEvent::ConnectPassive,
ScanType::Active => HiddenProbEvent::ConnectActive,
};
network.update_hidden_prob(connect_event);
// TODO(60619): Update the stash with new probability if it has changed
}
if has_change {
// Update persistent storage since a config has changed.
let data = network_config_vec_to_persistent_data(&networks);
if let Err(e) =
self.stash.lock().await.write(&id.into(), &data).await
{
info!("Failed to record successful connect in stash: {}", e);
}
}
}
fidl_sme::ConnectResultCode::CredentialRejected => {
network.perf_stats.failure_list.add(FailureReason::CredentialRejected);
}
fidl_sme::ConnectResultCode::Failed => {
network.perf_stats.failure_list.add(FailureReason::GeneralFailure);
}
fidl_sme::ConnectResultCode::Canceled => {}
}
return;
}
}
}
// Will not reach here if we find the saved network with matching SSID and credential.
info!("Failed to find network to record result of connect attempt.");
}
pub async fn record_periodic_metrics(&self) {
let saved_networks = self.saved_networks.lock().await;
let mut cobalt_api = self.cobalt_api.lock().await;
log_cobalt_metrics(&*saved_networks, &mut cobalt_api);
}
/// Update hidden networks probabilities based on scan results. Record either results of a
/// passive scan or a directed active scan.
pub async fn record_scan_result(
&self,
scan_type: ScanResultType,
results: Vec<types::NetworkIdentifier>,
) {
match scan_type {
ScanResultType::Undirected => {
self.record_hidden_prob_event(HiddenProbEvent::SeenPassive, results).await;
}
ScanResultType::Directed(target_ssids) => {
let ids_not_seen: Vec<types::NetworkIdentifier> = target_ssids
.into_iter()
.filter(|id| {
// If we can find a matching scan result, this network should not be
// included in the list of networks we didn't see.
!contains_matching_network(id, &results)
})
.collect();
self.record_hidden_prob_event(HiddenProbEvent::NotSeenActive, ids_not_seen).await;
}
}
}
async fn record_hidden_prob_event(
&self,
event: HiddenProbEvent,
networks: Vec<types::NetworkIdentifier>,
) {
let mut saved_networks = self.saved_networks.lock().await;
for network in networks.into_iter() {
if let Some(networks) = saved_networks.get_mut(&network.into()) {
for network in networks.iter_mut() {
network.update_hidden_prob(event);
}
// TODO(60619): Update the stash with new probability if it has changed
}
}
}
// Return a list of every network config that has been saved.
pub async fn get_networks(&self) -> Vec<NetworkConfig> {
self.saved_networks
.lock()
.await
.values()
.into_iter()
.map(|cfgs| cfgs.clone())
.flatten()
.collect()
}
}
/// Returns a subset of potentially hidden saved networks, filtering probabilistically based
/// on how certain they are to be hidden.
pub fn select_subset_potentially_hidden_networks(
saved_networks: Vec<NetworkConfig>,
) -> Vec<types::NetworkIdentifier> {
saved_networks
.into_iter()
.filter(|saved_network| {
// Roll a dice to see if we should scan for it. The function gen_range(low, high)
// has an inclusive lower bound and exclusive upper bound, so using it as
// `hidden_probability > gen_range(0, 1)` means that:
// - hidden_probability of 1 will _always_ be selected
// - hidden_probability of 0 will _never_ be selected
saved_network.hidden_probability > rand::thread_rng().gen_range(0.0, 1.0)
})
.map(|network| types::NetworkIdentifier {
ssid: network.ssid,
type_: network.security_type.into(),
})
.collect()
}
/// Checks whether the provided saved network identifier could be matched with one in the list of
/// scanned network identifiers. The SSID must be the same, and the security type must be the same
/// or one that matches by upgrading.
fn contains_matching_network(
saved_id: &types::NetworkIdentifier,
scanned_ids: &Vec<types::NetworkIdentifier>,
) -> bool {
scanned_ids.iter().any(|scanned_id| {
let security_matches = saved_id.type_ == scanned_id.type_
|| upgrade_security(&scanned_id.type_.clone().into()).as_ref() == Some(&saved_id.type_);
scanned_id.ssid == saved_id.ssid && security_matches
})
}
/// Returns a security type that could have been upgraded to the provided security type
/// in an auto connect.
fn lower_valid_security(security_type: &SecurityType) -> Option<SecurityType> {
match security_type {
SecurityType::Wpa3 => Some(SecurityType::Wpa2),
SecurityType::Wpa2 => Some(SecurityType::Wpa),
_ => None,
}
}
/// If the list of configs is at capacity for the number of saved configs per SSID,
/// remove a saved network that has never been successfully connected to. If all have
/// been successfully connected to, remove any. If a network config is evicted, that connection
/// is forgotten for future connections.
/// TODO(fxbug.dev/41232) - when network configs record information about successful connections,
/// use this to make a better decision what to forget if all networks have connected before.
/// TODO(fxbug.dev/41626) - make sure that we disconnect from the network if we evict a network config
/// for a network we are currently connected to.
fn evict_if_needed(configs: &mut Vec<NetworkConfig>) -> Option<NetworkConfig> {
if configs.len() < MAX_CONFIGS_PER_SSID {
return None;
}
for i in 0..configs.len() {
if let Some(config) = configs.get(i) {
if !config.has_ever_connected {
return Some(configs.remove(i));
}
}
}
// If all saved networks have connected, remove the first network
return Some(configs.remove(0));
}
/// Record Cobalt metrics related to Saved Networks
fn log_cobalt_metrics(saved_networks: &NetworkConfigMap, cobalt_api: &mut CobaltSender) {
// Count the total number of saved networks
let num_networks = match saved_networks.len() {
0 => SavedNetworksMetricDimensionSavedNetworks::Zero,
1 => SavedNetworksMetricDimensionSavedNetworks::One,
2..=4 => SavedNetworksMetricDimensionSavedNetworks::TwoToFour,
5..=40 => SavedNetworksMetricDimensionSavedNetworks::FiveToForty,
41..=500 => SavedNetworksMetricDimensionSavedNetworks::FortyToFiveHundred,
501..=usize::MAX => SavedNetworksMetricDimensionSavedNetworks::FiveHundredAndOneOrMore,
_ => unreachable!(),
};
cobalt_api.log_event(SAVED_NETWORKS_METRIC_ID, num_networks);
// Count the number of configs for each saved network
for saved_network in saved_networks {
let configs = saved_network.1;
use SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations as ConfigCountDimension;
let num_configs = match configs.len() {
0 => ConfigCountDimension::Zero,
1 => ConfigCountDimension::One,
2..=4 => ConfigCountDimension::TwoToFour,
5..=40 => ConfigCountDimension::FiveToForty,
41..=500 => ConfigCountDimension::FortyToFiveHundred,
501..=usize::MAX => ConfigCountDimension::FiveHundredAndOneOrMore,
_ => unreachable!(),
};
cobalt_api.log_event(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID, num_configs);
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
config_management::{
PROB_HIDDEN_DEFAULT, PROB_HIDDEN_IF_CONNECT_ACTIVE, PROB_HIDDEN_IF_CONNECT_PASSIVE,
PROB_HIDDEN_IF_SEEN_PASSIVE,
},
util::testing::cobalt::{
create_mock_cobalt_sender, create_mock_cobalt_sender_and_receiver,
},
},
cobalt_client::traits::AsEventCode,
fidl_fuchsia_cobalt::CobaltEvent,
fidl_fuchsia_stash as fidl_stash, fuchsia_async as fasync,
fuchsia_cobalt::cobalt_event_builder::CobaltEventExt,
fuchsia_zircon as zx,
futures::{task::Poll, TryStreamExt},
pin_utils::pin_mut,
rand::{distributions::Alphanumeric, thread_rng, Rng},
std::{io::Write, mem},
tempfile::TempDir,
test_case::test_case,
wlan_common::assert_variant,
};
#[fasync::run_singlethreaded(test)]
async fn store_and_lookup() {
let stash_id = "store_and_lookup";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
// Expect the store to be constructed successfully even if the file doesn't
// exist yet
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id_foo = NetworkIdentifier::new("foo", SecurityType::Wpa2);
assert!(saved_networks.lookup(network_id_foo.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Store a network and verify it was stored.
saved_networks
.store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' failed");
assert_eq!(
vec![network_config("foo", "qwertyuio")],
saved_networks.lookup(network_id_foo.clone()).await
);
assert_eq!(1, saved_networks.known_network_count().await);
// Store another network with the same SSID.
saved_networks
.store(network_id_foo.clone(), Credential::Password(b"12345678".to_vec()))
.await
.expect("storing 'foo' a second time failed");
// There should only be one saved "foo" network because MAX_CONFIGS_PER_SSID is 1.
// When this constant becomes greater than 1, both network configs should be found
assert_eq!(
vec![network_config("foo", "12345678")],
saved_networks.lookup(network_id_foo.clone()).await
);
assert_eq!(1, saved_networks.known_network_count().await);
// Store another network and verify.
let network_id_baz = NetworkIdentifier::new("baz", SecurityType::Wpa2);
let psk = Credential::Psk(vec![1; 32]);
let config_baz = NetworkConfig::new(network_id_baz.clone(), psk.clone(), false)
.expect("failed to create network config");
saved_networks
.store(network_id_baz.clone(), psk)
.await
.expect("storing 'baz' with PSK failed");
assert_eq!(vec![config_baz.clone()], saved_networks.lookup(network_id_baz.clone()).await);
assert_eq!(2, saved_networks.known_network_count().await);
// Saved networks should persist when we create a saved networks manager with the same ID.
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
assert_eq!(
vec![network_config("foo", "12345678")],
saved_networks.lookup(network_id_foo.clone()).await
);
assert_eq!(vec![config_baz], saved_networks.lookup(network_id_baz).await);
assert_eq!(2, saved_networks.known_network_count().await);
}
#[fasync::run_singlethreaded(test)]
async fn store_twice() {
let stash_id = "store_twice";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
// Expect the store to be constructed successfully even if the file doesn't
// exist yet
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2);
saved_networks
.store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' failed");
saved_networks
.store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' a second time failed");
let expected_cfgs = vec![network_config("foo", "qwertyuio")];
assert_eq!(expected_cfgs, saved_networks.lookup(network_id).await);
assert_eq!(1, saved_networks.known_network_count().await);
}
#[fasync::run_singlethreaded(test)]
async fn store_many_same_ssid() {
let stash_id = "store_many_same_ssid";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
// Expect the store to be constructed successfully even if the file doesn't
// exist yet
let network_id = NetworkIdentifier::new("foo", SecurityType::Wpa2);
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
// save max + 1 networks with same SSID and different credentials
for i in 0..MAX_CONFIGS_PER_SSID + 1 {
let mut password = b"password".to_vec();
password.push(i as u8);
saved_networks
.store(network_id.clone(), Credential::Password(password))
.await
.expect("Failed to saved network");
}
// since none have been connected to yet, we don't care which config was removed
assert_eq!(MAX_CONFIGS_PER_SSID, saved_networks.lookup(network_id).await.len());
}
#[fasync::run_singlethreaded(test)]
async fn store_and_remove() {
let stash_id = "store_and_remove";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id = NetworkIdentifier::new("foo", SecurityType::Wpa2);
let credential = Credential::Password(b"qwertyuio".to_vec());
assert!(saved_networks.lookup(network_id.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Store a network and verify it was stored.
saved_networks
.store(network_id.clone(), credential.clone())
.await
.expect("storing 'foo' failed");
assert_eq!(
vec![network_config("foo", "qwertyuio")],
saved_networks.lookup(network_id.clone()).await
);
assert_eq!(1, saved_networks.known_network_count().await);
// Remove a network with the same NetworkIdentifier but differenct credential and verify
// that the saved network is unaffected.
assert_eq!(
false,
saved_networks
.remove(network_id.clone(), Credential::Password(b"diff-password".to_vec()))
.await
.expect("removing 'foo' failed")
);
assert_eq!(1, saved_networks.known_network_count().await);
// Remove the network and check it is gone
assert_eq!(
true,
saved_networks
.remove(network_id.clone(), credential.clone())
.await
.expect("removing 'foo' failed")
);
assert_eq!(0, saved_networks.known_network_count().await);
// If we try to remove the network again, we won't get an error and nothing happens
assert_eq!(
false,
saved_networks
.remove(network_id.clone(), credential)
.await
.expect("removing 'foo' failed")
);
// Check that removal persists.
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("Failed to create SavedNetworksManager");
assert_eq!(0, saved_networks.known_network_count().await);
assert!(saved_networks.lookup(network_id.clone()).await.is_empty());
}
#[fasync::run_singlethreaded(test)]
async fn connect_network() {
let stash_id = "connect_network";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id = NetworkIdentifier::new("bar", SecurityType::Wpa2);
let credential = Credential::Password(b"password".to_vec());
// If connect and network hasn't been saved, we should not save the network.
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Success,
None,
)
.await;
assert!(saved_networks.lookup(network_id.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Save the network and record a successful connection.
saved_networks
.store(network_id.clone(), credential.clone())
.await
.expect("Failed save network");
let config = network_config("bar", "password");
assert_eq!(vec![config], saved_networks.lookup(network_id.clone()).await);
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Success,
None,
)
.await;
// The network should be saved with the connection recorded. We should not have recorded
// that the network was connected to passively or actively.
assert_variant!(saved_networks.lookup(network_id.clone()).await.as_slice(), [config] => {
assert_eq!(config.has_ever_connected, true);
assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
});
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Success,
Some(ScanType::Active),
)
.await;
// We should now see that we connected to the network after an active scan.
assert_variant!(saved_networks.lookup(network_id.clone()).await.as_slice(), [config] => {
assert_eq!(config.has_ever_connected, true);
assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
});
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Success,
Some(ScanType::Passive),
)
.await;
// The config should have a lower hidden probability after connecting after a passive scan.
assert_variant!(saved_networks.lookup(network_id.clone()).await.as_slice(), [config] => {
assert_eq!(config.has_ever_connected, true);
assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
});
// Success connects should be saved as persistent data.
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("Failed to create SavedNetworksManager");
assert_variant!(saved_networks.lookup(network_id).await.as_slice(), [config] => {
assert_eq!(config.has_ever_connected, true);
});
}
#[test_case(SecurityType::Wpa3, Some(SecurityType::Wpa2))]
#[test_case(SecurityType::Wpa2, Some(SecurityType::Wpa))]
#[test_case(SecurityType::Wpa, None)]
#[test_case(SecurityType::Wep, None)]
#[test_case(SecurityType::None, None)]
fn test_lower_security(
security_type: SecurityType,
expected_security_type: Option<SecurityType>,
) {
assert_eq!(expected_security_type, lower_valid_security(&security_type))
}
#[fasync::run_singlethreaded(test)]
async fn test_record_connect_valid_security() {
let stash_id = rand_string();
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let saved_network_id = NetworkIdentifier::new("foo", SecurityType::Wpa);
let credential = Credential::Password(b"some_password".to_vec());
let connected_id = NetworkIdentifier::new("foo", SecurityType::Wpa2);
// If we record a successful connection to a WPA2 network, we shouldn't mark a WPA3 network
let saved_unrecorded_id = NetworkIdentifier::new("foo", SecurityType::Wpa3);
saved_networks
.store(saved_network_id.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.store(saved_unrecorded_id.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.record_connect_result(
connected_id,
&credential,
fidl_sme::ConnectResultCode::Success,
Some(ScanType::Active),
)
.await;
assert_variant!(saved_networks.lookup(saved_network_id).await.as_slice(), [config] => {
assert!(config.has_ever_connected);
});
assert_variant!(saved_networks.lookup(saved_unrecorded_id).await.as_slice(), [config] => {
assert!(!config.has_ever_connected);
});
}
#[fasync::run_singlethreaded(test)]
async fn test_record_connect_updates_one() {
let stash_id = rand_string();
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let net_id = NetworkIdentifier::new("foo", SecurityType::Wpa2);
let net_id_also_valid = NetworkIdentifier::new("foo", SecurityType::Wpa);
let credential = Credential::Password(b"some_password".to_vec());
// Save the networks and record a successful connection.
saved_networks
.store(net_id.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.store(net_id_also_valid.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.record_connect_result(
net_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Success,
None,
)
.await;
assert_variant!(saved_networks.lookup(net_id).await.as_slice(), [config] => {
assert!(config.has_ever_connected);
});
// If the specified network identifier is found, record_conenct_result should not mark
// another config even if it could also have been used for the connect attempt.
assert_variant!(saved_networks.lookup(net_id_also_valid).await.as_slice(), [config] => {
assert!(!config.has_ever_connected);
});
}
#[fasync::run_singlethreaded(test)]
async fn test_record_connect_failure() {
let stash_id = "test_record_connect_failure";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id = NetworkIdentifier::new("foo", SecurityType::None);
let credential = Credential::None;
let before_recording = zx::Time::get_monotonic();
// Verify that recording connect result does not save the network.
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Failed,
None,
)
.await;
assert!(saved_networks.lookup(network_id.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Record that the connect failed.
saved_networks
.store(network_id.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::CredentialRejected,
None,
)
.await;
// Check that the failure was recorded correctly.
assert_eq!(1, saved_networks.known_network_count().await);
let saved_config = saved_networks
.lookup(network_id)
.await
.pop()
.expect("Failed to get saved network config");
let mut connect_failures =
saved_config.perf_stats.failure_list.get_recent(before_recording);
assert_eq!(1, connect_failures.len());
let connect_failure = connect_failures.pop().expect("Failed to get a connect failure");
assert_eq!(FailureReason::CredentialRejected, connect_failure.reason);
}
#[fasync::run_singlethreaded(test)]
async fn test_record_connect_cancelled_ignored() {
let stash_id = "test_record_connect_failure";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let network_id = NetworkIdentifier::new("foo", SecurityType::None);
let credential = Credential::None;
let before_recording = zx::Time::get_monotonic();
// Verify that recording connect result does not save the network.
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Canceled,
None,
)
.await;
assert!(saved_networks.lookup(network_id.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Record that the connect was canceled.
saved_networks
.store(network_id.clone(), credential.clone())
.await
.expect("Failed save network");
saved_networks
.record_connect_result(
network_id.clone(),
&credential,
fidl_sme::ConnectResultCode::Canceled,
None,
)
.await;
// Check that there are no failures recorded for this saved network.
assert_eq!(1, saved_networks.known_network_count().await);
let saved_config = saved_networks
.lookup(network_id)
.await
.pop()
.expect("Failed to get saved network config");
let connect_failures = saved_config.perf_stats.failure_list.get_recent(before_recording);
assert_eq!(0, connect_failures.len());
}
#[fasync::run_singlethreaded(test)]
async fn test_record_passive_scan() {
let stash_id = rand_string();
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
let saved_seen_id = NetworkIdentifier::new("foo", SecurityType::None);
let unsaved_id = NetworkIdentifier::new("bar", SecurityType::Wpa2);
let saved_unseen_id = NetworkIdentifier::new("baz", SecurityType::Wpa2);
let seen_credential = Credential::None;
let unseen_credential = Credential::Password(b"password".to_vec());
// Save the networks
saved_networks
.store(saved_seen_id.clone(), seen_credential.clone())
.await
.expect("Failed to save network");
saved_networks
.store(saved_unseen_id.clone(), unseen_credential.clone())
.await
.expect("Failed to save network");
// Record passive scan results, including the saved network and another network.
let seen_networks = vec![saved_seen_id.clone().into(), unsaved_id.clone().into()];
saved_networks.record_scan_result(ScanResultType::Undirected, seen_networks).await;
assert_variant!(saved_networks.lookup(saved_seen_id).await.as_slice(), [config] => {
assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
});
assert_variant!(saved_networks.lookup(saved_unseen_id).await.as_slice(), [config] => {
assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
});
}
#[test]
fn evict_if_needed_removes_unconnected() {
// this test is less meaningful when MAX_CONFIGS_PER_SSID is greater than 1, otherwise
// the only saved configs should be removed when the max capacity is met, regardless of
// whether it has been connected to.
let unconnected_config = network_config("foo", "password");
let mut connected_config = unconnected_config.clone();
connected_config.has_ever_connected = false;
let mut network_configs = vec![connected_config; MAX_CONFIGS_PER_SSID - 1];
network_configs.insert(MAX_CONFIGS_PER_SSID / 2, unconnected_config.clone());
assert_eq!(evict_if_needed(&mut network_configs), Some(unconnected_config));
assert_eq!(MAX_CONFIGS_PER_SSID - 1, network_configs.len());
// check that everything left has been connected to before, only one removed is
// the one that has never been connected to
for config in network_configs.iter() {
assert_eq!(true, config.has_ever_connected);
}
}
#[test]
fn evict_if_needed_already_has_space() {
let mut configs = vec![];
assert_eq!(evict_if_needed(&mut configs), None);
let expected_cfgs: Vec<NetworkConfig> = vec![];
assert_eq!(expected_cfgs, configs);
if MAX_CONFIGS_PER_SSID > 1 {
let mut configs = vec![network_config("foo", "password")];
assert_eq!(evict_if_needed(&mut configs), None);
// if MAX_CONFIGS_PER_SSID is 1, this wouldn't be true
assert_eq!(vec![network_config("foo", "password")], configs);
}
}
#[fasync::run_singlethreaded(test)]
async fn clear() {
let stash_id = "clear";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
// Expect the store to be constructed successfully even if the file doesn't
// exist yet
let network_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2);
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
saved_networks
.store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' failed");
assert!(path.exists());
assert_eq!(
vec![network_config("foo", "qwertyuio")],
saved_networks.lookup(network_id).await
);
assert_eq!(1, saved_networks.known_network_count().await);
saved_networks.clear().await.expect("clearing store failed");
assert_eq!(0, saved_networks.known_network_count().await);
// Load store from stash to verify it is also gone from persistent storage
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks manager");
assert_eq!(0, saved_networks.known_network_count().await);
}
#[fasync::run_singlethreaded(test)]
async fn ignore_legacy_file_bad_format() {
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let mut file = fs::File::create(&path).expect("failed to open file for writing");
// Write invalid JSON and close the file
file.write(b"{").expect("failed to write broken json into file");
mem::drop(file);
assert!(path.exists());
// Constructing a saved network config store should still succeed,
// but the invalid file should be gone now
let stash_id = "ignore_legacy_file_bad_format";
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// KnownEssStore deletes the file if it can't read it, as in this case.
assert!(!path.exists());
// Writing an entry should not create the file yet because networks configs don't persist.
assert_eq!(0, saved_networks.known_network_count().await);
let network_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2);
saved_networks
.store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' failed");
// There should be a file here again since we stored a network, so one will be created.
assert!(path.exists());
}
#[fasync::run_singlethreaded(test)]
async fn read_network_from_legacy_storage() {
// Possible contents of a file generated from KnownEssStore, with networks foo and bar with
// passwords foobar and password respecitively. Network foo should not be read into new
// saved network manager because the password is too short for a valid network password.
let contents = b"[{\"ssid\":[102,111,111],\"password\":[102,111,111,98,97,114]},
{\"ssid\":[98,97,114],\"password\":[112, 97, 115, 115, 119, 111, 114, 100]}]";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let mut file = fs::File::create(&path).expect("failed to open file for writing");
file.write(contents).expect("Failed to write to file");
file.flush().expect("failed to flush contents of file");
let stash_id = "read_network_from_legacy_storage";
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// We should not delete the file while creating SavedNetworksManager.
assert!(path.exists());
// Network bar should have been read into the saved networks manager because it is valid
assert_eq!(1, saved_networks.known_network_count().await);
let bar_id = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2);
let bar_config =
NetworkConfig::new(bar_id.clone(), Credential::Password(b"password".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![bar_config], saved_networks.lookup(bar_id).await);
// Network foo should not have been read into saved networks manager because it is invalid.
let foo_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::Wpa2);
assert!(saved_networks.lookup(foo_id).await.is_empty());
assert!(path.exists());
}
#[fasync::run_singlethreaded(test)]
async fn do_not_migrate_networks_twice() {
let contents = b"[{\"ssid\":[102,111,111],\"password\":[102,111,111,98,97,114]},
{\"ssid\":[98,97,114],\"password\":[112, 97, 115, 115, 119, 111, 114, 100]}]";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let mut file = fs::File::create(&path).expect("failed to open file for writing");
file.write(contents).expect("Failed to write to file");
file.flush().expect("failed to flush contents of file");
let stash_id = "do_not_migrate_networks_twice";
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// We should not have deleted the file while creating SavedNetworksManager.
assert!(path.exists());
// Verify the network config loaded from legacy storage
let net_id = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2);
let net_config =
NetworkConfig::new(net_id.clone(), Credential::Password(b"password".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![net_config], saved_networks.lookup(net_id.clone()).await);
// Replace the network 'bar' that was read from legacy version storage
saved_networks
.store(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()))
.await
.expect("failed to store network");
let new_net_config =
NetworkConfig::new(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![new_net_config.clone()], saved_networks.lookup(net_id.clone()).await);
// Recreate the SavedNetworksManager again, as would happen when the device restasts
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// We should not have deleted the file while creating SavedNetworksManager the first time.
assert!(path.exists());
// Expect to see the replaced network 'bar'
assert_eq!(1, saved_networks.known_network_count().await);
assert_eq!(vec![new_net_config], saved_networks.lookup(net_id).await);
}
#[fasync::run_singlethreaded(test)]
async fn ignore_legacy_if_stash_exists() {
let contents = b"[{\"ssid\":[102,111,111],\"password\":[102,111,111,98,97,114]},
{\"ssid\":[98,97,114],\"password\":[112, 97, 115, 115, 119, 111, 114, 100]}]";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let mut file = fs::File::create(&path).expect("failed to open file for writing");
file.write(contents).expect("Failed to write to file");
file.flush().expect("failed to flush contents of file");
let stash_id = "ignore_legacy_if_stash_exists";
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// We should not delete the file while creating SavedNetworksManager.
assert!(path.exists());
// Verify the network config loaded from legacy storage
let net_id = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2);
let net_config =
NetworkConfig::new(net_id.clone(), Credential::Password(b"password".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![net_config], saved_networks.lookup(net_id.clone()).await);
// Replace the network 'bar' that was read from legacy version storage
saved_networks
.store(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()))
.await
.expect("failed to store network");
let new_net_config =
NetworkConfig::new(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![new_net_config.clone()], saved_networks.lookup(net_id.clone()).await);
// Add legacy store file again as if we had failed to delete it
let mut file = fs::File::create(&path).expect("failed to open file for writing");
file.write(contents).expect("Failed to write to file");
file.flush().expect("failed to flush contents of file");
// Recreate the SavedNetworksManager again, as would happen when the device restasts
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// We should ignore the legacy file since there is something in the stash.
assert_eq!(1, saved_networks.known_network_count().await);
assert_eq!(vec![new_net_config], saved_networks.lookup(net_id).await);
}
#[fasync::run_singlethreaded(test)]
async fn write_and_load_legacy() {
let stash_id = "write_and_load_legacy";
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
// Save a network, which should write to the legacy store
let net_id = NetworkIdentifier::new(b"bar".to_vec(), SecurityType::Wpa2);
saved_networks
.store(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()))
.await
.expect("failed to store network");
// Explicitly clear just the stash
saved_networks.stash.lock().await.clear().await.expect("failed to clear the stash");
// Create the saved networks manager again to trigger reading from persistent storage
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("failed to create saved networks store");
// Verify that the network was read in from legacy store.
let net_config =
NetworkConfig::new(net_id.clone(), Credential::Password(b"foobarbaz".to_vec()), false)
.expect("failed to create network config");
assert_eq!(vec![net_config.clone()], saved_networks.lookup(net_id.clone()).await);
}
#[test]
fn test_store_waits_for_stash() {
let mut exec = fasync::Executor::new().expect("failed to create executor");
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join(rand_string());
let tmp_path = temp_dir.path().join(rand_string());
let (saved_networks, mut stash_server) =
exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server(path, tmp_path));
let network_id = NetworkIdentifier::new(b"foo".to_vec(), SecurityType::None);
let save_fut = saved_networks.store(network_id, Credential::None);
pin_mut!(save_fut);
// Verify that storing the network does not complete until stash responds.
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut stash_server.try_next()),
Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::SetValue { .. })))
);
assert_variant!(
exec.run_until_stalled(&mut stash_server.try_next()),
Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::Flush{responder}))) => {
responder.send(&mut Ok(())).expect("failed to send stash response");
}
);
assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None)));
}
/// Create a saved networks manager and clear the contents. Stash ID should be different for
/// each test so that they don't interfere.
async fn create_saved_networks(
stash_id: impl AsRef<str>,
path: impl AsRef<Path>,
tmp_path: impl AsRef<Path>,
) -> SavedNetworksManager {
let saved_networks = SavedNetworksManager::new_with_stash_or_paths(
stash_id,
&path,
&tmp_path,
create_mock_cobalt_sender(),
)
.await
.expect("Failed to create SavedNetworksManager");
saved_networks.clear().await.expect("Failed to clear new SavedNetworksManager");
saved_networks
}
/// Convience function for creating network configs with default values as they would be
/// initialized when read from KnownEssStore. Credential is password or none, and security
/// type is WPA2 or none.
fn network_config(ssid: impl Into<Vec<u8>>, password: impl Into<Vec<u8>>) -> NetworkConfig {
let credential = Credential::from_bytes(password.into());
let id = NetworkIdentifier::new(ssid.into(), credential.derived_security_type());
let has_ever_connected = false;
NetworkConfig::new(id, credential, has_ever_connected).unwrap()
}
fn rand_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(20).collect()
}
#[fasync::run_singlethreaded(test)]
async fn record_metrics_when_called_on_class() {
let stash_id = rand_string();
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let (cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let saved_networks =
SavedNetworksManager::new_with_stash_or_paths(&stash_id, &path, &tmp_path, cobalt_api)
.await
.unwrap();
let network_id_foo = NetworkIdentifier::new("foo", SecurityType::Wpa2);
let network_id_baz = NetworkIdentifier::new("baz", SecurityType::Wpa2);
assert!(saved_networks.lookup(network_id_foo.clone()).await.is_empty());
assert_eq!(0, saved_networks.known_network_count().await);
// Store a network and verify it was stored.
saved_networks
.store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
.await
.expect("storing 'foo' failed");
assert_eq!(1, saved_networks.known_network_count().await);
// Store another network and verify.
saved_networks
.store(network_id_baz.clone(), Credential::Psk(vec![1; 32]))
.await
.expect("storing 'baz' with PSK failed");
assert_eq!(2, saved_networks.known_network_count().await);
// Record metrics
saved_networks.record_periodic_metrics().await;
// Two saved networks
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_NETWORKS_METRIC_ID)
.with_event_code(
SavedNetworksMetricDimensionSavedNetworks::TwoToFour.as_event_code()
)
.as_event()
)
);
// One config for each network
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID)
.with_event_code(
SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::One
.as_event_code()
)
.as_event()
)
);
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID)
.with_event_code(
SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::One
.as_event_code()
)
.as_event()
)
);
// No more metrics
assert!(cobalt_events.try_next().is_err());
}
#[fasync::run_singlethreaded(test)]
async fn metrics_count_configs() {
let (mut cobalt_api, mut cobalt_events) = create_mock_cobalt_sender_and_receiver();
let network_id_foo = NetworkIdentifier::new("foo", SecurityType::Wpa2);
let network_id_baz = NetworkIdentifier::new("baz", SecurityType::Wpa2);
let networks: NetworkConfigMap = [
(network_id_foo, vec![]),
(
network_id_baz.clone(),
vec![
NetworkConfig::new(
network_id_baz.clone(),
Credential::Password(b"qwertyuio".to_vec()),
false,
)
.unwrap(),
NetworkConfig::new(
network_id_baz,
Credential::Password(b"asdfasdfasdf".to_vec()),
false,
)
.unwrap(),
],
),
]
.iter()
.cloned()
.collect();
log_cobalt_metrics(&networks, &mut cobalt_api);
// Two saved networks
assert_eq!(
cobalt_events.try_next().unwrap(),
Some(
CobaltEvent::builder(SAVED_NETWORKS_METRIC_ID)
.with_event_code(
SavedNetworksMetricDimensionSavedNetworks::TwoToFour.as_event_code()
)
.as_event()
)
);
// Extract the next two events, their order is not guaranteed
let cobalt_metrics = vec![
cobalt_events.try_next().unwrap().unwrap(),
cobalt_events.try_next().unwrap().unwrap(),
];
// Zero configs for one network
assert!(cobalt_metrics.iter().any(|metric| metric
== &CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID)
.with_event_code(
SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::Zero
.as_event_code()
)
.as_event()));
// Two configs for the other network
assert!(cobalt_metrics.iter().any(|metric| metric
== &CobaltEvent::builder(SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_METRIC_ID)
.with_event_code(
SavedConfigurationsForSavedNetworkMetricDimensionSavedConfigurations::TwoToFour
.as_event_code()
)
.as_event()));
// No more metrics
assert!(cobalt_events.try_next().is_err());
}
#[fasync::run_singlethreaded(test)]
async fn probabilistic_choosing_of_hidden_networks() {
// Create three networks with 1, 0, 0.5 hidden probability
let id_hidden =
types::NetworkIdentifier { ssid: b"hidden".to_vec(), type_: types::SecurityType::Wpa2 };
let mut net_config_hidden = NetworkConfig::new(
id_hidden.clone().into(),
Credential::Password(b"password".to_vec()),
false,
)
.expect("failed to create network config");
net_config_hidden.hidden_probability = 1.0;
let id_not_hidden = types::NetworkIdentifier {
ssid: b"not_hidden".to_vec(),
type_: types::SecurityType::Wpa2,
};
let mut net_config_not_hidden = NetworkConfig::new(
id_not_hidden.clone().into(),
Credential::Password(b"password".to_vec()),
false,
)
.expect("failed to create network config");
net_config_not_hidden.hidden_probability = 0.0;
let id_maybe_hidden = types::NetworkIdentifier {
ssid: b"maybe_hidden".to_vec(),
type_: types::SecurityType::Wpa2,
};
let mut net_config_maybe_hidden = NetworkConfig::new(
id_maybe_hidden.clone().into(),
Credential::Password(b"password".to_vec()),
false,
)
.expect("failed to create network config");
net_config_maybe_hidden.hidden_probability = 0.5;
let mut maybe_hidden_selection_count = 0;
let mut hidden_selection_count = 0;
// Run selection many times, to ensure the probability is working as expected.
for _ in 1..100 {
let selected_networks = select_subset_potentially_hidden_networks(vec![
net_config_hidden.clone(),
net_config_not_hidden.clone(),
net_config_maybe_hidden.clone(),
]);
// The 1.0 probability should always be picked
assert!(selected_networks.contains(&id_hidden));
// The 0 probability should never be picked
assert!(!selected_networks.contains(&id_not_hidden));
// Keep track of how often the networks were selected
if selected_networks.contains(&id_maybe_hidden) {
maybe_hidden_selection_count += 1;
}
if selected_networks.contains(&id_hidden) {
hidden_selection_count += 1;
}
}
// The 0.5 probability network should be picked at least once, but not every time. With 100
// runs, the chances of either of these assertions flaking is 1 / (0.5^100), i.e. 1 in 1e30.
// Even with a hypothetical 1,000,000 test runs per day, there would be an average of 1e24
// days between flakes due to this test.
assert!(maybe_hidden_selection_count > 0);
assert!(maybe_hidden_selection_count < hidden_selection_count);
}
#[fasync::run_singlethreaded(test)]
async fn test_record_not_seen_active_scan() {
// Test that if we update that we haven't seen a couple of networks in active scans, their
// hidden probability is updated.
let stash_id = rand_string();
let temp_dir = TempDir::new().expect("failed to create temporary directory");
let path = temp_dir.path().join("networks.json");
let tmp_path = temp_dir.path().join("tmp.json");
let saved_networks = create_saved_networks(stash_id, &path, &tmp_path).await;
// Seen in active scans
let id_1 = NetworkIdentifier::new("foo", SecurityType::Wpa);
let credential_1 = Credential::Password(b"some_password".to_vec());
let id_2 = NetworkIdentifier::new("bar", SecurityType::Wpa3);
let credential_2 = Credential::Password(b"another_password".to_vec());
// Seen in active scan but not saved
let id_3 = NetworkIdentifier::new("baz", SecurityType::None);
// Saved and targeted in active scan but not seen
let id_4 = NetworkIdentifier::new("foobar", SecurityType::None);
let credential_4 = Credential::None;
// Save 3 of the 4 networks
saved_networks.store(id_1.clone(), credential_1).await.expect("failed to store network");
saved_networks.store(id_2.clone(), credential_2).await.expect("failed to store network");
saved_networks.store(id_4.clone(), credential_4).await.expect("failed to store network");
// Check that the saved networks have the default hidden probability so later we can just
// check that the probability has changed.
let config_1 = saved_networks.lookup(id_1.clone()).await.pop().expect("failed to lookup");
assert_eq!(config_1.hidden_probability, PROB_HIDDEN_DEFAULT);
let config_2 = saved_networks.lookup(id_2.clone()).await.pop().expect("failed to lookup");
assert_eq!(config_2.hidden_probability, PROB_HIDDEN_DEFAULT);
let config_4 = saved_networks.lookup(id_4.clone()).await.pop().expect("failed to lookup");
assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
let seen_ids = vec![];
let not_seen_ids = vec![id_1.clone().into(), id_2.clone().into(), id_3.clone().into()];
saved_networks.record_scan_result(ScanResultType::Directed(not_seen_ids), seen_ids).await;
// Check that the configs' hidden probability has decreased
let config_1 = saved_networks.lookup(id_1).await.pop().expect("failed to lookup");
assert!(config_1.hidden_probability < PROB_HIDDEN_DEFAULT);
let config_2 = saved_networks.lookup(id_2).await.pop().expect("failed to lookup");
assert!(config_2.hidden_probability < PROB_HIDDEN_DEFAULT);
// Check that for the network that was target but not seen in the active scan, its hidden
// probability isn't lowered.
let config_4 = saved_networks.lookup(id_4.clone()).await.pop().expect("failed to lookup");
assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
// Check that a config was not saved for the identifier that was not saved before.
assert!(saved_networks.lookup(id_3).await.is_empty());
}
}