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