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