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