Skip to main content

wlancfg_lib/config_management/
network_config.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 crate::client::types as client_types;
6use crate::util::historical_list::{HistoricalList, Timestamped};
7use arbitrary::Arbitrary;
8#[cfg(test)]
9use fidl_fuchsia_wlan_internal as fidl_internal;
10use fidl_fuchsia_wlan_policy as fidl_policy;
11use fuchsia_async as fasync;
12use std::cmp::Reverse;
13use std::collections::{HashMap, HashSet};
14use std::fmt::{self, Debug};
15use wlan_common::security::wep::WepKey;
16use wlan_common::security::wpa::WpaDescriptor;
17use wlan_common::security::wpa::credential::{Passphrase, Psk};
18use wlan_common::security::{SecurityAuthenticator, SecurityDescriptor};
19
20/// The max number of connection results we will store per BSS at a time. For now, this number is
21/// chosen arbitartily.
22const NUM_CONNECTION_RESULTS_PER_BSS: usize = 10;
23/// constants for the constraints on valid credential values
24const WEP_40_ASCII_LEN: usize = 5;
25const WEP_40_HEX_LEN: usize = 10;
26const WEP_104_ASCII_LEN: usize = 13;
27const WEP_104_HEX_LEN: usize = 26;
28const WPA_MIN_PASSWORD_LEN: usize = 8;
29const WPA_MAX_PASSWORD_LEN: usize = 63;
30pub const WPA_PSK_BYTE_LEN: usize = 32;
31/// If we have seen a network in a passive scan, we will rarely actively scan for it.
32pub const PROB_HIDDEN_IF_SEEN_PASSIVE: f32 = 0.05;
33/// If we have connected to a network from a passive scan, we will never scan for it.
34pub const PROB_HIDDEN_IF_CONNECT_PASSIVE: f32 = 0.0;
35/// If we connected to a network after we had to scan actively to find it, it is likely hidden.
36pub const PROB_HIDDEN_IF_CONNECT_ACTIVE: f32 = 0.95;
37/// Default probability that we will actively scan for the network if we haven't seen it in any
38/// passive scan.
39pub const PROB_HIDDEN_DEFAULT: f32 = 0.9;
40/// The lowest we will set the probability for actively scanning for a network.
41pub const PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE: f32 = 0.25;
42/// How much we will lower the probability of scanning for an active network if we don't see the
43/// network in an active scan.
44pub const PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE: f32 = 0.14;
45/// Threshold for saying that a network has "high probability of being hidden".
46// Implementation detail: this is set to PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE
47// to allow for a newly saved network to be scanned at least twice before falling out of the
48// "HIDDEN_PROBABILITY_HIGH" range.
49pub const HIDDEN_PROBABILITY_HIGH: f32 =
50    PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
51// The probability at which we decisively claim a network to be hidden. Implementation detail: we
52// assume a network to be hidden iff we connect only after observing the network in an active scan,
53// not a passive scan.
54pub const PROB_IS_HIDDEN: f32 = PROB_HIDDEN_IF_CONNECT_ACTIVE;
55pub const NUM_SCANS_TO_DECIDE_LIKELY_SINGLE_BSS: usize = 4;
56
57pub type SaveError = fidl_policy::NetworkConfigChangeError;
58
59/// In-memory history of things that we need to know to calculated hidden network probability.
60#[derive(Clone, Debug, PartialEq)]
61struct HiddenProbabilityStats {
62    pub connected_active: bool,
63}
64
65impl HiddenProbabilityStats {
66    fn new() -> Self {
67        HiddenProbabilityStats { connected_active: false }
68    }
69}
70
71/// History of connects, disconnects, and connection strength to estimate whether we can establish
72/// and maintain connection with a network and if it is weakening. Used in choosing best network.
73#[derive(Clone, Debug, PartialEq)]
74pub struct PerformanceStats {
75    pub connect_failures: HistoricalListsByBssid<ConnectFailure>,
76    pub past_connections: HistoricalListsByBssid<PastConnectionData>,
77}
78
79impl Default for PerformanceStats {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl PerformanceStats {
86    pub fn new() -> Self {
87        Self {
88            connect_failures: HistoricalListsByBssid::new(),
89            past_connections: HistoricalListsByBssid::new(),
90        }
91    }
92}
93
94/// Data about scans involving this network. It is used to determine whether or not a network is
95/// likely multi-BSS or single-BSS, since one BSS could be missed, off, or out of range.
96#[derive(Clone, Debug, PartialEq)]
97struct ScanStats {
98    pub have_seen_multi_bss: bool,
99    /// The number of scans that have been performed for this metric.
100    pub num_scans: usize,
101}
102
103impl ScanStats {
104    pub fn new() -> Self {
105        Self { have_seen_multi_bss: false, num_scans: 0 }
106    }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub enum FailureReason {
111    // Failed to join because the authenticator did not accept the credentials provided.
112    CredentialRejected,
113    // Failed to join for other reason, mapped from SME ConnectResultCode::Failed
114    GeneralFailure,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct ConnectFailure {
119    /// For determining whether this connection failure is still relevant
120    pub time: fasync::MonotonicInstant,
121    /// The reason that connection failed
122    pub reason: FailureReason,
123    /// The BSSID that we failed to connect to
124    pub bssid: client_types::Bssid,
125}
126
127impl Timestamped for ConnectFailure {
128    fn time(&self) -> fasync::MonotonicInstant {
129        self.time
130    }
131}
132
133/// Data points related to historical connection
134#[derive(Clone, Copy, Debug, PartialEq)]
135pub struct PastConnectionData {
136    pub bssid: client_types::Bssid,
137    /// Time at which the connection was ended
138    pub disconnect_time: fasync::MonotonicInstant,
139    /// The time that the connection was up - from established to disconnected.
140    pub connection_uptime: zx::MonotonicDuration,
141    /// Cause of disconnect or failure to connect
142    pub disconnect_reason: client_types::DisconnectReason,
143    /// Final signal strength measure before disconnect
144    pub signal_at_disconnect: client_types::Signal,
145    /// Average phy rate over connection duration
146    pub average_tx_rate: u32,
147}
148
149impl PastConnectionData {
150    pub fn new(
151        bssid: client_types::Bssid,
152        disconnect_time: fasync::MonotonicInstant,
153        connection_uptime: zx::MonotonicDuration,
154        disconnect_reason: client_types::DisconnectReason,
155        signal_at_disconnect: client_types::Signal,
156        average_tx_rate: u32,
157    ) -> Self {
158        Self {
159            bssid,
160            disconnect_time,
161            connection_uptime,
162            disconnect_reason,
163            signal_at_disconnect,
164            average_tx_rate,
165        }
166    }
167}
168
169impl Timestamped for PastConnectionData {
170    fn time(&self) -> fasync::MonotonicInstant {
171        self.disconnect_time
172    }
173}
174
175/// Data structures for storing historical connection information for a BSS.
176pub type PastConnectionList = HistoricalList<PastConnectionData>;
177impl Default for PastConnectionList {
178    fn default() -> Self {
179        Self::new(NUM_CONNECTION_RESULTS_PER_BSS)
180    }
181}
182
183/// Struct for map from BSSID to HistoricalList
184#[derive(Clone, Debug, PartialEq)]
185pub struct HistoricalListsByBssid<T: Timestamped>(HashMap<client_types::Bssid, HistoricalList<T>>);
186
187impl<T> Default for HistoricalListsByBssid<T>
188where
189    T: Timestamped + Clone,
190{
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196impl<T> HistoricalListsByBssid<T>
197where
198    T: Timestamped + Clone,
199{
200    pub fn new() -> Self {
201        Self(HashMap::new())
202    }
203
204    pub fn add(&mut self, bssid: client_types::Bssid, data: T) {
205        self.0
206            .entry(bssid)
207            .or_insert_with(|| HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS))
208            .add(data);
209    }
210
211    /// Retrieve list of Data entries to any BSS with a time more recent than earliest_time, sorted
212    /// from oldest to newest. May be empty.
213    pub fn get_recent_for_network(&self, earliest_time: fasync::MonotonicInstant) -> Vec<T> {
214        let mut recents: Vec<T> = vec![];
215        for bssid in self.0.keys() {
216            recents.append(&mut self.get_list_for_bss(bssid).get_recent(earliest_time));
217        }
218        recents.sort_by_key(|a| a.time());
219        recents
220    }
221
222    /// Retrieve List for a particular BSS, in order to retrieve BSS specific Data entries.
223    pub fn get_list_for_bss(&self, bssid: &client_types::Bssid) -> HistoricalList<T> {
224        self.0
225            .get(bssid)
226            .cloned()
227            .unwrap_or_else(|| HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS))
228    }
229}
230
231/// Used to allow hidden probability calculations to make use of what happened most recently
232#[derive(Clone, Copy)]
233pub enum HiddenProbEvent {
234    /// We just saw the network in a passive scan
235    SeenPassive,
236    /// We just connected to the network using passive scan results
237    ConnectPassive,
238    /// We just connected to the network after needing an active scan to see it.
239    ConnectActive,
240    /// We just actively scanned for the network and did not see it.
241    NotSeenActive,
242}
243
244/// Saved data for networks, to remember how to connect to a network and determine if we should.
245#[derive(Clone, Debug, PartialEq)]
246pub struct NetworkConfig {
247    /// (persist) SSID and security type to identify a network.
248    pub ssid: client_types::Ssid,
249    pub security_type: SecurityType,
250    /// (persist) Credential to connect to a protected network or None if the network is open.
251    pub credential: Credential,
252    /// (persist) Remember whether our network indentifier and credential work.
253    pub has_ever_connected: bool,
254    /// How confident we are that this network is hidden, between 0 and 1. We will use
255    /// this number to probabilistically perform an active scan for the network. This is persisted
256    /// to maintain consistent behavior between reboots. 0 means not hidden.
257    pub hidden_probability: f32,
258    /// Data that we use to calculate hidden_probability.
259    hidden_probability_stats: HiddenProbabilityStats,
260    /// Used to estimate quality to determine whether we want to choose this network.
261    pub perf_stats: PerformanceStats,
262    /// Used to determine whether the BSS is likely a single-BSS network, so that roam scans
263    /// happen much less if it is single-BSS.
264    scan_stats: ScanStats,
265}
266
267impl NetworkConfig {
268    /// A new network config is created by loading from persistent storage on boot or when a new
269    /// network is saved.
270    pub fn new(
271        id: NetworkIdentifier,
272        credential: Credential,
273        has_ever_connected: bool,
274        hidden_probability: Option<f32>,
275    ) -> Result<Self, NetworkConfigError> {
276        check_config_errors(&id.ssid, &id.security_type, &credential)?;
277
278        Ok(Self {
279            ssid: id.ssid,
280            security_type: id.security_type,
281            credential,
282            has_ever_connected,
283            hidden_probability: hidden_probability.unwrap_or(PROB_HIDDEN_DEFAULT).clamp(0.0, 1.0),
284            hidden_probability_stats: HiddenProbabilityStats::new(),
285            perf_stats: PerformanceStats::new(),
286            scan_stats: ScanStats::new(),
287        })
288    }
289
290    // Update the network config's probability that we will actively scan for the network.
291    // If a network has been both seen in a passive scan and connected to after an active scan,
292    // we will determine probability based on what happened most recently.
293    // TODO(63306) Add metric to see if we see conflicting passive/active events.
294    pub fn update_hidden_prob(&mut self, event: HiddenProbEvent) {
295        match event {
296            HiddenProbEvent::ConnectPassive => {
297                self.hidden_probability = PROB_HIDDEN_IF_CONNECT_PASSIVE;
298            }
299            HiddenProbEvent::SeenPassive => {
300                // If the probability hidden is lower from connecting to the network after a
301                // passive scan, don't change.
302                if self.hidden_probability > PROB_HIDDEN_IF_SEEN_PASSIVE {
303                    self.hidden_probability = PROB_HIDDEN_IF_SEEN_PASSIVE;
304                }
305            }
306            HiddenProbEvent::ConnectActive => {
307                self.hidden_probability_stats.connected_active = true;
308                self.hidden_probability = PROB_HIDDEN_IF_CONNECT_ACTIVE;
309            }
310            HiddenProbEvent::NotSeenActive => {
311                // If we have previously required an active scan to connect this network, we are
312                // confident that it is hidden and don't care about this event.
313                if self.hidden_probability_stats.connected_active {
314                    return;
315                }
316                // The probability will not be changed if already lower than the threshold.
317                if self.hidden_probability <= PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE {
318                    return;
319                }
320                // If we failed to find the network in an active scan, lower the probability but
321                // not below a certain threshold.
322                let new_prob = self.hidden_probability - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
323                self.hidden_probability = new_prob.max(PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
324            }
325        }
326    }
327
328    pub fn is_hidden(&self) -> bool {
329        self.hidden_probability >= PROB_IS_HIDDEN
330    }
331
332    #[allow(clippy::assign_op_pattern, reason = "mass allow for https://fxbug.dev/381896734")]
333    pub fn update_seen_multiple_bss(&mut self, multi_bss: bool) {
334        self.scan_stats.have_seen_multi_bss = self.scan_stats.have_seen_multi_bss || multi_bss;
335        self.scan_stats.num_scans = self.scan_stats.num_scans + 1;
336    }
337
338    #[allow(clippy::needless_return, reason = "mass allow for https://fxbug.dev/381896734")]
339    /// We say that a BSS is likely a single-BSS network if only 1 BSS has ever been seen at a time
340    /// for the network and there have been at least some number of scans for the network.
341    pub fn is_likely_single_bss(&self) -> bool {
342        return !self.scan_stats.have_seen_multi_bss
343            && self.scan_stats.num_scans > NUM_SCANS_TO_DECIDE_LIKELY_SINGLE_BSS;
344    }
345}
346
347impl From<&NetworkConfig> for fidl_policy::NetworkConfig {
348    fn from(network_config: &NetworkConfig) -> Self {
349        let network_id = fidl_policy::NetworkIdentifier {
350            ssid: network_config.ssid.to_vec(),
351            type_: network_config.security_type.into(),
352        };
353        let credential = network_config.credential.clone().into();
354        fidl_policy::NetworkConfig {
355            id: Some(network_id),
356            credential: Some(credential),
357            ..Default::default()
358        }
359    }
360}
361
362/// The credential of a network connection. It mirrors the fidl_fuchsia_wlan_policy Credential
363#[derive(Arbitrary)] // Derive Arbitrary for fuzzer
364#[derive(Clone, Debug, PartialEq)]
365pub enum Credential {
366    None,
367    Password(Vec<u8>),
368    Psk(Vec<u8>),
369}
370
371impl Credential {
372    /// Returns:
373    /// - an Open-Credential instance iff `bytes` is empty,
374    /// - a Password-Credential in all other cases.
375    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
376    /// This function does not support reading PSK from bytes because the PSK byte length overlaps
377    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
378    /// with a valid password length. This function should only be used to load legacy data, where
379    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
380    /// PSK was not supported.
381    #[allow(clippy::doc_lazy_continuation, reason = "mass allow for https://fxbug.dev/381896734")]
382    /// Note: This function is of temporary nature to support legacy code.
383    pub fn from_bytes(bytes: impl AsRef<[u8]> + Into<Vec<u8>>) -> Self {
384        match bytes.as_ref().len() {
385            0 => Credential::None,
386            _ => Credential::Password(bytes.into()),
387        }
388    }
389
390    /// Transform credential into the bytes that represent the credential, dropping the information
391    /// of the type. This is used to support the legacy storage method.
392    pub fn into_bytes(self) -> Vec<u8> {
393        match self {
394            Credential::Password(pwd) => pwd,
395            Credential::Psk(psk) => psk,
396            Credential::None => vec![],
397        }
398    }
399
400    /// Choose a security type that fits the credential while we don't actually know the security type
401    /// of the saved networks. This should only be used if we don't have a specified security type.
402    pub fn derived_security_type(&self) -> SecurityType {
403        match self {
404            Credential::None => SecurityType::None,
405            _ => SecurityType::Wpa2,
406        }
407    }
408
409    pub fn type_str(&self) -> &str {
410        match self {
411            Credential::None => "None",
412            Credential::Password(_) => "Password",
413            Credential::Psk(_) => "PSK",
414        }
415    }
416}
417
418impl TryFrom<fidl_policy::Credential> for Credential {
419    type Error = NetworkConfigError;
420    /// Create a Credential from a fidl Crednetial value.
421    fn try_from(credential: fidl_policy::Credential) -> Result<Self, Self::Error> {
422        match credential {
423            fidl_policy::Credential::None(fidl_policy::Empty {}) => Ok(Self::None),
424            fidl_policy::Credential::Password(pwd) => Ok(Self::Password(pwd)),
425            fidl_policy::Credential::Psk(psk) => Ok(Self::Psk(psk)),
426            _ => Err(NetworkConfigError::CredentialTypeInvalid),
427        }
428    }
429}
430
431impl From<Credential> for fidl_policy::Credential {
432    fn from(credential: Credential) -> Self {
433        match credential {
434            Credential::Password(pwd) => fidl_policy::Credential::Password(pwd),
435            Credential::Psk(psk) => fidl_policy::Credential::Psk(psk),
436            Credential::None => fidl_policy::Credential::None(fidl_policy::Empty),
437        }
438    }
439}
440
441// TODO(https://fxbug.dev/42053561): Remove this operator implementation. Once calls to
442//                         `select_authentication_method` are removed from the state machine, there
443//                         will instead be an `Authentication` (or `SecurityAuthenticator`) field
444//                         in `ScannedCandidate` which can be more directly compared to SME
445//                         `ConnectRequest`s in tests.
446#[cfg(test)]
447impl PartialEq<Option<fidl_internal::Credentials>> for Credential {
448    fn eq(&self, credentials: &Option<fidl_internal::Credentials>) -> bool {
449        use fidl_internal::{Credentials, WepCredentials, WpaCredentials};
450
451        match credentials {
452            None => matches!(self, Credential::None),
453            Some(Credentials::Wep(WepCredentials { key })) => {
454                if let Credential::Password(unparsed) = self {
455                    // `Credential::Password` is used for both WEP and WPA. The encoding of WEP
456                    // keys is unspecified and may be either binary (unencoded) or ASCII-encoded
457                    // hexadecimal. To compare, this WEP key must be parsed.
458                    WepKey::parse(unparsed).is_ok_and(|parsed| &Vec::from(parsed) == key)
459                } else {
460                    false
461                }
462            }
463            Some(Credentials::Wpa(credentials)) => match credentials {
464                WpaCredentials::Passphrase(passphrase) => {
465                    if let Credential::Password(unparsed) = self {
466                        unparsed == &passphrase.clone()
467                    } else {
468                        false
469                    }
470                }
471                WpaCredentials::Psk(psk) => {
472                    if let Credential::Psk(unparsed) = self {
473                        unparsed == &Vec::from(*psk)
474                    } else {
475                        false
476                    }
477                }
478                _ => panic!("unrecognized FIDL variant"),
479            },
480            Some(_) => panic!("unrecognized FIDL variant"),
481        }
482    }
483}
484
485// TODO(https://fxbug.dev/42053561): Remove this operator implementation. See the similar conversion above.
486#[cfg(test)]
487impl PartialEq<Option<Box<fidl_internal::Credentials>>> for Credential {
488    fn eq(&self, credentials: &Option<Box<fidl_internal::Credentials>>) -> bool {
489        self.eq(&credentials.as_ref().map(|credentials| *credentials.clone()))
490    }
491}
492
493#[derive(Arbitrary)] // Derive Arbitrary for fuzzer
494#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
495pub enum SecurityType {
496    None,
497    Wep,
498    Wpa,
499    Wpa2,
500    Wpa3,
501}
502
503impl From<SecurityDescriptor> for SecurityType {
504    fn from(descriptor: SecurityDescriptor) -> Self {
505        match descriptor {
506            SecurityDescriptor::Open => SecurityType::None,
507            // TODO(https://fxbug.dev/458136222): Introduce SecurityType::Owe
508            SecurityDescriptor::Owe => SecurityType::None,
509            SecurityDescriptor::Wep => SecurityType::Wep,
510            SecurityDescriptor::Wpa(wpa) => match wpa {
511                WpaDescriptor::Wpa1 { .. } => SecurityType::Wpa,
512                WpaDescriptor::Wpa2 { .. } => SecurityType::Wpa2,
513                WpaDescriptor::Wpa3 { .. } => SecurityType::Wpa3,
514            },
515        }
516    }
517}
518
519impl From<fidl_policy::SecurityType> for SecurityType {
520    fn from(security: fidl_policy::SecurityType) -> Self {
521        match security {
522            fidl_policy::SecurityType::None => SecurityType::None,
523            fidl_policy::SecurityType::Wep => SecurityType::Wep,
524            fidl_policy::SecurityType::Wpa => SecurityType::Wpa,
525            fidl_policy::SecurityType::Wpa2 => SecurityType::Wpa2,
526            fidl_policy::SecurityType::Wpa3 => SecurityType::Wpa3,
527        }
528    }
529}
530
531impl From<SecurityType> for fidl_policy::SecurityType {
532    fn from(security_type: SecurityType) -> Self {
533        match security_type {
534            SecurityType::None => fidl_policy::SecurityType::None,
535            SecurityType::Wep => fidl_policy::SecurityType::Wep,
536            SecurityType::Wpa => fidl_policy::SecurityType::Wpa,
537            SecurityType::Wpa2 => fidl_policy::SecurityType::Wpa2,
538            SecurityType::Wpa3 => fidl_policy::SecurityType::Wpa3,
539        }
540    }
541}
542
543impl SecurityType {
544    /// List all security type variants.
545    pub fn list_variants() -> Vec<Self> {
546        vec![
547            SecurityType::None,
548            SecurityType::Wep,
549            SecurityType::Wpa,
550            SecurityType::Wpa2,
551            SecurityType::Wpa3,
552        ]
553    }
554
555    /// Return whether or not this saved security type can be used to connect scan results with
556    /// this detailed security type.
557    pub fn is_compatible_with_scanned_type(
558        &self,
559        scanned_type: &client_types::SecurityTypeDetailed,
560    ) -> bool {
561        match self {
562            SecurityType::None => {
563                // return true if the scanned security is open, or false otherwise.
564                scanned_type == &client_types::SecurityTypeDetailed::Open
565            }
566            SecurityType::Wep => scanned_type == &client_types::SecurityTypeDetailed::Wep,
567            SecurityType::Wpa => {
568                scanned_type == &client_types::SecurityTypeDetailed::Wpa1
569                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2Personal
570                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly
571                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Personal
572                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2PersonalTkipOnly
573            }
574            SecurityType::Wpa2 => {
575                scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2Personal
576                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa1Wpa2PersonalTkipOnly
577                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Personal
578                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2PersonalTkipOnly
579                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa2Wpa3Personal
580                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa3Personal
581            }
582            SecurityType::Wpa3 => {
583                scanned_type == &client_types::SecurityTypeDetailed::Wpa2Wpa3Personal
584                    || scanned_type == &client_types::SecurityTypeDetailed::Wpa3Personal
585            }
586        }
587    }
588}
589
590/// The network identifier is the SSID and security policy of the network, and it is used to
591/// distinguish networks. It mirrors the NetworkIdentifier in fidl_fuchsia_wlan_policy.
592#[derive(Arbitrary)]
593// Derive Arbitrary for fuzzer
594// To avoid printing PII, only allow Debug in tests, runtime logging should use Display
595#[derive(Clone, Eq, Hash, PartialEq)]
596#[cfg_attr(test, derive(Debug))]
597pub struct NetworkIdentifier {
598    pub ssid: client_types::Ssid,
599    pub security_type: SecurityType,
600}
601
602impl fmt::Display for NetworkIdentifier {
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        write!(f, "NetworkIdentifier: {}, {:?}", self.ssid, self.security_type)
605    }
606}
607
608impl NetworkIdentifier {
609    pub fn new(ssid: client_types::Ssid, security_type: SecurityType) -> Self {
610        NetworkIdentifier { ssid, security_type }
611    }
612
613    #[cfg(test)]
614    pub fn try_from(ssid: &str, security_type: SecurityType) -> Result<Self, anyhow::Error> {
615        Ok(NetworkIdentifier { ssid: client_types::Ssid::try_from(ssid)?, security_type })
616    }
617}
618
619impl From<fidl_policy::NetworkIdentifier> for NetworkIdentifier {
620    fn from(id: fidl_policy::NetworkIdentifier) -> Self {
621        Self::new(client_types::Ssid::from_bytes_unchecked(id.ssid), id.type_.into())
622    }
623}
624
625impl From<NetworkIdentifier> for fidl_policy::NetworkIdentifier {
626    fn from(id: NetworkIdentifier) -> Self {
627        fidl_policy::NetworkIdentifier { ssid: id.ssid.into(), type_: id.security_type.into() }
628    }
629}
630
631impl From<NetworkConfig> for fidl_policy::NetworkConfig {
632    fn from(config: NetworkConfig) -> Self {
633        let network_id = NetworkIdentifier::new(config.ssid, config.security_type);
634        fidl_policy::NetworkConfig {
635            id: Some(fidl_policy::NetworkIdentifier::from(network_id)),
636            credential: Some(fidl_policy::Credential::from(config.credential)),
637            ..Default::default()
638        }
639    }
640}
641
642/// Returns an error if the input network values are not valid or none if the values are valid.
643/// For example it is an error if the network is Open (no password) but a password is supplied.
644/// TODO(nmccracken) - Specific errors need to be added to the API and returned here
645fn check_config_errors(
646    ssid: &client_types::Ssid,
647    security_type: &SecurityType,
648    credential: &Credential,
649) -> Result<(), NetworkConfigError> {
650    // Verify SSID has at least 1 byte.
651    if ssid.is_empty() {
652        return Err(NetworkConfigError::SsidEmpty);
653    }
654    // Verify that credentials match the security type. This code only inspects the lengths of
655    // passphrases and PSKs; the underlying data is considered opaque here.
656    match security_type {
657        SecurityType::None => {
658            if let Credential::Psk(_) | Credential::Password(_) = credential {
659                return Err(NetworkConfigError::OpenNetworkPassword);
660            }
661        }
662        // Note that some vendors allow WEP passphrase and PSK lengths that are not described by
663        // IEEE 802.11. These lengths are unsupported. See also the `wep_deprecated` crate.
664        SecurityType::Wep => match credential {
665            Credential::Password(password) => match password.len() {
666                // ASCII encoding.
667                WEP_40_ASCII_LEN | WEP_104_ASCII_LEN => {}
668                // Hexadecimal encoding.
669                WEP_40_HEX_LEN | WEP_104_HEX_LEN => {}
670                _ => {
671                    return Err(NetworkConfigError::PasswordLen);
672                }
673            },
674            _ => {
675                return Err(NetworkConfigError::MissingPasswordPsk);
676            }
677        },
678        SecurityType::Wpa | SecurityType::Wpa2 | SecurityType::Wpa3 => match credential {
679            Credential::Password(pwd) => {
680                if pwd.len() < WPA_MIN_PASSWORD_LEN || pwd.len() > WPA_MAX_PASSWORD_LEN {
681                    return Err(NetworkConfigError::PasswordLen);
682                }
683            }
684            Credential::Psk(psk) => {
685                if security_type == &SecurityType::Wpa3 {
686                    return Err(NetworkConfigError::Wpa3Psk);
687                }
688                if psk.len() != WPA_PSK_BYTE_LEN {
689                    return Err(NetworkConfigError::PskLen);
690                }
691            }
692            _ => {
693                return Err(NetworkConfigError::MissingPasswordPsk);
694            }
695        },
696    }
697    Ok(())
698}
699
700/// Error codes representing problems in trying to save a network config, such as errors saving
701/// or removing a network config, or for invalid values when trying to create a network config.
702#[derive(Hash, PartialEq, Eq)]
703pub enum NetworkConfigError {
704    OpenNetworkPassword,
705    Wpa3Psk,
706    PasswordLen,
707    PskLen,
708    SsidEmpty,
709    MissingPasswordPsk,
710    ConfigMissingId,
711    ConfigMissingCredential,
712    CredentialTypeInvalid,
713    FileWriteError,
714    LegacyWriteError,
715}
716
717impl Debug for NetworkConfigError {
718    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
719        match self {
720            NetworkConfigError::OpenNetworkPassword => {
721                write!(f, "can't have an open network with a password or PSK")
722            }
723            NetworkConfigError::Wpa3Psk => {
724                write!(f, "can't use a PSK to connect to a WPA3 network")
725            }
726            NetworkConfigError::PasswordLen => write!(f, "invalid password length"),
727            NetworkConfigError::PskLen => write!(f, "invalid PSK length"),
728            NetworkConfigError::SsidEmpty => {
729                write!(f, "SSID must have a non-zero length")
730            }
731            NetworkConfigError::MissingPasswordPsk => {
732                write!(f, "no password or PSK provided but required by security type")
733            }
734            NetworkConfigError::ConfigMissingId => {
735                write!(f, "cannot create network config, network id is None")
736            }
737            NetworkConfigError::ConfigMissingCredential => {
738                write!(f, "cannot create network config, no credential is given")
739            }
740            NetworkConfigError::CredentialTypeInvalid => {
741                write!(f, "cannot convert fidl Credential, unknown variant")
742            }
743            NetworkConfigError::FileWriteError => {
744                write!(f, "error writing network config to file")
745            }
746            NetworkConfigError::LegacyWriteError => {
747                write!(f, "error writing network config to legacy storage")
748            }
749        }
750    }
751}
752
753impl From<NetworkConfigError> for fidl_policy::NetworkConfigChangeError {
754    fn from(err: NetworkConfigError) -> Self {
755        match err {
756            NetworkConfigError::OpenNetworkPassword
757            | NetworkConfigError::MissingPasswordPsk
758            | NetworkConfigError::Wpa3Psk => {
759                fidl_policy::NetworkConfigChangeError::InvalidSecurityCredentialError
760            }
761            NetworkConfigError::PasswordLen | NetworkConfigError::PskLen => {
762                fidl_policy::NetworkConfigChangeError::CredentialLenError
763            }
764            NetworkConfigError::SsidEmpty => fidl_policy::NetworkConfigChangeError::SsidEmptyError,
765            NetworkConfigError::ConfigMissingId | NetworkConfigError::ConfigMissingCredential => {
766                fidl_policy::NetworkConfigChangeError::NetworkConfigMissingFieldError
767            }
768            NetworkConfigError::CredentialTypeInvalid => {
769                fidl_policy::NetworkConfigChangeError::UnsupportedCredentialError
770            }
771            NetworkConfigError::FileWriteError | NetworkConfigError::LegacyWriteError => {
772                fidl_policy::NetworkConfigChangeError::NetworkConfigWriteError
773            }
774        }
775    }
776}
777
778/// Binds a credential to a security protocol.
779///
780/// Binding constructs a `SecurityAuthenticator` that can be used to construct an SME
781/// `ConnectRequest`. This function is similar to `SecurityDescriptor::bind`, but operates on the
782/// Policy `Credential` type, which requires some additional logic to determine how the credential
783/// data is interpreted.
784///
785/// Returns `None` if the given protocol is incompatible with the given credential.
786fn bind_credential_to_protocol(
787    protocol: SecurityDescriptor,
788    credential: &Credential,
789) -> Option<SecurityAuthenticator> {
790    match protocol {
791        SecurityDescriptor::Open => match credential {
792            Credential::None => protocol.bind(None).ok(),
793            _ => None,
794        },
795        SecurityDescriptor::Owe => match credential {
796            Credential::None => protocol.bind(None).ok(),
797            _ => None,
798        },
799        SecurityDescriptor::Wep => match credential {
800            Credential::Password(key) => {
801                WepKey::parse(key).ok().and_then(|key| protocol.bind(Some(key.into())).ok())
802            }
803            _ => None,
804        },
805        SecurityDescriptor::Wpa(wpa) => match wpa {
806            WpaDescriptor::Wpa1 { .. } | WpaDescriptor::Wpa2 { .. } => match credential {
807                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
808                    .ok()
809                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
810                Credential::Psk(psk) => {
811                    Psk::parse(psk).ok().and_then(|psk| protocol.bind(Some(psk.into())).ok())
812                }
813                _ => None,
814            },
815            WpaDescriptor::Wpa3 { .. } => match credential {
816                Credential::Password(passphrase) => Passphrase::try_from(passphrase.as_slice())
817                    .ok()
818                    .and_then(|passphrase| protocol.bind(Some(passphrase.into())).ok()),
819                _ => None,
820            },
821        },
822    }
823}
824
825/// Creates a security authenticator based on supported security protocols and credentials.
826///
827/// The authentication method is chosen based on the general strength of each mutually supported
828/// security protocol (the protocols supported by both the local and remote stations) and the
829/// compatibility of those protocols with the given credentials.
830///
831/// Returns `None` if no appropriate authentication method can be selected for the given protocols
832/// and credentials.
833pub fn select_authentication_method(
834    mutual_security_protocols: HashSet<SecurityDescriptor>,
835    credential: &Credential,
836) -> Option<SecurityAuthenticator> {
837    let mut protocols: Vec<_> = mutual_security_protocols.into_iter().collect();
838    protocols.sort_by_key(|protocol| {
839        Reverse(match protocol {
840            SecurityDescriptor::Open => 0,
841            SecurityDescriptor::Owe => 3,
842            SecurityDescriptor::Wep => 1,
843            SecurityDescriptor::Wpa(wpa) => match wpa {
844                WpaDescriptor::Wpa1 { .. } => 2,
845                WpaDescriptor::Wpa2 { .. } => 4,
846                WpaDescriptor::Wpa3 { .. } => 5,
847            },
848        })
849    });
850    protocols
851        .into_iter()
852        .flat_map(|protocol| bind_credential_to_protocol(protocol, credential))
853        .next()
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use crate::util::testing::{generate_string, random_connection_data};
860    use assert_matches::assert_matches;
861    use std::collections::VecDeque;
862    use test_case::test_case;
863    use wlan_common::security::wep::WepAuthenticator;
864    use wlan_common::security::wpa::{
865        Authentication, Wpa1Credentials, Wpa2PersonalCredentials, Wpa3PersonalCredentials,
866        WpaAuthenticator,
867    };
868
869    #[fuchsia::test]
870    fn new_network_config_none_credential() {
871        let credential = Credential::None;
872        let network_config = NetworkConfig::new(
873            NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(),
874            credential.clone(),
875            false,
876            None,
877        )
878        .expect("Error creating network config for foo");
879
880        assert_eq!(
881            network_config,
882            NetworkConfig {
883                ssid: client_types::Ssid::try_from("foo").unwrap(),
884                security_type: SecurityType::None,
885                credential,
886                has_ever_connected: false,
887                hidden_probability: PROB_HIDDEN_DEFAULT,
888                hidden_probability_stats: HiddenProbabilityStats::new(),
889                perf_stats: PerformanceStats::new(),
890                scan_stats: ScanStats::new(),
891            }
892        );
893    }
894
895    #[fuchsia::test]
896    fn new_network_config_with_hidden_prob_some() {
897        let credential = Credential::None;
898        let network_config = NetworkConfig::new(
899            NetworkIdentifier::try_from("foo", SecurityType::None).unwrap(),
900            credential.clone(),
901            false,
902            Some(0.05),
903        )
904        .expect("Error creating network config for foo");
905
906        assert_eq!(network_config.hidden_probability, 0.05);
907    }
908
909    #[fuchsia::test]
910    fn new_network_config_password_credential() {
911        let credential = Credential::Password(b"foo-password".to_vec());
912
913        let network_config = NetworkConfig::new(
914            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
915            credential.clone(),
916            false,
917            None,
918        )
919        .expect("Error creating network config for foo");
920
921        assert_eq!(
922            network_config,
923            NetworkConfig {
924                ssid: client_types::Ssid::try_from("foo").unwrap(),
925                security_type: SecurityType::Wpa2,
926                credential,
927                has_ever_connected: false,
928                hidden_probability: PROB_HIDDEN_DEFAULT,
929                hidden_probability_stats: HiddenProbabilityStats::new(),
930                perf_stats: PerformanceStats::new(),
931                scan_stats: ScanStats::new(),
932            }
933        );
934        assert!(network_config.perf_stats.connect_failures.0.is_empty());
935    }
936
937    #[fuchsia::test]
938    fn new_network_config_psk_credential() {
939        let credential = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
940
941        let network_config = NetworkConfig::new(
942            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
943            credential.clone(),
944            false,
945            None,
946        )
947        .expect("Error creating network config for foo");
948
949        assert_eq!(
950            network_config,
951            NetworkConfig {
952                ssid: client_types::Ssid::try_from("foo").unwrap(),
953                security_type: SecurityType::Wpa2,
954                credential,
955                has_ever_connected: false,
956                hidden_probability: PROB_HIDDEN_DEFAULT,
957                hidden_probability_stats: HiddenProbabilityStats::new(),
958                perf_stats: PerformanceStats::new(),
959                scan_stats: ScanStats::new(),
960            }
961        );
962    }
963
964    #[fuchsia::test]
965    fn new_network_config_invalid_password() {
966        let credential = Credential::Password([1; 64].to_vec());
967
968        let config_result = NetworkConfig::new(
969            NetworkIdentifier::try_from("foo", SecurityType::Wpa).unwrap(),
970            credential,
971            false,
972            None,
973        );
974
975        assert_matches!(config_result, Err(NetworkConfigError::PasswordLen));
976    }
977
978    #[fuchsia::test]
979    fn new_network_config_invalid_psk() {
980        let credential = Credential::Psk(b"bar".to_vec());
981
982        let config_result = NetworkConfig::new(
983            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
984            credential,
985            false,
986            None,
987        );
988
989        assert_matches!(config_result, Err(NetworkConfigError::PskLen));
990    }
991
992    #[fuchsia::test]
993    fn check_config_errors_invalid_wep_password() {
994        // Unsupported length (7).
995        let password = Credential::Password(b"1234567".to_vec());
996        assert_matches!(
997            check_config_errors(
998                &client_types::Ssid::try_from("valid_ssid").unwrap(),
999                &SecurityType::Wep,
1000                &password
1001            ),
1002            Err(NetworkConfigError::PasswordLen)
1003        );
1004    }
1005
1006    #[fuchsia::test]
1007    fn check_config_errors_invalid_wpa_password() {
1008        // password too short
1009        let short_password = Credential::Password(b"1234567".to_vec());
1010        assert_matches!(
1011            check_config_errors(
1012                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1013                &SecurityType::Wpa2,
1014                &short_password
1015            ),
1016            Err(NetworkConfigError::PasswordLen)
1017        );
1018
1019        // password too long
1020        let long_password = Credential::Password([5, 65].to_vec());
1021        assert_matches!(
1022            check_config_errors(
1023                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1024                &SecurityType::Wpa2,
1025                &long_password
1026            ),
1027            Err(NetworkConfigError::PasswordLen)
1028        );
1029    }
1030
1031    #[fuchsia::test]
1032    fn check_config_errors_invalid_wep_credential_variant() {
1033        // Unsupported variant (`Psk`).
1034        let psk = Credential::Psk(b"12345".to_vec());
1035        assert_matches!(
1036            check_config_errors(
1037                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1038                &SecurityType::Wep,
1039                &psk
1040            ),
1041            Err(NetworkConfigError::MissingPasswordPsk)
1042        );
1043    }
1044
1045    #[fuchsia::test]
1046    fn check_config_errors_invalid_wpa_psk() {
1047        // PSK length not 32 characters
1048        let short_psk = Credential::Psk([6; WPA_PSK_BYTE_LEN - 1].to_vec());
1049
1050        assert_matches!(
1051            check_config_errors(
1052                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1053                &SecurityType::Wpa2,
1054                &short_psk
1055            ),
1056            Err(NetworkConfigError::PskLen)
1057        );
1058
1059        let long_psk = Credential::Psk([7; WPA_PSK_BYTE_LEN + 1].to_vec());
1060        assert_matches!(
1061            check_config_errors(
1062                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1063                &SecurityType::Wpa2,
1064                &long_psk
1065            ),
1066            Err(NetworkConfigError::PskLen)
1067        );
1068    }
1069
1070    #[fuchsia::test]
1071    fn check_config_errors_invalid_security_credential() {
1072        // Use a password with open network.
1073        let password = Credential::Password(b"password".to_vec());
1074        assert_matches!(
1075            check_config_errors(
1076                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1077                &SecurityType::None,
1078                &password
1079            ),
1080            Err(NetworkConfigError::OpenNetworkPassword)
1081        );
1082
1083        let psk = Credential::Psk([1; WPA_PSK_BYTE_LEN].to_vec());
1084        assert_matches!(
1085            check_config_errors(
1086                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1087                &SecurityType::None,
1088                &psk
1089            ),
1090            Err(NetworkConfigError::OpenNetworkPassword)
1091        );
1092        // Use no password with a protected network.
1093        let password = Credential::None;
1094        assert_matches!(
1095            check_config_errors(
1096                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1097                &SecurityType::Wpa,
1098                &password
1099            ),
1100            Err(NetworkConfigError::MissingPasswordPsk)
1101        );
1102
1103        assert_matches!(
1104            check_config_errors(
1105                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1106                &SecurityType::Wpa2,
1107                &password
1108            ),
1109            Err(NetworkConfigError::MissingPasswordPsk)
1110        );
1111
1112        assert_matches!(
1113            check_config_errors(
1114                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1115                &SecurityType::Wpa3,
1116                &password
1117            ),
1118            Err(NetworkConfigError::MissingPasswordPsk)
1119        );
1120
1121        assert_matches!(
1122            check_config_errors(
1123                &client_types::Ssid::try_from("valid_ssid").unwrap(),
1124                &SecurityType::Wpa3,
1125                &psk
1126            ),
1127            Err(NetworkConfigError::Wpa3Psk)
1128        );
1129    }
1130
1131    #[fuchsia::test]
1132    fn check_config_errors_ssid_empty() {
1133        assert_matches!(
1134            check_config_errors(
1135                &client_types::Ssid::empty(),
1136                &SecurityType::None,
1137                &Credential::None
1138            ),
1139            Err(NetworkConfigError::SsidEmpty)
1140        );
1141    }
1142
1143    #[fasync::run_singlethreaded(test)]
1144    async fn test_connect_failures_by_bssid_add_and_get() {
1145        let mut connect_failures = HistoricalListsByBssid::new();
1146        let curr_time = fasync::MonotonicInstant::now();
1147
1148        // Add two failures for BSSID_1
1149        let bssid_1 = client_types::Bssid::from([1; 6]);
1150        let failure_1_bssid_1 = ConnectFailure {
1151            time: curr_time - zx::MonotonicDuration::from_seconds(10),
1152            bssid: bssid_1,
1153            reason: FailureReason::GeneralFailure,
1154        };
1155        connect_failures.add(bssid_1, failure_1_bssid_1);
1156
1157        let failure_2_bssid_1 = ConnectFailure {
1158            time: curr_time - zx::MonotonicDuration::from_seconds(5),
1159            bssid: bssid_1,
1160            reason: FailureReason::CredentialRejected,
1161        };
1162        connect_failures.add(bssid_1, failure_2_bssid_1);
1163
1164        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1165        assert_eq!(
1166            connect_failures
1167                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1168            vec![failure_1_bssid_1, failure_2_bssid_1]
1169        );
1170
1171        // Add one failure for BSSID_2
1172        let bssid_2 = client_types::Bssid::from([2; 6]);
1173        let failure_1_bssid_2 = ConnectFailure {
1174            time: curr_time - zx::MonotonicDuration::from_seconds(3),
1175            bssid: bssid_2,
1176            reason: FailureReason::GeneralFailure,
1177        };
1178        connect_failures.add(bssid_2, failure_1_bssid_2);
1179
1180        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1181        assert_eq!(
1182            connect_failures
1183                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1184            vec![failure_1_bssid_1, failure_2_bssid_1, failure_1_bssid_2]
1185        );
1186
1187        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1188        assert_eq!(
1189            connect_failures
1190                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1191            vec![failure_2_bssid_1, failure_1_bssid_2]
1192        );
1193
1194        // Verify get_recent_for_network(curr_time) is empty
1195        assert_eq!(connect_failures.get_recent_for_network(curr_time), vec![]);
1196
1197        // Verify get_list_for_bss retrieves correct connect failures
1198        assert_eq!(
1199            connect_failures.get_list_for_bss(&bssid_1),
1200            HistoricalList(VecDeque::from_iter([failure_1_bssid_1, failure_2_bssid_1]))
1201        );
1202
1203        assert_eq!(
1204            connect_failures.get_list_for_bss(&bssid_2),
1205            HistoricalList(VecDeque::from_iter([failure_1_bssid_2]))
1206        );
1207    }
1208
1209    #[fasync::run_singlethreaded(test)]
1210    async fn failure_list_add_and_get() {
1211        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1212
1213        // Get time before adding so we can get back everything we added.
1214        let curr_time = fasync::MonotonicInstant::now();
1215        assert!(connect_failures.get_recent(curr_time).is_empty());
1216        let bssid = client_types::Bssid::from([1; 6]);
1217        let failure =
1218            ConnectFailure { time: curr_time, bssid, reason: FailureReason::GeneralFailure };
1219        connect_failures.add(failure);
1220
1221        let result_list = connect_failures.get_recent(curr_time);
1222        assert_eq!(1, result_list.len());
1223        assert_eq!(FailureReason::GeneralFailure, result_list[0].reason);
1224        assert_eq!(bssid, result_list[0].bssid);
1225        // Should not get any results if we request denials older than the specified time.
1226        let later_time = fasync::MonotonicInstant::now();
1227        assert!(connect_failures.get_recent(later_time).is_empty());
1228    }
1229
1230    #[fasync::run_singlethreaded(test)]
1231    async fn test_failure_list_add_when_full() {
1232        let mut connect_failures = HistoricalList::new(NUM_CONNECTION_RESULTS_PER_BSS);
1233        let curr_time = fasync::MonotonicInstant::now();
1234
1235        // Add to list, exceeding the capacity by one entry
1236        for i in 0..connect_failures.0.capacity() + 1 {
1237            connect_failures.add(ConnectFailure {
1238                time: curr_time + zx::MonotonicDuration::from_seconds(i as i64),
1239                reason: FailureReason::GeneralFailure,
1240                bssid: client_types::Bssid::from([1; 6]),
1241            })
1242        }
1243
1244        // Validate entry with time = curr_time was evicted.
1245        for (i, e) in connect_failures.0.iter().enumerate() {
1246            assert_eq!(e.time, curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1));
1247        }
1248    }
1249
1250    #[fasync::run_singlethreaded(test)]
1251    async fn test_past_connections_by_bssid_add_and_get() {
1252        let mut past_connections_list = HistoricalListsByBssid::new();
1253        let curr_time = fasync::MonotonicInstant::now();
1254
1255        // Add two past_connections for BSSID_1
1256        let mut data_1_bssid_1 = random_connection_data();
1257        let bssid_1 = data_1_bssid_1.bssid;
1258        data_1_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(10);
1259
1260        past_connections_list.add(bssid_1, data_1_bssid_1);
1261
1262        let mut data_2_bssid_1 = random_connection_data();
1263        data_2_bssid_1.bssid = bssid_1;
1264        data_2_bssid_1.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(5);
1265        past_connections_list.add(bssid_1, data_2_bssid_1);
1266
1267        // Verify get_recent_for_network(curr_time - 10) retrieves both entries
1268        assert_eq!(
1269            past_connections_list
1270                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1271            vec![data_1_bssid_1, data_2_bssid_1]
1272        );
1273
1274        // Add one past_connection for BSSID_2
1275        let mut data_1_bssid_2 = random_connection_data();
1276        let bssid_2 = data_1_bssid_2.bssid;
1277        data_1_bssid_2.disconnect_time = curr_time - zx::MonotonicDuration::from_seconds(3);
1278        past_connections_list.add(bssid_2, data_1_bssid_2);
1279
1280        // Verify get_recent_for_network(curr_time - 10) includes entries from both BSSIDs
1281        assert_eq!(
1282            past_connections_list
1283                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(10)),
1284            vec![data_1_bssid_1, data_2_bssid_1, data_1_bssid_2]
1285        );
1286
1287        // Verify get_recent_for_network(curr_time - 9) excludes older entries
1288        assert_eq!(
1289            past_connections_list
1290                .get_recent_for_network(curr_time - zx::MonotonicDuration::from_seconds(9)),
1291            vec![data_2_bssid_1, data_1_bssid_2]
1292        );
1293
1294        // Verify get_recent_for_network(curr_time) is empty
1295        assert_eq!(past_connections_list.get_recent_for_network(curr_time), vec![]);
1296
1297        // Verify get_list_for_bss retrieves correct PastConnectionLists
1298        assert_eq!(
1299            past_connections_list.get_list_for_bss(&bssid_1),
1300            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_1, data_2_bssid_1]) }
1301        );
1302
1303        assert_eq!(
1304            past_connections_list.get_list_for_bss(&bssid_2),
1305            PastConnectionList { 0: VecDeque::from_iter([data_1_bssid_2]) }
1306        );
1307    }
1308
1309    #[fasync::run_singlethreaded(test)]
1310    async fn test_past_connections_list_add_when_full() {
1311        let mut past_connections_list = PastConnectionList::default();
1312        let curr_time = fasync::MonotonicInstant::now();
1313
1314        // Add to list, exceeding the capacity by one entry
1315        for i in 0..past_connections_list.0.capacity() + 1 {
1316            let mut data = random_connection_data();
1317            data.bssid = client_types::Bssid::from([1; 6]);
1318            data.disconnect_time = curr_time + zx::MonotonicDuration::from_seconds(i as i64);
1319            past_connections_list.add(data);
1320        }
1321
1322        // Validate entry with time = curr_time was evicted.
1323        for (i, e) in past_connections_list.0.iter().enumerate() {
1324            assert_eq!(
1325                e.disconnect_time,
1326                curr_time + zx::MonotonicDuration::from_seconds(i as i64 + 1)
1327            );
1328        }
1329    }
1330
1331    #[fasync::run_singlethreaded(test)]
1332    async fn test_past_connections_list_add_and_get() {
1333        let mut past_connections_list = PastConnectionList::default();
1334        let curr_time = fasync::MonotonicInstant::now();
1335        assert!(past_connections_list.get_recent(curr_time).is_empty());
1336
1337        let mut past_connection_data = random_connection_data();
1338        past_connection_data.disconnect_time = curr_time;
1339        // Add a past connection
1340        past_connections_list.add(past_connection_data);
1341
1342        // We should get back the added data when specifying the same or an earlier time.
1343        assert_eq!(past_connections_list.get_recent(curr_time).len(), 1);
1344        assert_matches!(past_connections_list.get_recent(curr_time).as_slice(), [data] => {
1345            assert_eq!(data, &past_connection_data.clone());
1346        });
1347        let earlier_time = curr_time - zx::MonotonicDuration::from_seconds(1);
1348        assert_matches!(past_connections_list.get_recent(earlier_time).as_slice(), [data] => {
1349            assert_eq!(data, &data.clone());
1350        });
1351        // The results should be empty if the requested time is after the latest past connection's
1352        // time.
1353        let later_time = curr_time + zx::MonotonicDuration::from_seconds(1);
1354        assert!(past_connections_list.get_recent(later_time).is_empty());
1355    }
1356
1357    #[fuchsia::test]
1358    fn test_credential_from_bytes() {
1359        assert_eq!(Credential::from_bytes(vec![1]), Credential::Password(vec![1]));
1360        assert_eq!(Credential::from_bytes(vec![2; 63]), Credential::Password(vec![2; 63]));
1361        // credential from bytes should only be used to load legacy data, so PSK won't be supported
1362        assert_eq!(
1363            Credential::from_bytes(vec![2; WPA_PSK_BYTE_LEN]),
1364            Credential::Password(vec![2; WPA_PSK_BYTE_LEN])
1365        );
1366        assert_eq!(Credential::from_bytes(vec![]), Credential::None);
1367    }
1368
1369    #[fuchsia::test]
1370    fn test_derived_security_type_from_credential() {
1371        let password = Credential::Password(b"password".to_vec());
1372        let psk = Credential::Psk(b"psk-type".to_vec());
1373        let none = Credential::None;
1374
1375        assert_eq!(SecurityType::Wpa2, password.derived_security_type());
1376        assert_eq!(SecurityType::Wpa2, psk.derived_security_type());
1377        assert_eq!(SecurityType::None, none.derived_security_type());
1378    }
1379
1380    #[fuchsia::test]
1381    fn test_hidden_prob_calculation() {
1382        let mut network_config = NetworkConfig::new(
1383            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1384            Credential::None,
1385            false,
1386            None,
1387        )
1388        .expect("Failed to create network config");
1389        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_DEFAULT);
1390
1391        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1392        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1393
1394        network_config.update_hidden_prob(HiddenProbEvent::ConnectPassive);
1395        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1396
1397        // Hidden probability shouldn't go back up after seeing a network in a passive
1398        // scan again after connecting with a passive scan
1399        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1400        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_PASSIVE);
1401    }
1402
1403    #[fuchsia::test]
1404    fn test_hidden_prob_calc_active_connect() {
1405        let mut network_config = NetworkConfig::new(
1406            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1407            Credential::None,
1408            false,
1409            None,
1410        )
1411        .expect("Failed to create network config");
1412
1413        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1414        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1415
1416        // If we see a network in a passive scan after connecting from an active scan,
1417        // we won't care that we previously needed an active scan.
1418        network_config.update_hidden_prob(HiddenProbEvent::SeenPassive);
1419        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_SEEN_PASSIVE);
1420
1421        // If we require an active scan to connect to a network, raise probability as if the
1422        // network has become hidden.
1423        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1424        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1425    }
1426
1427    #[fuchsia::test]
1428    fn test_hidden_prob_calc_not_seen_in_active_scan_lowers_prob() {
1429        // Test that updating hidden probability after not seeing the network in a directed active
1430        // scan lowers the hidden probability
1431        let mut network_config = NetworkConfig::new(
1432            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1433            Credential::None,
1434            false,
1435            None,
1436        )
1437        .expect("Failed to create network config");
1438
1439        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1440        let expected_prob = PROB_HIDDEN_DEFAULT - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1441        assert_eq!(network_config.hidden_probability, expected_prob);
1442
1443        // If we update hidden probability again, the probability should lower again.
1444        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1445        let expected_prob = expected_prob - PROB_HIDDEN_INCREMENT_NOT_SEEN_ACTIVE;
1446        assert_eq!(network_config.hidden_probability, expected_prob);
1447    }
1448
1449    #[fuchsia::test]
1450    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_lower_past_threshold() {
1451        let mut network_config = NetworkConfig::new(
1452            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1453            Credential::None,
1454            false,
1455            None,
1456        )
1457        .expect("Failed to create network config");
1458
1459        // If hidden probability is slightly above the minimum from not seing the network in an
1460        // active scan, it should not be lowered past the minimum.
1461        network_config.hidden_probability = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE + 0.01;
1462        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1463        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1464
1465        // If hidden probability is at the minimum, it should not be lowered.
1466        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1467        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE);
1468    }
1469
1470    #[fuchsia::test]
1471    fn test_hidden_prob_calc_not_seen_in_active_scan_does_not_change_if_lower_than_threshold() {
1472        let mut network_config = NetworkConfig::new(
1473            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1474            Credential::None,
1475            false,
1476            None,
1477        )
1478        .expect("Failed to create network config");
1479
1480        // If the hidden probability is lower than the minimum of not seeing the network in an,
1481        // active scan, which could happen after seeing it in a passive scan, the hidden
1482        // probability will not lower from this event.
1483        let prob_before_update = PROB_HIDDEN_MIN_FROM_NOT_SEEN_ACTIVE - 0.1;
1484        network_config.hidden_probability = prob_before_update;
1485        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1486        assert_eq!(network_config.hidden_probability, prob_before_update);
1487    }
1488
1489    #[fuchsia::test]
1490    fn test_hidden_prob_calc_not_seen_active_after_active_connect() {
1491        // Test the specific case where we fail to see the network in an active scan after we
1492        // previously connected to the network after an active scan was required.
1493        let mut network_config = NetworkConfig::new(
1494            NetworkIdentifier::try_from("some_ssid", SecurityType::None).unwrap(),
1495            Credential::None,
1496            false,
1497            None,
1498        )
1499        .expect("Failed to create network config");
1500
1501        network_config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1502        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1503
1504        // If we update the probability after a not-seen-in-active-scan, the probability should
1505        // still reflect that we think the network is hidden after the connect.
1506        network_config.update_hidden_prob(HiddenProbEvent::NotSeenActive);
1507        assert_eq!(network_config.hidden_probability, PROB_HIDDEN_IF_CONNECT_ACTIVE);
1508    }
1509
1510    #[fuchsia::test]
1511    fn test_is_hidden_implementation() {
1512        let mut config = NetworkConfig::new(
1513            NetworkIdentifier::try_from("foo", SecurityType::Wpa2).unwrap(),
1514            policy_wpa_password(),
1515            false,
1516            None,
1517        )
1518        .expect("Error creating network config for foo");
1519        config.update_hidden_prob(HiddenProbEvent::ConnectActive);
1520        assert!(config.is_hidden());
1521    }
1522
1523    fn policy_wep_key() -> Credential {
1524        Credential::Password("abcdef0000".as_bytes().to_vec())
1525    }
1526
1527    fn common_wep_key() -> WepKey {
1528        WepKey::parse("abcdef0000").unwrap()
1529    }
1530
1531    fn policy_wpa_password() -> Credential {
1532        Credential::Password("password".as_bytes().to_vec())
1533    }
1534
1535    fn common_wpa_password() -> Passphrase {
1536        Passphrase::try_from("password").unwrap()
1537    }
1538
1539    fn policy_wpa_psk() -> Credential {
1540        Credential::Psk(vec![0u8; WPA_PSK_BYTE_LEN])
1541    }
1542
1543    fn common_wpa_psk() -> Psk {
1544        Psk::from([0u8; WPA_PSK_BYTE_LEN])
1545    }
1546
1547    // Expect successful mapping in the following cases.
1548    #[test_case(
1549        [SecurityDescriptor::OPEN],
1550        Credential::None
1551        =>
1552        Some(SecurityAuthenticator::Open)
1553    )]
1554    #[test_case(
1555        [SecurityDescriptor::OWE],
1556        Credential::None
1557        =>
1558        Some(SecurityAuthenticator::Owe)
1559    )]
1560    #[test_case(
1561        [SecurityDescriptor::WEP],
1562        policy_wep_key()
1563        =>
1564        Some(SecurityAuthenticator::Wep(WepAuthenticator {
1565            key: common_wep_key(),
1566        }))
1567    )]
1568    #[test_case(
1569        [SecurityDescriptor::WPA1],
1570        policy_wpa_password()
1571        =>
1572        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa1 {
1573            credentials: Wpa1Credentials::Passphrase(common_wpa_password()),
1574        }))
1575    )]
1576    #[test_case(
1577        [SecurityDescriptor::OPEN, SecurityDescriptor::OWE],
1578        Credential::None
1579        =>
1580        Some(SecurityAuthenticator::Owe)
1581    )]
1582    #[test_case(
1583        [SecurityDescriptor::WPA1, SecurityDescriptor::WPA2_PERSONAL],
1584        policy_wpa_psk()
1585        =>
1586        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1587            cipher: None,
1588            authentication: Authentication::Personal(
1589                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1590            ),
1591        }))
1592    )]
1593    #[test_case(
1594        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1595        policy_wpa_password()
1596        =>
1597        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa3 {
1598            cipher: None,
1599            authentication: Authentication::Personal(
1600                Wpa3PersonalCredentials::Passphrase(common_wpa_password())
1601            ),
1602        }))
1603    )]
1604    #[test_case(
1605        [SecurityDescriptor::WPA2_PERSONAL],
1606        policy_wpa_password()
1607        =>
1608        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1609            cipher: None,
1610            authentication: Authentication::Personal(
1611                Wpa2PersonalCredentials::Passphrase(common_wpa_password())
1612            ),
1613        }))
1614    )]
1615    #[test_case(
1616        [SecurityDescriptor::WPA2_PERSONAL, SecurityDescriptor::WPA3_PERSONAL],
1617        policy_wpa_psk()
1618        =>
1619        Some(SecurityAuthenticator::Wpa(WpaAuthenticator::Wpa2 {
1620            cipher: None,
1621            authentication: Authentication::Personal(
1622                Wpa2PersonalCredentials::Psk(common_wpa_psk())
1623            ),
1624        }))
1625    )]
1626    // Expect failed mapping in the following cases.
1627    #[test_case(
1628        [SecurityDescriptor::WPA3_PERSONAL],
1629        policy_wpa_psk()
1630        =>
1631        None
1632    )]
1633    #[fuchsia::test(add_test_attr = false)]
1634    fn select_authentication_method_matrix(
1635        mutual_security_protocols: impl IntoIterator<Item = SecurityDescriptor>,
1636        credential: Credential,
1637    ) -> Option<SecurityAuthenticator> {
1638        super::select_authentication_method(
1639            mutual_security_protocols.into_iter().collect(),
1640            &credential,
1641        )
1642    }
1643
1644    #[test_case(SecurityType::None)]
1645    #[test_case(SecurityType::Wep)]
1646    #[test_case(SecurityType::Wpa)]
1647    #[test_case(SecurityType::Wpa2)]
1648    #[test_case(SecurityType::Wpa3)]
1649    fn test_security_type_list_includes_type(security: SecurityType) {
1650        let types = SecurityType::list_variants();
1651        assert!(types.contains(&security));
1652    }
1653
1654    // If this test doesn't compile, add the security type to this test and list_security_types().
1655    #[fuchsia::test]
1656    fn test_security_type_list_completeness() {
1657        // Any variant works here.
1658        let security = SecurityType::Wpa;
1659        // This will not compile if a new variant is added until this test is updated. Do not
1660        // a wildcard branch.
1661        match security {
1662            SecurityType::None => {}
1663            SecurityType::Wep => {}
1664            SecurityType::Wpa => {}
1665            SecurityType::Wpa2 => {}
1666            SecurityType::Wpa3 => {}
1667        }
1668    }
1669
1670    #[fuchsia::test]
1671    fn test_is_likely_single_bss() {
1672        let ssid = generate_string();
1673        let mut network_config = NetworkConfig::new(
1674            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1675            Credential::None,
1676            false,
1677            None,
1678        )
1679        .expect("Failed to create network config");
1680
1681        // Record that the network was seen with 1 BSS a few times.
1682        for _ in 0..5 {
1683            network_config.update_seen_multiple_bss(false);
1684        }
1685
1686        // Verify the network is considered single BSS.
1687        assert!(network_config.is_likely_single_bss());
1688    }
1689
1690    #[fuchsia::test]
1691    fn test_is_not_single_bss() {
1692        let ssid = generate_string();
1693        let mut network_config = NetworkConfig::new(
1694            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1695            Credential::None,
1696            false,
1697            None,
1698        )
1699        .expect("Failed to create network config");
1700
1701        // Record that the network was seen with multiple BSS a few times then one BSS once.
1702        for _ in 0..5 {
1703            network_config.update_seen_multiple_bss(true);
1704        }
1705        network_config.update_seen_multiple_bss(false);
1706
1707        // Verify the network is not considered single BSS.
1708        assert!(!network_config.is_likely_single_bss());
1709    }
1710
1711    #[fuchsia::test]
1712    fn test_cannot_yet_determine_single_bss() {
1713        let ssid = generate_string();
1714        let mut network_config = NetworkConfig::new(
1715            NetworkIdentifier::try_from(&ssid, SecurityType::None).unwrap(),
1716            Credential::None,
1717            false,
1718            None,
1719        )
1720        .expect("Failed to create network config");
1721
1722        // Record that the network was seen with a single BSS only a couple times.
1723        network_config.update_seen_multiple_bss(false);
1724        network_config.update_seen_multiple_bss(false);
1725
1726        // Verify the network is not considered likely single BSS.
1727        assert!(!network_config.is_likely_single_bss());
1728    }
1729}