Skip to main content

wlancfg_lib/config_management/
config_manager.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::network_config::{
6    ConnectFailure, Credential, FailureReason, HIDDEN_PROBABILITY_HIGH, HiddenProbEvent,
7    NetworkConfig, NetworkConfigError, NetworkIdentifier, PastConnectionData, PastConnectionList,
8    SecurityType,
9};
10use super::stash_conversion::*;
11use crate::client::types::{self, ScanObservation};
12use crate::telemetry::{TelemetryEvent, TelemetrySender};
13use anyhow::format_err;
14use async_trait::async_trait;
15use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
16use fidl_fuchsia_wlan_sme as fidl_sme;
17use fuchsia_async as fasync;
18use futures::lock::Mutex;
19use log::{error, info};
20use std::collections::hash_map::Entry;
21use std::collections::{HashMap, HashSet};
22use wlan_storage::policy::{POLICY_STORAGE_ID, PolicyStorage};
23
24const MAX_CONFIGS_PER_SSID: usize = 1;
25
26/// The Saved Network Manager keeps track of saved networks and provides thread-safe access to
27/// saved networks. Networks are saved by NetworkConfig and accessed by their NetworkIdentifier
28/// (SSID and security protocol). Network configs are saved in-memory, and part of each network
29/// data is saved persistently. Futures aware locks are used in order to wait for the storage flush
30/// operations to complete when data changes.
31pub struct SavedNetworksManager {
32    saved_networks: Mutex<NetworkConfigMap>,
33    // Persistent storage for networks, which should be updated when there is a change to the data
34    // that is saved between reboots.
35    store: Mutex<PolicyStorage>,
36    telemetry_sender: TelemetrySender,
37}
38
39/// Save multiple network configs per SSID in able to store multiple connections with different
40/// credentials, for different authentication credentials on the same network or for different
41/// networks with the same name.
42type NetworkConfigMap = HashMap<NetworkIdentifier, Vec<NetworkConfig>>;
43
44#[async_trait(?Send)]
45pub trait SavedNetworksManagerApi {
46    /// Attempt to remove the NetworkConfig described by the specified NetworkIdentifier and
47    /// Credential. Return true if a NetworkConfig is remove and false otherwise.
48    async fn remove(
49        &self,
50        network_id: NetworkIdentifier,
51        credential: Credential,
52    ) -> Result<bool, NetworkConfigError>;
53
54    /// Get the count of networks in store, including multiple values with same SSID
55    async fn known_network_count(&self) -> usize;
56
57    /// Return a list of network configs that match the given SSID.
58    async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig>;
59
60    /// Return a list of network configs that could be used with the security type seen in a scan.
61    /// This includes configs that have a lower security type that can be upgraded to match the
62    /// provided detailed security type.
63    async fn lookup_compatible(
64        &self,
65        ssid: &types::Ssid,
66        scan_security: types::SecurityTypeDetailed,
67    ) -> Vec<NetworkConfig>;
68
69    /// Save a network by SSID and password. If the SSID and password have been saved together
70    /// before, do not modify the saved config. Update the legacy storage to keep it consistent
71    /// with what it did before the new version. If a network is pushed out because of the newly
72    /// saved network, this will return the removed config.
73    async fn store(
74        &self,
75        network_id: NetworkIdentifier,
76        credential: Credential,
77    ) -> Result<Option<NetworkConfig>, NetworkConfigError>;
78
79    /// Update the specified saved network with the result of an attempted connect.  If the
80    /// specified network could have been connected to with a different security type and we
81    /// do not find the specified config, we will check the other possible security type. For
82    /// example if a WPA3 network is specified, we will check WPA2 if it isn't found. If the
83    /// specified network is not saved, this function does not save it.
84    async fn record_connect_result(
85        &self,
86        id: NetworkIdentifier,
87        credential: &Credential,
88        bssid: types::Bssid,
89        connect_result: fidl_sme::ConnectResult,
90        scan_type: types::ScanObservation,
91    );
92
93    /// Record the disconnect from a network, to be used for things such as avoiding connections
94    /// that drop soon after starting.
95    async fn record_disconnect(
96        &self,
97        id: &NetworkIdentifier,
98        credential: &Credential,
99        data: PastConnectionData,
100    );
101
102    async fn record_periodic_metrics(&self);
103
104    /// Update hidden networks probabilities based on scan results. Record either results of a
105    /// passive scan or a directed active scan.
106    async fn record_scan_result(
107        &self,
108        target_ssids: Vec<types::Ssid>,
109        results: &HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>>,
110    );
111
112    async fn is_network_single_bss(
113        &self,
114        id: &NetworkIdentifier,
115        credential: &Credential,
116    ) -> Result<bool, anyhow::Error>;
117
118    // Return a list of every network config that has been saved.
119    async fn get_networks(&self) -> Vec<NetworkConfig>;
120
121    // Get the list of past connections for a specific BSS
122    async fn get_past_connections(
123        &self,
124        id: &NetworkIdentifier,
125        credential: &Credential,
126        bssid: &types::Bssid,
127    ) -> PastConnectionList;
128}
129
130impl SavedNetworksManager {
131    /// Initializes a new Saved Network Manager by reading saved networks from local storage using
132    /// a WLAN helper library. It will attempt to migrate any data from legacy storage.
133    pub async fn new(telemetry_sender: TelemetrySender) -> Self {
134        let storage = PolicyStorage::new_with_id(POLICY_STORAGE_ID).await;
135        Self::new_with_storage(storage, telemetry_sender).await
136    }
137
138    /// Load data from persistent storage. The legacy stash data is deleted if it exists.
139    pub async fn new_with_storage(
140        mut store: PolicyStorage,
141        telemetry_sender: TelemetrySender,
142    ) -> Self {
143        let mut saved_networks: HashMap<NetworkIdentifier, Vec<NetworkConfig>> = HashMap::new();
144        // Load saved networks from persistent storage. An error loading would mean that there was
145        // nothing saved in the current version of persistent store and there was an error loading
146        // legacy stash data.
147        let stored_networks = store.load().await.unwrap_or_else(|e| {
148            // If there is an error loading saved networks, we will run with no saved networks.
149            error!("No saved networks loaded; error loading saved networks from storage: {}", e);
150            Vec::new()
151        });
152        let mut errors_building_configs = HashSet::new();
153
154        // Collect the list of persisted networks into the map that will be used internally.
155        for persisted_data in stored_networks.into_iter() {
156            let id = NetworkIdentifier::new(
157                types::Ssid::from_bytes_unchecked(persisted_data.ssid),
158                persisted_data.security_type.into(),
159            );
160            let config = NetworkConfig::new(
161                id.clone(),
162                persisted_data.credential.clone().into(),
163                persisted_data.has_ever_connected,
164                persisted_data.hidden_probability,
165            );
166            match config {
167                Ok(config) => saved_networks.entry(id).or_default().push(config),
168                Err(e) => {
169                    _ = errors_building_configs.insert(e);
170                }
171            }
172        }
173
174        // If there errors creating network configs from persisted data, log unique types.
175        if !errors_building_configs.is_empty() {
176            error!(
177                "At least one error occurred building network config from persisted data: {:?}",
178                errors_building_configs
179            )
180        }
181
182        Self {
183            saved_networks: Mutex::new(saved_networks),
184            store: Mutex::new(store),
185            telemetry_sender,
186        }
187    }
188
189    /// Creates a new config with a random storage path, ensuring a clean environment for an
190    /// individual test
191    #[cfg(test)]
192    pub async fn new_for_test() -> Self {
193        use crate::util::testing::generate_string;
194        use futures::channel::mpsc;
195
196        let store_id = generate_string();
197        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
198        let telemetry_sender = TelemetrySender::new(telemetry_sender);
199        let store = PolicyStorage::new_with_id(&store_id).await;
200        Self::new_with_storage(store, telemetry_sender).await
201    }
202
203    /// Clear the in memory storage and the persistent storage.
204    #[cfg(test)]
205    pub async fn clear(&self) -> Result<(), anyhow::Error> {
206        self.saved_networks.lock().await.clear();
207        self.store.lock().await.clear()
208    }
209}
210
211#[async_trait(?Send)]
212impl SavedNetworksManagerApi for SavedNetworksManager {
213    async fn remove(
214        &self,
215        network_id: NetworkIdentifier,
216        credential: Credential,
217    ) -> Result<bool, NetworkConfigError> {
218        // Find any matching NetworkConfig and remove it.
219        let mut saved_networks = self.saved_networks.lock().await;
220        if let Some(network_configs) = saved_networks.get_mut(&network_id) {
221            let original_len = network_configs.len();
222            // Keep the configs that don't match provided NetworkIdentifier and Credential.
223            network_configs.retain(|cfg| cfg.credential != credential);
224            if original_len != network_configs.len() {
225                // If there was only one config with this ID before removing it, remove the ID.
226                if network_configs.is_empty() {
227                    _ = saved_networks.remove(&network_id);
228                }
229
230                // Update persistent storage
231                self.store
232                    .lock()
233                    .await
234                    .write(persistent_data_from_config_map(&saved_networks))
235                    .map_err(|e| {
236                        error!("error writing network to persistent storage: {}", e);
237                        NetworkConfigError::FileWriteError
238                    })?;
239
240                return Ok(true);
241            } else {
242                // Log whether there were any matching credential types without logging specific
243                // network data
244                let credential_types = network_configs
245                    .iter()
246                    .map(|nc| nc.credential.type_str())
247                    .collect::<HashSet<_>>();
248                if credential_types.contains(credential.type_str()) {
249                    info!("No matching network with the provided credential was found to remove.");
250                } else {
251                    info!(
252                        "No credential matching type {:?} found to remove for this network identifier. Help: found credential type(s): {:?}",
253                        credential.type_str(),
254                        credential_types
255                    );
256                }
257            }
258        } else {
259            // Check whether there is another network with the same SSID but different security
260            // type to remove.
261            let mut found_securities = SecurityType::list_variants();
262            found_securities.retain(|security| {
263                let id = NetworkIdentifier::new(network_id.ssid.clone(), *security);
264                saved_networks.contains_key(&id)
265            });
266            if found_securities.is_empty() {
267                info!("No network was found to remove with the provided SSID.");
268            } else {
269                info!(
270                    "No config to remove with security type {:?}. Help: found different config(s) for this SSID with security {:?}",
271                    network_id.security_type, found_securities
272                );
273            }
274        }
275        Ok(false)
276    }
277
278    /// Get the count of networks in store, including multiple values with same SSID
279    async fn known_network_count(&self) -> usize {
280        self.saved_networks.lock().await.values().flatten().count()
281    }
282
283    /// Return the network configs that have this network identifier. The configs may be different
284    /// because of their credentials. Note that these are copies of the current data, so if data
285    /// could have changed it should be looked up again. For example, data about roam scans change
286    /// throughout a connection so callers cannot keep using the same network config for that data
287    /// throughout the connection.
288    async fn lookup(&self, id: &NetworkIdentifier) -> Vec<NetworkConfig> {
289        self.saved_networks.lock().await.get(id).cloned().unwrap_or_default()
290    }
291
292    async fn lookup_compatible(
293        &self,
294        ssid: &types::Ssid,
295        scan_security: types::SecurityTypeDetailed,
296    ) -> Vec<NetworkConfig> {
297        let saved_networks_guard = self.saved_networks.lock().await;
298        let mut matching_configs = Vec::new();
299        for security in compatible_policy_securities(&scan_security) {
300            let id = NetworkIdentifier::new(ssid.clone(), security);
301            let saved_configs = saved_networks_guard.get(&id);
302            if let Some(configs) = saved_configs {
303                matching_configs.extend(
304                    configs
305                        .iter()
306                        // Check for conflicts; PSKs can't be used to connect to WPA3 networks.
307                        .filter(|config| security_is_compatible(&scan_security, &config.credential))
308                        .map(Clone::clone),
309                );
310            }
311        }
312        matching_configs
313    }
314
315    async fn store(
316        &self,
317        network_id: NetworkIdentifier,
318        credential: Credential,
319    ) -> Result<Option<NetworkConfig>, NetworkConfigError> {
320        let mut saved_networks = self.saved_networks.lock().await;
321        let network_entry = saved_networks.entry(network_id.clone());
322
323        if let Entry::Occupied(network_configs) = &network_entry
324            && network_configs.get().iter().any(|cfg| cfg.credential == credential)
325        {
326            info!("Saving a previously saved network with same password.");
327            return Ok(None);
328        }
329        let network_config =
330            NetworkConfig::new(network_id.clone(), credential.clone(), false, None)?;
331        let network_configs = network_entry.or_default();
332        let evicted_config = evict_if_needed(network_configs);
333        network_configs.push(network_config);
334
335        self.store.lock().await.write(persistent_data_from_config_map(&saved_networks)).map_err(
336            |e| {
337                error!("error writing network to persistent storage: {}", e);
338                NetworkConfigError::FileWriteError
339            },
340        )?;
341
342        Ok(evicted_config)
343    }
344
345    async fn record_connect_result(
346        &self,
347        id: NetworkIdentifier,
348        credential: &Credential,
349        bssid: types::Bssid,
350        connect_result: fidl_sme::ConnectResult,
351        scan_type: types::ScanObservation,
352    ) {
353        let mut saved_networks = self.saved_networks.lock().await;
354        let networks = match saved_networks.get_mut(&id) {
355            Some(networks) => networks,
356            None => {
357                error!("Failed to find network to record result of connect attempt.");
358                return;
359            }
360        };
361        for network in networks.iter_mut() {
362            if &network.credential == credential {
363                match (connect_result.code, connect_result.is_credential_rejected) {
364                    (fidl_ieee80211::StatusCode::Success, _) => {
365                        let mut has_change = false;
366                        let old_hidden_prob = network.hidden_probability;
367                        if !network.has_ever_connected {
368                            network.has_ever_connected = true;
369                            has_change = true;
370                        }
371                        // Update hidden network probabiltiy
372                        match scan_type {
373                            types::ScanObservation::Passive => {
374                                network.update_hidden_prob(HiddenProbEvent::ConnectPassive);
375                            }
376                            types::ScanObservation::Active => {
377                                network.update_hidden_prob(HiddenProbEvent::ConnectActive);
378                            }
379                            types::ScanObservation::Unknown => {}
380                        };
381
382                        if network.hidden_probability != old_hidden_prob {
383                            has_change = true;
384                        }
385
386                        if has_change {
387                            // Update persistent storage since a config has changed.
388                            let data = persistent_data_from_config_map(&saved_networks);
389                            if let Err(e) = self.store.lock().await.write(data) {
390                                info!("Failed to record successful connect in store: {}", e);
391                            }
392                        }
393                    }
394                    (fidl_ieee80211::StatusCode::Canceled, _) => {}
395                    (_, true) => {
396                        network.perf_stats.connect_failures.add(
397                            bssid,
398                            ConnectFailure {
399                                time: fasync::MonotonicInstant::now(),
400                                reason: FailureReason::CredentialRejected,
401                                bssid,
402                            },
403                        );
404                    }
405                    (_, _) => {
406                        network.perf_stats.connect_failures.add(
407                            bssid,
408                            ConnectFailure {
409                                time: fasync::MonotonicInstant::now(),
410                                reason: FailureReason::GeneralFailure,
411                                bssid,
412                            },
413                        );
414                    }
415                }
416                return;
417            }
418        }
419        // Will not reach here if we find the saved network with matching SSID and credential.
420        error!("Failed to find matching network to record result of connect attempt.");
421    }
422
423    async fn record_disconnect(
424        &self,
425        id: &NetworkIdentifier,
426        credential: &Credential,
427        data: PastConnectionData,
428    ) {
429        let bssid = data.bssid;
430        let mut saved_networks = self.saved_networks.lock().await;
431        let networks = match saved_networks.get_mut(id) {
432            Some(networks) => networks,
433            None => {
434                info!("Failed to find network to record disconnect stats");
435                return;
436            }
437        };
438        for network in networks.iter_mut() {
439            if &network.credential == credential {
440                network.perf_stats.past_connections.add(bssid, data);
441                return;
442            }
443        }
444    }
445
446    async fn record_periodic_metrics(&self) {
447        let saved_networks = self.saved_networks.lock().await;
448        // Count the number of configs for each saved network
449        let config_counts = saved_networks
450            .iter()
451            .map(|saved_network| {
452                let configs = saved_network.1;
453                configs.len()
454            })
455            .collect();
456        self.telemetry_sender.send(TelemetryEvent::SavedNetworkCount {
457            saved_network_count: saved_networks.len(),
458            config_count_per_saved_network: config_counts,
459        });
460    }
461
462    async fn record_scan_result(
463        &self,
464        target_ssids: Vec<types::Ssid>,
465        results: &HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>>,
466    ) {
467        let mut saved_networks = self.saved_networks.lock().await;
468        let mut has_change = false;
469
470        for (network, bss_list) in results {
471            // If there are BSSs seen with the same SSID but different security, it will be
472            // recorded as multi BSS. But this is fine since the network will just not get the
473            //  improvement to scan less.
474            let has_multiple_bss = bss_list.len() > 1;
475            // Determine if any BSSs seen for this network were observed passively.
476            if bss_list.iter().any(|bss| bss.observation == ScanObservation::Passive) {
477                // Look for compatible configs and record them as "SeenPassive" and with single
478                // or multi BSS data.
479                for security in compatible_policy_securities(&network.security_type) {
480                    let configs = match saved_networks
481                        .get_mut(&NetworkIdentifier::new(network.ssid.clone(), security))
482                    {
483                        Some(configs) => configs,
484                        None => continue,
485                    };
486                    // Check that the credential is compatible with the actual security type of
487                    // the scan result.
488                    let compatible_configs = configs.iter_mut().filter(|config| {
489                        security_is_compatible(&network.security_type, &config.credential)
490                    });
491                    for config in compatible_configs {
492                        let old_hidden_prob = config.hidden_probability;
493                        config.update_hidden_prob(HiddenProbEvent::SeenPassive);
494                        config.update_seen_multiple_bss(has_multiple_bss);
495                        if config.hidden_probability != old_hidden_prob {
496                            has_change = true;
497                        }
498                    }
499                }
500            }
501        }
502
503        // Update saved networks that match one of the targeted SSIDs but were *not* in scan results.
504        for (id, configs) in saved_networks.iter_mut() {
505            if !target_ssids.contains(&id.ssid) {
506                continue;
507            }
508            // For each config, check whether there is a scan result that
509            // could be used to connect. If not, update the hidden probability.
510            let potential_scan_results =
511                results.iter().filter(|(scan_id, _)| scan_id.ssid == id.ssid).collect::<Vec<_>>();
512            for config in configs {
513                if !potential_scan_results.iter().any(|(scan_id, _)| {
514                    compatible_policy_securities(&scan_id.security_type)
515                        .contains(&config.security_type)
516                        && security_is_compatible(&scan_id.security_type, &config.credential)
517                }) {
518                    let old_hidden_prob = config.hidden_probability;
519                    config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
520                    if config.hidden_probability != old_hidden_prob {
521                        has_change = true;
522                    }
523                }
524            }
525        }
526        if has_change {
527            let data = persistent_data_from_config_map(&saved_networks);
528            if let Err(e) = self.store.lock().await.write(data) {
529                info!("Failed to record scan result updates in store: {}", e);
530            }
531        }
532    }
533
534    /// Returns whether or not the network likely has only one BSS based on previous scans. This
535    /// should be used instead of the network config if the network config may have been updated.
536    /// For example, when making roam scan decisions this should be used instead of a network
537    /// config obtained at the time of connecting.
538    async fn is_network_single_bss(
539        &self,
540        id: &NetworkIdentifier,
541        credential: &Credential,
542    ) -> Result<bool, anyhow::Error> {
543        let saved_networks_guard = self.saved_networks.lock().await;
544        let possible_configs = saved_networks_guard.get(id).ok_or_else(|| {
545            format_err!(
546                "error checking if network is single BSS; no config with matching identifier"
547            )
548        })?;
549        let config =
550            possible_configs.iter().find(|c| &c.credential == credential).ok_or_else(|| {
551                format_err!(
552                    "error checking if network is single BSS; no config with matching credential"
553                )
554            })?;
555        return Ok(config.is_likely_single_bss());
556    }
557
558    async fn get_networks(&self) -> Vec<NetworkConfig> {
559        self.saved_networks.lock().await.values().flat_map(|cfgs| cfgs.clone()).collect()
560    }
561
562    async fn get_past_connections(
563        &self,
564        id: &NetworkIdentifier,
565        credential: &Credential,
566        bssid: &types::Bssid,
567    ) -> PastConnectionList {
568        self.saved_networks
569            .lock()
570            .await
571            .get(id)
572            .and_then(|configs| configs.iter().find(|config| &config.credential == credential))
573            .map(|config| config.perf_stats.past_connections.get_list_for_bss(bssid))
574            .unwrap_or_default()
575    }
576}
577
578/// Returns a subset of potentially hidden saved networks, filtering probabilistically based
579/// on how certain they are to be hidden.
580pub fn select_subset_potentially_hidden_networks(
581    saved_networks: Vec<NetworkConfig>,
582) -> Vec<types::NetworkIdentifier> {
583    saved_networks
584        .into_iter()
585        .filter(|saved_network| {
586            // Roll a dice to see if we should scan for it. The function gen_range(low..high)
587            // has an inclusive lower bound and exclusive upper bound, so using it as
588            // `hidden_probability > gen_range(0..1)` means that:
589            // - hidden_probability of 1 will _always_ be selected
590            // - hidden_probability of 0 will _never_ be selected
591            saved_network.hidden_probability > rand::random_range(0.0..1.0)
592        })
593        .map(|network| types::NetworkIdentifier {
594            ssid: network.ssid,
595            security_type: network.security_type,
596        })
597        .collect()
598}
599
600/// Returns all saved networks which we think have a high probability of being hidden.
601pub fn select_high_probability_hidden_networks(
602    saved_networks: Vec<NetworkConfig>,
603) -> Vec<types::NetworkIdentifier> {
604    saved_networks
605        .into_iter()
606        .filter(|saved_network| saved_network.hidden_probability >= HIDDEN_PROBABILITY_HIGH)
607        .map(|network| types::NetworkIdentifier {
608            ssid: network.ssid,
609            security_type: network.security_type,
610        })
611        .collect()
612}
613
614/// Gets compatible `SecurityType`s for network candidates.
615///
616/// This function returns a sequence of `SecurityType`s that may be used to connect to a network
617/// configured as described by the given `SecurityTypeDetailed`. If there is no compatible
618/// `SecurityType`, then the sequence will be empty.
619pub fn compatible_policy_securities(
620    detailed_security: &types::SecurityTypeDetailed,
621) -> Vec<SecurityType> {
622    use fidl_sme::Protection::*;
623    match detailed_security {
624        Wpa3Enterprise | Wpa3Personal | Wpa2Wpa3Personal => {
625            vec![SecurityType::Wpa2, SecurityType::Wpa3]
626        }
627        Wpa2Enterprise
628        | Wpa2Personal
629        | Wpa1Wpa2Personal
630        | Wpa2PersonalTkipOnly
631        | Wpa1Wpa2PersonalTkipOnly => vec![SecurityType::Wpa, SecurityType::Wpa2],
632        Wpa1 => vec![SecurityType::Wpa],
633        Wep => vec![SecurityType::Wep],
634        // TODO(https://fxbug.dev/462514157): Map Owe and OpenOweTransition to correct security types
635        Owe => vec![SecurityType::None],
636        OpenOweTransition => vec![SecurityType::None],
637        Open => vec![SecurityType::None],
638        Unknown => vec![],
639    }
640}
641
642pub fn security_is_compatible(
643    scan_security: &types::SecurityTypeDetailed,
644    credential: &Credential,
645) -> bool {
646    if (scan_security == &types::SecurityTypeDetailed::Wpa3Personal
647        || scan_security == &types::SecurityTypeDetailed::Wpa3Enterprise)
648        && let Credential::Psk(_) = credential
649    {
650        return false;
651    }
652    true
653}
654
655/// If the list of configs is at capacity for the number of saved configs per SSID,
656/// remove a saved network that has never been successfully connected to. If all have
657/// been successfully connected to, remove any. If a network config is evicted, that connection
658/// is forgotten for future connections.
659/// TODO(https://fxbug.dev/42117293) - when network configs record information about successful connections,
660/// use this to make a better decision what to forget if all networks have connected before.
661/// TODO(https://fxbug.dev/42117730) - make sure that we disconnect from the network if we evict a network config
662/// for a network we are currently connected to.
663fn evict_if_needed(configs: &mut Vec<NetworkConfig>) -> Option<NetworkConfig> {
664    if configs.len() < MAX_CONFIGS_PER_SSID {
665        return None;
666    }
667
668    for i in 0..configs.len() {
669        if let Some(config) = configs.get(i)
670            && !config.has_ever_connected
671        {
672            return Some(configs.remove(i));
673        }
674    }
675    // If all saved networks have connected, remove the first network
676    Some(configs.remove(0))
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::config_management::{
683        HistoricalListsByBssid, PROB_HIDDEN_DEFAULT, PROB_HIDDEN_IF_CONNECT_ACTIVE,
684        PROB_HIDDEN_IF_CONNECT_PASSIVE, PROB_HIDDEN_IF_SEEN_PASSIVE,
685    };
686    use crate::util::testing::{generate_random_bss, generate_string, random_connection_data};
687    use assert_matches::assert_matches;
688    use futures::channel::mpsc;
689    use futures::task::Poll;
690    use std::pin::pin;
691    use test_case::test_case;
692
693    #[fuchsia::test]
694    async fn store_and_lookup() {
695        let store_id = generate_string();
696        let saved_networks = create_saved_networks(&store_id).await;
697        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
698
699        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
700        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
701        assert_eq!(0, saved_networks.known_network_count().await);
702
703        // Store a network and verify it was stored.
704        assert!(
705            saved_networks
706                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
707                .await
708                .expect("storing 'foo' failed")
709                .is_none()
710        );
711        assert_eq!(
712            vec![network_config("foo", "qwertyuio")],
713            saved_networks.lookup(&network_id_foo).await
714        );
715        assert_eq!(1, saved_networks.known_network_count().await);
716
717        // Store another network with the same SSID.
718        let popped_network = saved_networks
719            .store(network_id_foo.clone(), Credential::Password(b"12345678".to_vec()))
720            .await
721            .expect("storing 'foo' a second time failed");
722        assert_eq!(popped_network, Some(network_config("foo", "qwertyuio")));
723
724        // There should only be one saved "foo" network because MAX_CONFIGS_PER_SSID is 1.
725        // When this constant becomes greater than 1, both network configs should be found
726        assert_eq!(
727            vec![network_config("foo", "12345678")],
728            saved_networks.lookup(&network_id_foo).await
729        );
730        assert_eq!(1, saved_networks.known_network_count().await);
731
732        // Store another network and verify.
733        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
734        let psk = Credential::Psk(vec![1; 32]);
735        let config_baz = NetworkConfig::new(network_id_baz.clone(), psk.clone(), false, None)
736            .expect("failed to create network config");
737        assert!(
738            saved_networks
739                .store(network_id_baz.clone(), psk)
740                .await
741                .expect("storing 'baz' with PSK failed")
742                .is_none()
743        );
744        assert_eq!(vec![config_baz.clone()], saved_networks.lookup(&network_id_baz).await);
745        assert_eq!(2, saved_networks.known_network_count().await);
746
747        // Saved networks should persist when we create a saved networks manager with the same ID.
748        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
749        let store = PolicyStorage::new_with_id(&store_id).await;
750
751        let saved_networks =
752            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
753                .await;
754        assert_eq!(
755            vec![network_config("foo", "12345678")],
756            saved_networks.lookup(&network_id_foo).await
757        );
758        assert_eq!(vec![config_baz], saved_networks.lookup(&network_id_baz).await);
759        assert_eq!(2, saved_networks.known_network_count().await);
760    }
761
762    #[fuchsia::test]
763    async fn store_twice() {
764        let saved_networks = SavedNetworksManager::new_for_test().await;
765        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
766
767        assert!(
768            saved_networks
769                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
770                .await
771                .expect("storing 'foo' failed")
772                .is_none()
773        );
774        let popped_network = saved_networks
775            .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
776            .await
777            .expect("storing 'foo' a second time failed");
778        // Because the same network was stored twice, nothing was evicted, so popped_network == None
779        assert_eq!(popped_network, None);
780        let expected_cfgs = vec![network_config("foo", "qwertyuio")];
781        assert_eq!(expected_cfgs, saved_networks.lookup(&network_id).await);
782        assert_eq!(1, saved_networks.known_network_count().await);
783    }
784
785    #[fuchsia::test]
786    async fn store_many_same_ssid() {
787        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
788        let saved_networks = SavedNetworksManager::new_for_test().await;
789
790        // save max + 1 networks with same SSID and different credentials
791        for i in 0..MAX_CONFIGS_PER_SSID + 1 {
792            let mut password = b"password".to_vec();
793            password.push(i as u8);
794            let popped_network = saved_networks
795                .store(network_id.clone(), Credential::Password(password))
796                .await
797                .expect("Failed to saved network");
798            if i >= MAX_CONFIGS_PER_SSID {
799                assert!(popped_network.is_some());
800            } else {
801                assert!(popped_network.is_none());
802            }
803        }
804
805        // since none have been connected to yet, we don't care which config was removed
806        assert_eq!(MAX_CONFIGS_PER_SSID, saved_networks.lookup(&network_id).await.len());
807    }
808
809    #[fuchsia::test]
810    async fn store_and_remove() {
811        let store_id = generate_string();
812        let saved_networks = create_saved_networks(&store_id).await;
813
814        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
815        let credential = Credential::Password(b"qwertyuio".to_vec());
816        assert!(saved_networks.lookup(&network_id).await.is_empty());
817        assert_eq!(0, saved_networks.known_network_count().await);
818
819        // Store a network and verify it was stored.
820        assert!(
821            saved_networks
822                .store(network_id.clone(), credential.clone())
823                .await
824                .expect("storing 'foo' failed")
825                .is_none()
826        );
827        assert_eq!(
828            vec![network_config("foo", "qwertyuio")],
829            saved_networks.lookup(&network_id).await
830        );
831        assert_eq!(1, saved_networks.known_network_count().await);
832
833        // Remove a network with the same NetworkIdentifier but differenct credential and verify
834        // that the saved network is unaffected.
835        assert!(
836            !saved_networks
837                .remove(network_id.clone(), Credential::Password(b"diff-password".to_vec()))
838                .await
839                .expect("removing 'foo' failed")
840        );
841        assert_eq!(1, saved_networks.known_network_count().await);
842
843        // Remove the network and check it is gone
844        assert!(
845            saved_networks
846                .remove(network_id.clone(), credential.clone())
847                .await
848                .expect("removing 'foo' failed")
849        );
850        assert_eq!(0, saved_networks.known_network_count().await);
851        // Check that the key in the saved networks manager's internal hashmap was removed.
852        assert!(saved_networks.saved_networks.lock().await.get(&network_id).is_none());
853
854        // If we try to remove the network again, we won't get an error and nothing happens
855        assert!(
856            !saved_networks
857                .remove(network_id.clone(), credential)
858                .await
859                .expect("removing 'foo' failed")
860        );
861
862        // Check that removal persists.
863        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
864        let store = PolicyStorage::new_with_id(&store_id).await;
865        let saved_networks =
866            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
867                .await;
868        assert_eq!(0, saved_networks.known_network_count().await);
869        assert!(saved_networks.lookup(&network_id).await.is_empty());
870    }
871
872    #[fuchsia::test]
873    fn sme_protection_converts_to_lower_compatible() {
874        use fidl_sme::Protection::*;
875        let lower_compatible_pairs = vec![
876            (Wpa3Enterprise, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
877            (Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
878            (Wpa2Wpa3Personal, vec![SecurityType::Wpa2, SecurityType::Wpa3]),
879            (Wpa2Enterprise, vec![SecurityType::Wpa, SecurityType::Wpa2]),
880            (Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
881            (Wpa1Wpa2Personal, vec![SecurityType::Wpa, SecurityType::Wpa2]),
882            (Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
883            (Wpa1Wpa2PersonalTkipOnly, vec![SecurityType::Wpa, SecurityType::Wpa2]),
884            (Wpa1, vec![SecurityType::Wpa]),
885            (Wep, vec![SecurityType::Wep]),
886            (Open, vec![SecurityType::None]),
887            (Unknown, vec![]),
888        ];
889        for (detailed_security, security) in lower_compatible_pairs {
890            assert_eq!(compatible_policy_securities(&detailed_security), security);
891        }
892    }
893
894    #[fuchsia::test]
895    async fn lookup_compatible_returns_both_compatible_configs() {
896        let saved_networks = SavedNetworksManager::new_for_test().await;
897        let ssid = types::Ssid::try_from("foo").unwrap();
898        let network_id_wpa2 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
899        let network_id_wpa3 = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
900        let credential_wpa2 = Credential::Password(b"password".to_vec());
901        let credential_wpa3 = Credential::Password(b"wpa3-password".to_vec());
902
903        // Check that lookup_compatible does not modify the SavedNetworksManager and returns an
904        // empty vector if there is no matching config.
905        let results = saved_networks
906            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
907            .await;
908        assert!(results.is_empty());
909        assert_eq!(saved_networks.known_network_count().await, 0);
910
911        // Store a couple of network configs that could both be use to connect to a WPA2/WPA3
912        // network.
913        assert!(
914            saved_networks
915                .store(network_id_wpa2.clone(), credential_wpa2.clone())
916                .await
917                .expect("Failed to store network")
918                .is_none()
919        );
920        assert!(
921            saved_networks
922                .store(network_id_wpa3.clone(), credential_wpa3.clone())
923                .await
924                .expect("Failed to store network")
925                .is_none()
926        );
927        // Store a network with the same SSID but a not-compatible security type.
928        let network_id_wep = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa);
929        assert!(
930            saved_networks
931                .store(network_id_wep.clone(), Credential::Password(b"abcdefgh".to_vec()))
932                .await
933                .expect("Failed to store network")
934                .is_none()
935        );
936
937        let results = saved_networks
938            .lookup_compatible(&ssid, types::SecurityTypeDetailed::Wpa2Wpa3Personal)
939            .await;
940        let expected_config_wpa2 =
941            NetworkConfig::new(network_id_wpa2, credential_wpa2, false, None)
942                .expect("Failed to create config");
943        let expected_config_wpa3 =
944            NetworkConfig::new(network_id_wpa3, credential_wpa3, false, None)
945                .expect("Failed to create config");
946        assert_eq!(results.len(), 2);
947        assert!(results.contains(&expected_config_wpa2));
948        assert!(results.contains(&expected_config_wpa3));
949    }
950
951    #[test_case(types::SecurityTypeDetailed::Wpa3Personal)]
952    #[test_case(types::SecurityTypeDetailed::Wpa3Enterprise)]
953    #[fuchsia::test(add_test_attr = false)]
954    fn lookup_compatible_does_not_return_wpa3_psk(
955        wpa3_detailed_security: types::SecurityTypeDetailed,
956    ) {
957        let mut exec = fasync::TestExecutor::new();
958        let saved_networks = exec.run_singlethreaded(SavedNetworksManager::new_for_test());
959
960        // Store a WPA3 config with a password that will match and a PSK config that won't match
961        // to a WPA3 network.
962        let ssid = types::Ssid::try_from("foo").unwrap();
963        let network_id_psk = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa2);
964        let network_id_password = NetworkIdentifier::new(ssid.clone(), SecurityType::Wpa3);
965        let credential_psk = Credential::Psk(vec![5; 32]);
966        let credential_password = Credential::Password(b"mypassword".to_vec());
967        assert!(
968            exec.run_singlethreaded(
969                saved_networks.store(network_id_psk.clone(), credential_psk.clone()),
970            )
971            .expect("Failed to store network")
972            .is_none()
973        );
974        assert!(
975            exec.run_singlethreaded(
976                saved_networks.store(network_id_password.clone(), credential_password.clone()),
977            )
978            .expect("Failed to store network")
979            .is_none()
980        );
981
982        // Only the WPA3 config with a credential should be returned.
983        let expected_config_wpa3 =
984            NetworkConfig::new(network_id_password, credential_password, false, None)
985                .expect("Failed to create configc");
986        let results = exec
987            .run_singlethreaded(saved_networks.lookup_compatible(&ssid, wpa3_detailed_security));
988        assert_eq!(results, vec![expected_config_wpa3]);
989    }
990
991    #[fuchsia::test]
992    async fn connect_network() {
993        let store_id = generate_string();
994
995        let saved_networks = create_saved_networks(&store_id).await;
996
997        let network_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
998        let credential = Credential::Password(b"password".to_vec());
999        let bssid = types::Bssid::from([4; 6]);
1000
1001        // If connect and network hasn't been saved, we should not save the network.
1002        saved_networks
1003            .record_connect_result(
1004                network_id.clone(),
1005                &credential,
1006                bssid,
1007                fake_successful_connect_result(),
1008                types::ScanObservation::Unknown,
1009            )
1010            .await;
1011        assert!(saved_networks.lookup(&network_id).await.is_empty());
1012        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1013        assert_eq!(0, saved_networks.known_network_count().await);
1014
1015        // Save the network and record a successful connection.
1016        assert!(
1017            saved_networks
1018                .store(network_id.clone(), credential.clone())
1019                .await
1020                .expect("Failed save network")
1021                .is_none()
1022        );
1023
1024        let config = network_config("bar", "password");
1025        assert_eq!(vec![config], saved_networks.lookup(&network_id).await);
1026
1027        saved_networks
1028            .record_connect_result(
1029                network_id.clone(),
1030                &credential,
1031                bssid,
1032                fake_successful_connect_result(),
1033                types::ScanObservation::Unknown,
1034            )
1035            .await;
1036
1037        // The network should be saved with the connection recorded. We should not have recorded
1038        // that the network was connected to passively or actively.
1039        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1040            assert!(config.has_ever_connected);
1041            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1042        });
1043
1044        saved_networks
1045            .record_connect_result(
1046                network_id.clone(),
1047                &credential,
1048                bssid,
1049                fake_successful_connect_result(),
1050                types::ScanObservation::Active,
1051            )
1052            .await;
1053        // We should now see that we connected to the network after an active scan.
1054        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1055            assert!(config.has_ever_connected);
1056            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1057        });
1058
1059        saved_networks
1060            .record_connect_result(
1061                network_id.clone(),
1062                &credential,
1063                bssid,
1064                fake_successful_connect_result(),
1065                types::ScanObservation::Passive,
1066            )
1067            .await;
1068        // The config should have a lower hidden probability after connecting after a passive scan.
1069        assert_matches!(saved_networks.lookup(&network_id).await.as_slice(), [config] => {
1070            assert!(config.has_ever_connected);
1071            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1072        });
1073
1074        // Check that recording the connect event updates the persisted data by loading the data.
1075        let store_reloaded = PolicyStorage::new_with_id(&store_id).await;
1076        let (telemetry_sender_reloaded, _) = mpsc::channel::<TelemetryEvent>(100);
1077        let saved_networks_reloaded = SavedNetworksManager::new_with_storage(
1078            store_reloaded,
1079            TelemetrySender::new(telemetry_sender_reloaded),
1080        )
1081        .await;
1082        assert_matches!(saved_networks_reloaded.lookup(&network_id).await.as_slice(), [config] => {
1083            assert!(config.has_ever_connected);
1084            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1085        });
1086    }
1087
1088    #[fuchsia::test]
1089    async fn test_record_connect_updates_one() {
1090        let saved_networks = SavedNetworksManager::new_for_test().await;
1091        let net_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1092        let net_id_also_valid = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
1093        let credential = Credential::Password(b"some_password".to_vec());
1094        let bssid = types::Bssid::from([2; 6]);
1095
1096        // Save the networks and record a successful connection.
1097        assert!(
1098            saved_networks
1099                .store(net_id.clone(), credential.clone())
1100                .await
1101                .expect("Failed save network")
1102                .is_none()
1103        );
1104        assert!(
1105            saved_networks
1106                .store(net_id_also_valid.clone(), credential.clone())
1107                .await
1108                .expect("Failed save network")
1109                .is_none()
1110        );
1111        saved_networks
1112            .record_connect_result(
1113                net_id.clone(),
1114                &credential,
1115                bssid,
1116                fake_successful_connect_result(),
1117                types::ScanObservation::Unknown,
1118            )
1119            .await;
1120
1121        assert_matches!(saved_networks.lookup(&net_id).await.as_slice(), [config] => {
1122            assert!(config.has_ever_connected);
1123        });
1124        // If the specified network identifier is found, record_conenct_result should not mark
1125        // another config even if it could also have been used for the connect attempt.
1126        assert_matches!(saved_networks.lookup(&net_id_also_valid).await.as_slice(), [config] => {
1127            assert!(!config.has_ever_connected);
1128        });
1129    }
1130
1131    #[fuchsia::test]
1132    async fn test_record_connect_failure() {
1133        let saved_networks = SavedNetworksManager::new_for_test().await;
1134        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1135        let credential = Credential::None;
1136        let bssid = types::Bssid::from([1; 6]);
1137        let before_recording = fasync::MonotonicInstant::now();
1138
1139        // Verify that recording connect result does not save the network.
1140        saved_networks
1141            .record_connect_result(
1142                network_id.clone(),
1143                &credential,
1144                bssid,
1145                fidl_sme::ConnectResult {
1146                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1147                    ..fake_successful_connect_result()
1148                },
1149                types::ScanObservation::Unknown,
1150            )
1151            .await;
1152        assert!(saved_networks.lookup(&network_id).await.is_empty());
1153        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1154        assert_eq!(0, saved_networks.known_network_count().await);
1155
1156        // Record that the connect failed.
1157        assert!(
1158            saved_networks
1159                .store(network_id.clone(), credential.clone())
1160                .await
1161                .expect("Failed save network")
1162                .is_none()
1163        );
1164        saved_networks
1165            .record_connect_result(
1166                network_id.clone(),
1167                &credential,
1168                bssid,
1169                fidl_sme::ConnectResult {
1170                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1171                    ..fake_successful_connect_result()
1172                },
1173                types::ScanObservation::Unknown,
1174            )
1175            .await;
1176        saved_networks
1177            .record_connect_result(
1178                network_id.clone(),
1179                &credential,
1180                bssid,
1181                fidl_sme::ConnectResult {
1182                    code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1183                    is_credential_rejected: true,
1184                    ..fake_successful_connect_result()
1185                },
1186                types::ScanObservation::Unknown,
1187            )
1188            .await;
1189
1190        // Check that the failures were recorded correctly.
1191        assert_eq!(1, saved_networks.known_network_count().await);
1192        let saved_config = saved_networks
1193            .lookup(&network_id)
1194            .await
1195            .pop()
1196            .expect("Failed to get saved network config");
1197        let connect_failures =
1198            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1199        assert_matches!(connect_failures, failures => {
1200            // There are 2 failures. One is a general failure and one rejected credentials failure.
1201            assert_eq!(failures.len(), 2);
1202            assert!(failures.iter().any(|failure| failure.reason == FailureReason::GeneralFailure));
1203            assert!(failures.iter().any(|failure| failure.reason == FailureReason::CredentialRejected));
1204            // Both failures have the correct BSSID
1205            for failure in failures.iter() {
1206                assert_eq!(failure.bssid, bssid);
1207                assert_eq!(failure.bssid, bssid);
1208            }
1209        });
1210    }
1211
1212    #[fuchsia::test]
1213    async fn test_record_connect_cancelled_ignored() {
1214        let saved_networks = SavedNetworksManager::new_for_test().await;
1215        let network_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1216        let credential = Credential::None;
1217        let bssid = types::Bssid::from([0; 6]);
1218        let before_recording = fasync::MonotonicInstant::now();
1219
1220        // Verify that recording connect result does not save the network.
1221        saved_networks
1222            .record_connect_result(
1223                network_id.clone(),
1224                &credential,
1225                bssid,
1226                fidl_sme::ConnectResult {
1227                    code: fidl_ieee80211::StatusCode::Canceled,
1228                    ..fake_successful_connect_result()
1229                },
1230                types::ScanObservation::Unknown,
1231            )
1232            .await;
1233        assert!(saved_networks.lookup(&network_id).await.is_empty());
1234        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1235        assert_eq!(0, saved_networks.known_network_count().await);
1236
1237        // Record that the connect was canceled.
1238        assert!(
1239            saved_networks
1240                .store(network_id.clone(), credential.clone())
1241                .await
1242                .expect("Failed save network")
1243                .is_none()
1244        );
1245        saved_networks
1246            .record_connect_result(
1247                network_id.clone(),
1248                &credential,
1249                bssid,
1250                fidl_sme::ConnectResult {
1251                    code: fidl_ieee80211::StatusCode::Canceled,
1252                    ..fake_successful_connect_result()
1253                },
1254                types::ScanObservation::Unknown,
1255            )
1256            .await;
1257
1258        // Check that there are no failures recorded for this saved network.
1259        assert_eq!(1, saved_networks.known_network_count().await);
1260        let saved_config = saved_networks
1261            .lookup(&network_id)
1262            .await
1263            .pop()
1264            .expect("Failed to get saved network config");
1265        let connect_failures =
1266            saved_config.perf_stats.connect_failures.get_recent_for_network(before_recording);
1267        assert_eq!(0, connect_failures.len());
1268    }
1269
1270    #[fuchsia::test]
1271    async fn test_record_disconnect() {
1272        let saved_networks = SavedNetworksManager::new_for_test().await;
1273        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1274        let credential = Credential::Psk(vec![1; 32]);
1275        let data = random_connection_data();
1276
1277        saved_networks.record_disconnect(&id, &credential, data).await;
1278        // Verify that nothing happens if the network was not already saved.
1279        assert_eq!(saved_networks.saved_networks.lock().await.len(), 0);
1280        assert_eq!(saved_networks.known_network_count().await, 0);
1281
1282        // Save the network and record a disconnect.
1283        assert!(
1284            saved_networks
1285                .store(id.clone(), credential.clone())
1286                .await
1287                .expect("Failed to save network")
1288                .is_none()
1289        );
1290        saved_networks.record_disconnect(&id, &credential, data).await;
1291
1292        // Check that a data was recorded about the connection that just ended.
1293        let recent_connections = saved_networks
1294            .lookup(&id)
1295            .await
1296            .pop()
1297            .expect("Failed to get saved network")
1298            .perf_stats
1299            .past_connections
1300            .get_recent_for_network(fasync::MonotonicInstant::INFINITE_PAST);
1301        assert_matches!(recent_connections.as_slice(), [connection_data] => {
1302            assert_eq!(connection_data, &data);
1303        })
1304    }
1305
1306    #[fuchsia::test]
1307    async fn test_record_undirected_scan() {
1308        let store_id = generate_string();
1309        let saved_networks = create_saved_networks(&store_id).await;
1310        let saved_seen_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1311        let saved_seen_network = types::NetworkIdentifierDetailed {
1312            ssid: saved_seen_id.ssid.clone(),
1313            security_type: types::SecurityTypeDetailed::Open,
1314        };
1315        let unsaved_id = NetworkIdentifier::try_from("bar", SecurityType::Wpa2).unwrap();
1316        let unsaved_network = types::NetworkIdentifierDetailed {
1317            ssid: unsaved_id.ssid.clone(),
1318            security_type: types::SecurityTypeDetailed::Wpa2Personal,
1319        };
1320        let saved_unseen_id = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1321        let seen_credential = Credential::None;
1322        let unseen_credential = Credential::Password(b"password".to_vec());
1323
1324        // Save the networks
1325        assert!(
1326            saved_networks
1327                .store(saved_seen_id.clone(), seen_credential.clone())
1328                .await
1329                .expect("Failed to save network")
1330                .is_none()
1331        );
1332        assert!(
1333            saved_networks
1334                .store(saved_unseen_id.clone(), unseen_credential.clone())
1335                .await
1336                .expect("Failed to save network")
1337                .is_none()
1338        );
1339
1340        // Record passive scan results, including the saved network and another network.
1341        let results: HashMap<types::NetworkIdentifierDetailed, Vec<types::Bss>> = HashMap::from([
1342            (
1343                saved_seen_network,
1344                vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1345            ),
1346            (unsaved_network, vec![generate_random_bss()]),
1347        ]);
1348
1349        saved_networks
1350            .record_scan_result(vec!["some_other_ssid".try_into().unwrap()], &results)
1351            .await;
1352
1353        assert_matches!(saved_networks.lookup(&saved_seen_id).await.as_slice(), [config] => {
1354            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1355        });
1356        assert_matches!(saved_networks.lookup(&saved_unseen_id).await.as_slice(), [config] => {
1357            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1358        });
1359
1360        // Re-open storage from store_id to verify the new hidden_probability persisted across reload.
1361        let store_reloaded = PolicyStorage::new_with_id(&store_id).await;
1362        let (telemetry_sender_reloaded, _) = mpsc::channel::<TelemetryEvent>(100);
1363        let saved_networks_reloaded = SavedNetworksManager::new_with_storage(
1364            store_reloaded,
1365            TelemetrySender::new(telemetry_sender_reloaded),
1366        )
1367        .await;
1368        assert_matches!(saved_networks_reloaded.lookup(&saved_seen_id).await.as_slice(), [config] => {
1369            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1370        });
1371        assert_matches!(saved_networks_reloaded.lookup(&saved_unseen_id).await.as_slice(), [config] => {
1372            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1373        });
1374    }
1375
1376    #[fuchsia::test]
1377    async fn test_record_undirected_scan_with_upgraded_security() {
1378        // Test that if we see a different compatible (higher) scan result for a saved network that
1379        // could be used to connect, recording the scan results will change the hidden probability.
1380        let saved_networks = SavedNetworksManager::new_for_test().await;
1381        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1382        let credential = Credential::Password(b"credential".to_vec());
1383
1384        // Save the networks
1385        assert!(
1386            saved_networks
1387                .store(id.clone(), credential.clone())
1388                .await
1389                .expect("Failed to save network")
1390                .is_none()
1391        );
1392
1393        // Record passive scan results
1394        let results = HashMap::from([(
1395            types::NetworkIdentifierDetailed {
1396                ssid: id.ssid.clone(),
1397                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1398            },
1399            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1400        )]);
1401        saved_networks.record_scan_result(vec![], &results).await;
1402        // The network was seen in a passive scan, so hidden probability should be updated.
1403        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1404            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1405        });
1406    }
1407
1408    #[fuchsia::test]
1409    async fn test_record_undirected_scan_incompatible_credential() {
1410        // Test that if we see a different compatible (higher) scan result for a saved network that
1411        // could be used to connect, recording the scan results will change the hidden probability.
1412        let saved_networks = SavedNetworksManager::new_for_test().await;
1413        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa2).unwrap();
1414        let credential = Credential::Psk(vec![8; 32]);
1415
1416        // Save the networks
1417        assert!(
1418            saved_networks
1419                .store(id.clone(), credential.clone())
1420                .await
1421                .expect("Failed to save network")
1422                .is_none()
1423        );
1424
1425        // Record passive scan results, including the saved network and another network.
1426        let results = HashMap::from([(
1427            types::NetworkIdentifierDetailed {
1428                ssid: id.ssid.clone(),
1429                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1430            },
1431            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1432        )]);
1433        saved_networks.record_scan_result(vec![], &results).await;
1434        // The network in the passive scan results was not compatible, so hidden probability should
1435        // not have been updated.
1436        assert_matches!(saved_networks.lookup(&id).await.as_slice(), [config] => {
1437            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1438        });
1439    }
1440
1441    #[fuchsia::test]
1442    async fn test_record_directed_scan_for_upgraded_security() {
1443        // Test that if we see a different compatible (higher) scan result for a saved network that
1444        // could be used to connect in a directed scan, the hidden probability will not be lowered.
1445        let saved_networks = SavedNetworksManager::new_for_test().await;
1446        let id = NetworkIdentifier::try_from("foobar", SecurityType::Wpa).unwrap();
1447        let credential = Credential::Password(b"credential".to_vec());
1448
1449        // Save the networks
1450        assert!(
1451            saved_networks
1452                .store(id.clone(), credential.clone())
1453                .await
1454                .expect("Failed to save network")
1455                .is_none()
1456        );
1457        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1458        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1459
1460        // Record directed scan results. The config's probability hidden should not be lowered
1461        // since we did not fail to see it in a directed scan.
1462        let results = HashMap::from([(
1463            types::NetworkIdentifierDetailed {
1464                ssid: id.ssid.clone(),
1465                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1466            },
1467            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1468        )]);
1469        let target = vec![id.ssid.clone()];
1470        saved_networks.record_scan_result(target, &results).await;
1471
1472        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1473        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1474    }
1475
1476    #[fuchsia::test]
1477    async fn test_record_directed_scan_for_incompatible_credential() {
1478        // Test that if we see a network that is not compatible because of the saved credential
1479        // (but is otherwise compatible), the directed scan is not considered successful and the
1480        // hidden probability of the config is lowered.
1481        let saved_networks = SavedNetworksManager::new_for_test().await;
1482        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1483        let credential = Credential::Psk(vec![11; 32]);
1484
1485        // Save the networks
1486        assert!(
1487            saved_networks
1488                .store(id.clone(), credential.clone())
1489                .await
1490                .expect("Failed to save network")
1491                .is_none()
1492        );
1493        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1494        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1495
1496        // Record directed scan results. The seen network does not match the saved network even
1497        // though security is compatible, since the security type is not compatible with the PSK.
1498        let target = vec![id.ssid.clone()];
1499        let results = HashMap::from([(
1500            types::NetworkIdentifierDetailed {
1501                ssid: id.ssid.clone(),
1502                security_type: types::SecurityTypeDetailed::Wpa3Personal,
1503            },
1504            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1505        )]);
1506        saved_networks.record_scan_result(target, &results).await;
1507        // The hidden probability should have been lowered because a directed scan failed to find
1508        // the network.
1509        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1510        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1511    }
1512
1513    #[fuchsia::test]
1514    async fn test_record_directed_scan_no_ssid_match() {
1515        // Test that recording directed active scan results does not mistakenly match a config with
1516        // a network with a different SSID.
1517
1518        let saved_networks = SavedNetworksManager::new_for_test().await;
1519        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1520        let credential = Credential::Psk(vec![11; 32]);
1521        let diff_ssid = types::Ssid::try_from("other-ssid").unwrap();
1522
1523        // Save the networks
1524        assert!(
1525            saved_networks
1526                .store(id.clone(), credential.clone())
1527                .await
1528                .expect("Failed to save network")
1529                .is_none()
1530        );
1531        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1532        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1533
1534        // Record directed scan results. We target the saved network but see a different one.
1535        let target = vec![id.ssid.clone()];
1536        let results = HashMap::from([(
1537            types::NetworkIdentifierDetailed {
1538                ssid: diff_ssid,
1539                security_type: types::SecurityTypeDetailed::Wpa2Personal,
1540            },
1541            vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1542        )]);
1543        saved_networks.record_scan_result(target, &results).await;
1544
1545        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1546        assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1547    }
1548
1549    #[fuchsia::test]
1550    async fn test_record_directed_one_not_compatible_one_compatible() {
1551        // Test that if we see two networks with the same SSID but only one is compatible, the scan
1552        // is recorded as successful for the config. In other words it isn't mistakenly recorded as
1553        // a failure because of the config that isn't compatible.
1554        let saved_networks = SavedNetworksManager::new_for_test().await;
1555        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1556        let credential = Credential::Password(b"foo-pass".to_vec());
1557
1558        // Save the networks
1559        assert!(
1560            saved_networks
1561                .store(id.clone(), credential.clone())
1562                .await
1563                .expect("Failed to save network")
1564                .is_none()
1565        );
1566        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1567        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1568
1569        // Record directed scan results. We see one network with the same SSID that doesn't match,
1570        // and one that does match.
1571        let target = vec![id.ssid.clone()];
1572        let results = HashMap::from([
1573            (
1574                types::NetworkIdentifierDetailed {
1575                    ssid: id.ssid.clone(),
1576                    security_type: types::SecurityTypeDetailed::Wpa1,
1577                },
1578                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1579            ),
1580            (
1581                types::NetworkIdentifierDetailed {
1582                    ssid: id.ssid.clone(),
1583                    security_type: types::SecurityTypeDetailed::Wpa2Personal,
1584                },
1585                vec![types::Bss { observation: ScanObservation::Active, ..generate_random_bss() }],
1586            ),
1587        ]);
1588        saved_networks.record_scan_result(target, &results).await;
1589        // Since the directed scan found a matching network, the hidden probability should not
1590        // have been lowered.
1591        let config = saved_networks.lookup(&id).await.pop().expect("failed to lookup config");
1592        assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1593    }
1594
1595    #[fuchsia::test]
1596    async fn test_record_both_directed_and_undirected() {
1597        let saved_networks = SavedNetworksManager::new_for_test().await;
1598        let saved_undirected_id = NetworkIdentifier::try_from("foo", SecurityType::None).unwrap();
1599        let saved_undirected_network = types::NetworkIdentifierDetailed {
1600            ssid: saved_undirected_id.ssid.clone(),
1601            security_type: types::SecurityTypeDetailed::Open,
1602        };
1603        let saved_directed_id = NetworkIdentifier::try_from("bar", SecurityType::None).unwrap();
1604        let credential = Credential::None;
1605
1606        // Save the networks
1607        assert!(
1608            saved_networks
1609                .store(saved_undirected_id.clone(), credential.clone())
1610                .await
1611                .expect("Failed to save network")
1612                .is_none()
1613        );
1614        assert!(
1615            saved_networks
1616                .store(saved_directed_id.clone(), credential.clone())
1617                .await
1618                .expect("Failed to save network")
1619                .is_none()
1620        );
1621
1622        // Verify assumption
1623        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1624            assert_eq!(config.hidden_probability, PROB_HIDDEN_DEFAULT);
1625        });
1626
1627        // Record scan results
1628        let results = HashMap::from([(
1629            saved_undirected_network,
1630            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
1631        )]);
1632        saved_networks.record_scan_result(vec![saved_directed_id.ssid.clone()], &results).await;
1633
1634        // The undirected (but seen) network is modified
1635        assert_matches!(saved_networks.lookup(&saved_undirected_id).await.as_slice(), [config] => {
1636            assert_eq!(config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1637        });
1638        // The directed (but *not* seen) network is modified
1639        assert_matches!(saved_networks.lookup(&saved_directed_id).await.as_slice(), [config] => {
1640            assert!(config.hidden_probability < PROB_HIDDEN_DEFAULT);
1641        });
1642    }
1643
1644    #[fuchsia::test]
1645    fn evict_if_needed_removes_unconnected() {
1646        // this test is less meaningful when MAX_CONFIGS_PER_SSID is greater than 1, otherwise
1647        // the only saved configs should be removed when the max capacity is met, regardless of
1648        // whether it has been connected to.
1649        let unconnected_config = network_config("foo", "password");
1650        let mut connected_config = unconnected_config.clone();
1651        connected_config.has_ever_connected = false;
1652        let mut network_configs = vec![connected_config; MAX_CONFIGS_PER_SSID - 1];
1653        network_configs.insert(MAX_CONFIGS_PER_SSID / 2, unconnected_config.clone());
1654
1655        assert_eq!(evict_if_needed(&mut network_configs), Some(unconnected_config));
1656        assert_eq!(MAX_CONFIGS_PER_SSID - 1, network_configs.len());
1657        // check that everything left has been connected to before, only one removed is
1658        // the one that has never been connected to
1659        for config in network_configs.iter() {
1660            assert!(config.has_ever_connected);
1661        }
1662    }
1663
1664    #[fuchsia::test]
1665    fn evict_if_needed_already_has_space() {
1666        let mut configs = vec![];
1667        assert_eq!(evict_if_needed(&mut configs), None);
1668        let expected_cfgs: Vec<NetworkConfig> = vec![];
1669        assert_eq!(expected_cfgs, configs);
1670
1671        if MAX_CONFIGS_PER_SSID > 1 {
1672            let mut configs = vec![network_config("foo", "password")];
1673            assert_eq!(evict_if_needed(&mut configs), None);
1674            // if MAX_CONFIGS_PER_SSID is 1, this wouldn't be true
1675            assert_eq!(vec![network_config("foo", "password")], configs);
1676        }
1677    }
1678
1679    #[fuchsia::test]
1680    async fn clear() {
1681        let store_id = "clear";
1682        let network_id = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1683        let saved_networks = create_saved_networks(store_id).await;
1684
1685        assert!(
1686            saved_networks
1687                .store(network_id.clone(), Credential::Password(b"qwertyuio".to_vec()))
1688                .await
1689                .expect("storing 'foo' failed")
1690                .is_none()
1691        );
1692        assert_eq!(
1693            vec![network_config("foo", "qwertyuio")],
1694            saved_networks.lookup(&network_id).await
1695        );
1696        assert_eq!(1, saved_networks.known_network_count().await);
1697
1698        saved_networks.clear().await.expect("failed to clear saved networks");
1699        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1700        assert_eq!(0, saved_networks.known_network_count().await);
1701
1702        // Load store from storage to verify it is also gone from persistent storage
1703        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1704        let store = PolicyStorage::new_with_id(store_id).await;
1705        let saved_networks =
1706            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1707                .await;
1708
1709        assert_eq!(0, saved_networks.known_network_count().await);
1710    }
1711
1712    impl std::fmt::Debug for SavedNetworksManager {
1713        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1714            f.debug_struct("SavedNetworksManager")
1715                .field("saved_networks", &self.saved_networks)
1716                .finish()
1717        }
1718    }
1719
1720    #[fuchsia::test]
1721    fn test_store_errors_cause_write_errors() {
1722        use fidl::endpoints::create_request_stream;
1723        use fidl_fuchsia_stash as fidl_stash;
1724        use futures::StreamExt;
1725        use std::sync::Arc;
1726        use std::sync::atomic::{AtomicBool, Ordering};
1727
1728        // Use a path for the persistent store that will cause write errors.
1729        let store_path_str = "/////";
1730        let mut exec = fasync::TestExecutor::new();
1731
1732        // Initialize stash proxies such that SavedNetworksManager initialize doesn't wait on
1733        // and doesn't load anything from the legacy stash.
1734        let (stash_client, mut request_stream) =
1735            create_request_stream::<fidl_stash::SecureStoreMarker>();
1736
1737        let read_from_stash = Arc::new(AtomicBool::new(false));
1738
1739        let _task = {
1740            let read_from_stash = read_from_stash.clone();
1741            fasync::Task::local(async move {
1742                while let Some(request) = request_stream.next().await {
1743                    match request.unwrap() {
1744                        fidl_stash::SecureStoreRequest::Identify { .. } => {}
1745                        fidl_stash::SecureStoreRequest::CreateAccessor {
1746                            accessor_request, ..
1747                        } => {
1748                            let read_from_stash = read_from_stash.clone();
1749                            fuchsia_async::EHandle::local().spawn_detached(async move {
1750                                let mut request_stream = accessor_request.into_stream();
1751                                while let Some(request) = request_stream.next().await {
1752                                    match request.unwrap() {
1753                                        fidl_stash::StoreAccessorRequest::ListPrefix { .. } => {
1754                                            read_from_stash.store(true, Ordering::Relaxed);
1755                                            // If we just drop the iterator, it should trigger a
1756                                            // read error.
1757                                        }
1758                                        _ => unreachable!(),
1759                                    }
1760                                }
1761                            });
1762                        }
1763                    }
1764                }
1765            })
1766        };
1767
1768        // Use a persistent store with the invalid file name and legacy stash which returns errors.
1769        let store =
1770            PolicyStorage::new_with_stash_proxy_and_id(stash_client.into_proxy(), store_path_str);
1771
1772        // Initialize the saved networks manager with the file that should cause write errors, the
1773        // legacy stash that will load nothing, and empty legacy known ess store file.
1774        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1775        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1776        let init_fut = SavedNetworksManager::new_with_storage(store, telemetry_sender);
1777        let mut init_fut = pin!(init_fut);
1778        let saved_networks = assert_matches!(exec.run_until_stalled(&mut init_fut), Poll::Ready(snm) => {
1779            snm
1780        });
1781
1782        // Save and remove networks and check that we get storage write errors
1783        let ssid = "foo";
1784        let credential = Credential::None;
1785        let network_id = NetworkIdentifier::try_from(ssid, SecurityType::None).unwrap();
1786        let save_fut = saved_networks.store(network_id.clone(), credential);
1787        let mut save_fut = pin!(save_fut);
1788
1789        assert_matches!(
1790            exec.run_until_stalled(&mut save_fut),
1791            Poll::Ready(Err(NetworkConfigError::FileWriteError))
1792        );
1793
1794        // The network should have been saved temporarily even if saving the network gives an error.
1795        assert_matches!(exec.run_until_stalled(&mut saved_networks.lookup(&network_id)), Poll::Ready(configs) => {
1796            assert_eq!(configs, vec![network_config(ssid, "")]);
1797        });
1798        assert_matches!(exec.run_until_stalled(&mut saved_networks.known_network_count()), Poll::Ready(count) => {
1799            assert_eq!(count, 1);
1800        });
1801    }
1802
1803    /// Create a saved networks manager and clear the contents. Storage ID should be different for
1804    /// each test so that they don't interfere.
1805    async fn create_saved_networks(store_id: &str) -> SavedNetworksManager {
1806        let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1807        let store = PolicyStorage::new_with_id(store_id).await;
1808        let saved_networks =
1809            SavedNetworksManager::new_with_storage(store, TelemetrySender::new(telemetry_sender))
1810                .await;
1811        saved_networks.clear().await.expect("failed to clear saved networks");
1812        saved_networks
1813    }
1814
1815    /// Convience function for creating network configs with default values as they would be
1816    /// initialized when read from KnownEssStore. Credential is password or none, and security
1817    /// type is WPA2 or none.
1818    fn network_config(ssid: &str, password: impl Into<Vec<u8>>) -> NetworkConfig {
1819        let credential = Credential::from_bytes(password.into());
1820        let id = NetworkIdentifier::try_from(ssid, credential.derived_security_type()).unwrap();
1821        let has_ever_connected = false;
1822        NetworkConfig::new(id, credential, has_ever_connected, None).unwrap()
1823    }
1824
1825    #[fuchsia::test]
1826    async fn record_metrics_when_called_on_class() {
1827        let store_id = generate_string();
1828        let (telemetry_sender, mut telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
1829        let telemetry_sender = TelemetrySender::new(telemetry_sender);
1830        let store = PolicyStorage::new_with_id(&store_id).await;
1831
1832        let saved_networks = SavedNetworksManager::new_with_storage(store, telemetry_sender).await;
1833        let network_id_foo = NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap();
1834        let network_id_baz = NetworkIdentifier::try_from("baz", SecurityType::Wpa2).unwrap();
1835
1836        assert!(saved_networks.lookup(&network_id_foo).await.is_empty());
1837        assert_eq!(0, saved_networks.saved_networks.lock().await.len());
1838        assert_eq!(0, saved_networks.known_network_count().await);
1839
1840        // Store a network and verify it was stored.
1841        assert!(
1842            saved_networks
1843                .store(network_id_foo.clone(), Credential::Password(b"qwertyuio".to_vec()))
1844                .await
1845                .expect("storing 'foo' failed")
1846                .is_none()
1847        );
1848        assert_eq!(1, saved_networks.known_network_count().await);
1849
1850        // Store another network and verify.
1851        assert!(
1852            saved_networks
1853                .store(network_id_baz.clone(), Credential::Psk(vec![1; 32]))
1854                .await
1855                .expect("storing 'baz' with PSK failed")
1856                .is_none()
1857        );
1858        assert_eq!(2, saved_networks.known_network_count().await);
1859
1860        // Record metrics
1861        saved_networks.record_periodic_metrics().await;
1862
1863        // Verify metric is logged with two saved networks, which each have one config
1864        assert_matches!(telemetry_receiver.try_next(), Ok(Some(TelemetryEvent::SavedNetworkCount { saved_network_count, config_count_per_saved_network })) => {
1865            assert_eq!(saved_network_count, 2);
1866            assert_eq!(config_count_per_saved_network, [1, 1]);
1867        });
1868    }
1869
1870    #[fuchsia::test]
1871    async fn probabilistic_choosing_of_hidden_networks() {
1872        // Create three networks with 1, 0, 0.5 hidden probability
1873        let id_hidden = types::NetworkIdentifier {
1874            ssid: types::Ssid::try_from("hidden").unwrap(),
1875            security_type: types::SecurityType::Wpa2,
1876        };
1877        let net_config_hidden = NetworkConfig::new(
1878            id_hidden.clone(),
1879            Credential::Password(b"password".to_vec()),
1880            false,
1881            Some(1.0),
1882        )
1883        .expect("failed to create network config");
1884
1885        let id_not_hidden = types::NetworkIdentifier {
1886            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1887            security_type: types::SecurityType::Wpa2,
1888        };
1889        let net_config_not_hidden = NetworkConfig::new(
1890            id_not_hidden.clone(),
1891            Credential::Password(b"password".to_vec()),
1892            false,
1893            Some(0.0),
1894        )
1895        .expect("failed to create network config");
1896
1897        let id_maybe_hidden = types::NetworkIdentifier {
1898            ssid: types::Ssid::try_from("maybe_hidden").unwrap(),
1899            security_type: types::SecurityType::Wpa2,
1900        };
1901        let net_config_maybe_hidden = NetworkConfig::new(
1902            id_maybe_hidden.clone(),
1903            Credential::Password(b"password".to_vec()),
1904            false,
1905            Some(0.5),
1906        )
1907        .expect("failed to create network config");
1908
1909        let mut maybe_hidden_selection_count = 0;
1910        let mut hidden_selection_count = 0;
1911
1912        // Run selection many times, to ensure the probability is working as expected.
1913        for _ in 1..100 {
1914            let selected_networks = select_subset_potentially_hidden_networks(vec![
1915                net_config_hidden.clone(),
1916                net_config_not_hidden.clone(),
1917                net_config_maybe_hidden.clone(),
1918            ]);
1919            // The 1.0 probability should always be picked
1920            assert!(selected_networks.contains(&id_hidden));
1921            // The 0 probability should never be picked
1922            assert!(!selected_networks.contains(&id_not_hidden));
1923
1924            // Keep track of how often the networks were selected
1925            if selected_networks.contains(&id_maybe_hidden) {
1926                maybe_hidden_selection_count += 1;
1927            }
1928            if selected_networks.contains(&id_hidden) {
1929                hidden_selection_count += 1;
1930            }
1931        }
1932
1933        // The 0.5 probability network should be picked at least once, but not every time. With 100
1934        // runs, the chances of either of these assertions flaking is 1 / (0.5^100), i.e. 1 in 1e30.
1935        // Even with a hypothetical 1,000,000 test runs per day, there would be an average of 1e24
1936        // days between flakes due to this test.
1937        assert!(maybe_hidden_selection_count > 0);
1938        assert!(maybe_hidden_selection_count < hidden_selection_count);
1939    }
1940
1941    #[fuchsia::test]
1942    async fn test_select_high_probability_hidden_networks() {
1943        // Create three networks with 1, 0, 0.5 hidden probability
1944        let id_hidden = types::NetworkIdentifier {
1945            ssid: types::Ssid::try_from("hidden").unwrap(),
1946            security_type: types::SecurityType::Wpa2,
1947        };
1948        let net_config_hidden = NetworkConfig::new(
1949            id_hidden.clone(),
1950            Credential::Password(b"password".to_vec()),
1951            false,
1952            Some(1.0),
1953        )
1954        .expect("failed to create network config");
1955
1956        let id_maybe_hidden_high = types::NetworkIdentifier {
1957            ssid: types::Ssid::try_from("maybe_hidden_high").unwrap(),
1958            security_type: types::SecurityType::Wpa2,
1959        };
1960        let net_config_maybe_hidden_high = NetworkConfig::new(
1961            id_maybe_hidden_high.clone(),
1962            Credential::Password(b"password".to_vec()),
1963            false,
1964            Some(0.8),
1965        )
1966        .expect("failed to create network config");
1967
1968        let id_maybe_hidden_low = types::NetworkIdentifier {
1969            ssid: types::Ssid::try_from("maybe_hidden_low").unwrap(),
1970            security_type: types::SecurityType::Wpa2,
1971        };
1972        let net_config_maybe_hidden_low = NetworkConfig::new(
1973            id_maybe_hidden_low.clone(),
1974            Credential::Password(b"password".to_vec()),
1975            false,
1976            Some(0.7),
1977        )
1978        .expect("failed to create network config");
1979
1980        let id_not_hidden = types::NetworkIdentifier {
1981            ssid: types::Ssid::try_from("not_hidden").unwrap(),
1982            security_type: types::SecurityType::Wpa2,
1983        };
1984        let net_config_not_hidden = NetworkConfig::new(
1985            id_not_hidden.clone(),
1986            Credential::Password(b"password".to_vec()),
1987            false,
1988            Some(0.0),
1989        )
1990        .expect("failed to create network config");
1991
1992        let selected_networks = select_high_probability_hidden_networks(vec![
1993            net_config_hidden.clone(),
1994            net_config_maybe_hidden_high.clone(),
1995            net_config_maybe_hidden_low.clone(),
1996            net_config_not_hidden.clone(),
1997        ]);
1998
1999        // The 1.0 probability should always be picked
2000        assert!(selected_networks.contains(&id_hidden));
2001        // The high probability should always be picked
2002        assert!(selected_networks.contains(&id_maybe_hidden_high));
2003        // The low probability should never be picked
2004        assert!(!selected_networks.contains(&id_maybe_hidden_low));
2005        // The 0 probability should never be picked
2006        assert!(!selected_networks.contains(&id_not_hidden));
2007    }
2008
2009    #[fuchsia::test]
2010    async fn test_record_not_seen_active_scan() {
2011        // Test that if we update that we haven't seen a couple of networks in active scans, their
2012        // hidden probability is updated.
2013        let saved_networks = SavedNetworksManager::new_for_test().await;
2014
2015        // Seen in active scans
2016        let id_1 = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2017        let credential_1 = Credential::Password(b"some_password".to_vec());
2018        let id_2 = NetworkIdentifier::try_from("bar", SecurityType::Wpa3).unwrap();
2019        let credential_2 = Credential::Password(b"another_password".to_vec());
2020        // Seen in active scan but not saved
2021        let id_3 = NetworkIdentifier::try_from("baz", SecurityType::None).unwrap();
2022        // Saved and targeted in active scan but not seen
2023        let id_4 = NetworkIdentifier::try_from("foobar", SecurityType::None).unwrap();
2024        let credential_4 = Credential::None;
2025
2026        // Save 3 of the 4 networks
2027        assert!(
2028            saved_networks
2029                .store(id_1.clone(), credential_1)
2030                .await
2031                .expect("failed to store network")
2032                .is_none()
2033        );
2034        assert!(
2035            saved_networks
2036                .store(id_2.clone(), credential_2)
2037                .await
2038                .expect("failed to store network")
2039                .is_none()
2040        );
2041        assert!(
2042            saved_networks
2043                .store(id_4.clone(), credential_4)
2044                .await
2045                .expect("failed to store network")
2046                .is_none()
2047        );
2048        // Check that the saved networks have the default hidden probability so later we can just
2049        // check that the probability has changed.
2050        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2051        assert_eq!(config_1.hidden_probability, PROB_HIDDEN_DEFAULT);
2052        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2053        assert_eq!(config_2.hidden_probability, PROB_HIDDEN_DEFAULT);
2054        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2055        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2056
2057        let not_seen_ids = vec![id_1.ssid.clone(), id_2.ssid.clone(), id_3.ssid.clone()];
2058        saved_networks.record_scan_result(not_seen_ids, &HashMap::new()).await;
2059
2060        // Check that the configs' hidden probability has decreased
2061        let config_1 = saved_networks.lookup(&id_1).await.pop().expect("failed to lookup");
2062        assert!(config_1.hidden_probability < PROB_HIDDEN_DEFAULT);
2063        let config_2 = saved_networks.lookup(&id_2).await.pop().expect("failed to lookup");
2064        assert!(config_2.hidden_probability < PROB_HIDDEN_DEFAULT);
2065
2066        // Check that for the network that was target but not seen in the active scan, its hidden
2067        // probability isn't lowered.
2068        let config_4 = saved_networks.lookup(&id_4).await.pop().expect("failed to lookup");
2069        assert_eq!(config_4.hidden_probability, PROB_HIDDEN_DEFAULT);
2070
2071        // Check that a config was not saved for the identifier that was not saved before.
2072        assert!(saved_networks.lookup(&id_3).await.is_empty());
2073    }
2074
2075    #[fuchsia::test]
2076    async fn test_update_scan_stats_for_single_bss() {
2077        // Record multiple scans for a network where there is only 1 BSS for the network. The
2078        // config should be considered likely single BSS.
2079        let saved_networks = SavedNetworksManager::new_for_test().await;
2080
2081        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2082        let credential = Credential::Password(b"some_password".to_vec());
2083        assert!(
2084            saved_networks
2085                .store(id.clone(), credential.clone())
2086                .await
2087                .expect("failed to store network")
2088                .is_none()
2089        );
2090
2091        let id_detailed = types::NetworkIdentifierDetailed {
2092            ssid: id.ssid.clone(),
2093            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2094        };
2095        let scan_results = HashMap::from([(
2096            id_detailed.clone(),
2097            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2098        )]);
2099
2100        // likely has one BSS
2101        for _ in 0..5 {
2102            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2103        }
2104
2105        let is_single_bss = saved_networks
2106            .is_network_single_bss(&id, &credential)
2107            .await
2108            .expect("failed to lookup if network is single BSS");
2109        assert!(is_single_bss);
2110    }
2111
2112    #[fuchsia::test]
2113    async fn test_update_scan_stats_for_multiple_bss_at_least_once() {
2114        // Record multiple scans for a network where there are multiple BSS. The network config
2115        // should say that the network is not single BSS.
2116        let saved_networks = SavedNetworksManager::new_for_test().await;
2117
2118        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2119        let credential = Credential::Password(b"some_password".to_vec());
2120        assert!(
2121            saved_networks
2122                .store(id.clone(), credential.clone())
2123                .await
2124                .expect("failed to store network")
2125                .is_none()
2126        );
2127
2128        let id_detailed = types::NetworkIdentifierDetailed {
2129            ssid: id.ssid.clone(),
2130            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2131        };
2132        let scan_results_single = HashMap::from([(
2133            id_detailed.clone(),
2134            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2135        )]);
2136
2137        let scan_results_multi = HashMap::from([(
2138            id_detailed.clone(),
2139            vec![
2140                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2141                types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() },
2142            ],
2143        )]);
2144
2145        // Record some scan results with one BSS, and record once with multiple BSS.
2146        for _ in 0..2 {
2147            saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2148        }
2149
2150        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_multi).await;
2151        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results_single).await;
2152
2153        // The one scan with multiple BSS results should make the network determined to be
2154        // multi BSS.
2155        let is_single_bss = saved_networks
2156            .is_network_single_bss(&id, &credential)
2157            .await
2158            .expect("failed to lookup if network is single BSS");
2159        assert!(!is_single_bss);
2160    }
2161
2162    #[fuchsia::test]
2163    async fn test_record_scan_more_than_once_to_decide_single_bss() {
2164        // Test that a network is not decided to be single BSS after only one scan.
2165        let saved_networks = SavedNetworksManager::new_for_test().await;
2166
2167        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2168        let credential = Credential::Password(b"some_password".to_vec());
2169        assert!(
2170            saved_networks
2171                .store(id.clone(), credential.clone())
2172                .await
2173                .expect("failed to store network")
2174                .is_none()
2175        );
2176
2177        let id_detailed = types::NetworkIdentifierDetailed {
2178            ssid: id.ssid.clone(),
2179            security_type: types::SecurityTypeDetailed::Wpa2Personal,
2180        };
2181        let scan_results = HashMap::from([(
2182            id_detailed,
2183            vec![types::Bss { observation: ScanObservation::Passive, ..generate_random_bss() }],
2184        )]);
2185
2186        // Record the scan multiple times, since multiple scans are needed to decide the network
2187        // likely has one BSS
2188        saved_networks.record_scan_result(vec![id.ssid.clone()], &scan_results).await;
2189
2190        let is_single_bss = saved_networks
2191            .is_network_single_bss(&id, &credential)
2192            .await
2193            .expect("failed to lookup if network is single BSS");
2194        assert!(!is_single_bss);
2195    }
2196
2197    #[fuchsia::test]
2198    async fn test_get_past_connections() {
2199        let saved_networks_manager = SavedNetworksManager::new_for_test().await;
2200
2201        let id = NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap();
2202        let credential = Credential::Password(b"some_password".to_vec());
2203        let mut config = NetworkConfig::new(id.clone(), credential.clone(), true, None)
2204            .expect("failed to create config");
2205        let mut past_connections = HistoricalListsByBssid::new();
2206
2207        // Add two past connections with the same bssid
2208        let data_1 = random_connection_data();
2209        let bssid_1 = data_1.bssid;
2210        let mut data_2 = random_connection_data();
2211        data_2.bssid = bssid_1;
2212        past_connections.add(bssid_1, data_1);
2213        past_connections.add(bssid_1, data_2);
2214
2215        // Add a past connection with different bssid
2216        let data_3 = random_connection_data();
2217        let bssid_2 = data_3.bssid;
2218        past_connections.add(bssid_2, data_3);
2219        config.perf_stats.past_connections = past_connections;
2220
2221        // Create SavedNetworksManager with configs that have past connections
2222        assert!(
2223            saved_networks_manager
2224                .saved_networks
2225                .lock()
2226                .await
2227                .insert(id.clone(), vec![config])
2228                .is_none()
2229        );
2230
2231        // Check that get_past_connections gets the two PastConnectionLists for the BSSIDs.
2232        let mut expected_past_connections = PastConnectionList::default();
2233        expected_past_connections.add(data_1);
2234        expected_past_connections.add(data_2);
2235        let actual_past_connections =
2236            saved_networks_manager.get_past_connections(&id, &credential, &bssid_1).await;
2237        assert_eq!(actual_past_connections, expected_past_connections);
2238
2239        let mut expected_past_connections = PastConnectionList::default();
2240        expected_past_connections.add(data_3);
2241        let actual_past_connections =
2242            saved_networks_manager.get_past_connections(&id, &credential, &bssid_2).await;
2243        assert_eq!(actual_past_connections, expected_past_connections);
2244
2245        // Check that get_past_connections will not get the PastConnectionLists if the specified
2246        // Credential is different.
2247        let actual_past_connections = saved_networks_manager
2248            .get_past_connections(&id, &Credential::Password(b"other-password".to_vec()), &bssid_1)
2249            .await;
2250        assert_eq!(actual_past_connections, PastConnectionList::default());
2251    }
2252
2253    fn fake_successful_connect_result() -> fidl_sme::ConnectResult {
2254        fidl_sme::ConnectResult {
2255            code: fidl_ieee80211::StatusCode::Success,
2256            is_credential_rejected: false,
2257            is_reconnect: false,
2258        }
2259    }
2260}