wlan_sme/client/
mod.rs

1// Copyright 2021 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
5mod event;
6mod inspect;
7mod protection;
8mod rsn;
9mod scan;
10mod state;
11
12mod wpa;
13
14#[cfg(test)]
15pub mod test_utils;
16
17use self::event::Event;
18use self::protection::{Protection, SecurityContext};
19use self::scan::{DiscoveryScan, ScanScheduler};
20use self::state::{ClientState, ConnectCommand};
21use crate::responder::Responder;
22use crate::{Config, MlmeRequest, MlmeSink, MlmeStream};
23use fuchsia_inspect_auto_persist::{self as auto_persist, AutoPersist};
24use futures::channel::{mpsc, oneshot};
25use ieee80211::{Bssid, MacAddrBytes, Ssid};
26use log::{error, info, warn};
27use std::sync::Arc;
28use wlan_common::bss::{BssDescription, Protection as BssProtection};
29use wlan_common::capabilities::derive_join_capabilities;
30use wlan_common::ie::rsn::rsne;
31use wlan_common::ie::{self, wsc};
32use wlan_common::mac::MacRole;
33use wlan_common::scan::{Compatibility, Compatible, Incompatible, ScanResult};
34use wlan_common::security::{SecurityAuthenticator, SecurityDescriptor};
35use wlan_common::sink::UnboundedSink;
36use wlan_common::timer;
37use wlan_rsn::auth;
38use {
39    fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211,
40    fidl_fuchsia_wlan_internal as fidl_internal, fidl_fuchsia_wlan_mlme as fidl_mlme,
41    fidl_fuchsia_wlan_sme as fidl_sme, fidl_fuchsia_wlan_stats as fidl_stats,
42};
43
44// This is necessary to trick the private-in-public checker.
45// A private module is not allowed to include private types in its interface,
46// even though the module itself is private and will never be exported.
47// As a workaround, we add another private module with public types.
48mod internal {
49    use crate::client::event::Event;
50    use crate::client::{inspect, ConnectionAttemptId};
51    use crate::MlmeSink;
52    use std::sync::Arc;
53    use wlan_common::timer::Timer;
54    use {fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_mlme as fidl_mlme};
55
56    pub struct Context {
57        pub device_info: Arc<fidl_mlme::DeviceInfo>,
58        pub mlme_sink: MlmeSink,
59        pub(crate) timer: Timer<Event>,
60        pub att_id: ConnectionAttemptId,
61        pub(crate) inspect: Arc<inspect::SmeTree>,
62        pub security_support: fidl_common::SecuritySupport,
63    }
64}
65
66use self::internal::*;
67
68// An automatically increasing sequence number that uniquely identifies a logical
69// connection attempt. For example, a new connection attempt can be triggered
70// by a DisassociateInd message from the MLME.
71pub type ConnectionAttemptId = u64;
72
73pub type ScanTxnId = u64;
74
75#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
76pub struct ClientConfig {
77    cfg: Config,
78    pub wpa3_supported: bool,
79}
80
81impl ClientConfig {
82    pub fn from_config(cfg: Config, wpa3_supported: bool) -> Self {
83        Self { cfg, wpa3_supported }
84    }
85
86    /// Converts a given BssDescription into a ScanResult.
87    pub fn create_scan_result(
88        &self,
89        timestamp: zx::MonotonicInstant,
90        bss_description: BssDescription,
91        device_info: &fidl_mlme::DeviceInfo,
92        security_support: &fidl_common::SecuritySupport,
93    ) -> ScanResult {
94        ScanResult {
95            compatibility: self.bss_compatibility(&bss_description, device_info, security_support),
96            timestamp,
97            bss_description,
98        }
99    }
100
101    /// Gets the compatible modes of operation of the BSS with respect to driver and hardware
102    /// support.
103    ///
104    /// Returns `None` if the BSS is not supported by the client.
105    pub fn bss_compatibility(
106        &self,
107        bss: &BssDescription,
108        device_info: &fidl_mlme::DeviceInfo,
109        security_support: &fidl_common::SecuritySupport,
110    ) -> Compatibility {
111        // TODO(https://fxbug.dev/384797729): Include information about disjoint channels and data
112        //                                    rates in `Incompatible`.
113        self.has_compatible_channel_and_data_rates(bss, device_info)
114            .then(|| {
115                Compatible::try_from_features(
116                    self.security_protocol_intersection(bss, security_support),
117                )
118            })
119            .flatten()
120            .ok_or_else(|| {
121                Incompatible::try_from_features(
122                    "incompatible channel, PHY data rates, or security protocols",
123                    Some(self.security_protocols_by_mac_role(bss)),
124                )
125                .unwrap_or_else(|| {
126                    Incompatible::from_description("incompatible channel or PHY data rates")
127                })
128            })
129    }
130
131    /// Gets the intersection of security protocols supported by the BSS and local interface.
132    ///
133    /// Security protocol support of the local interface is determined by the given
134    /// `SecuritySupport`. The set of mutually supported protocols may be empty.
135    fn security_protocol_intersection(
136        &self,
137        bss: &BssDescription,
138        security_support: &fidl_common::SecuritySupport,
139    ) -> Vec<SecurityDescriptor> {
140        // Construct queries for security protocol support based on hardware, driver, and BSS
141        // compatibility.
142        let has_privacy = wlan_common::mac::CapabilityInfo(bss.capability_info).privacy();
143        let has_wep_support = || self.cfg.wep_supported;
144        let has_wpa1_support = || self.cfg.wpa1_supported;
145        let has_wpa2_support = || {
146            // TODO(https://fxbug.dev/42059694): Unlike other protocols, hardware and driver
147            //                                   support for WPA2 is assumed here. Query and track
148            //                                   this as with other security protocols.
149            has_privacy
150                && bss.rsne().is_some_and(|rsne| {
151                    rsne::from_bytes(rsne)
152                        .is_ok_and(|(_, a_rsne)| a_rsne.is_wpa2_rsn_compatible(security_support))
153                })
154        };
155        let has_wpa3_support = || {
156            self.wpa3_supported
157                && has_privacy
158                && bss.rsne().is_some_and(|rsne| {
159                    rsne::from_bytes(rsne)
160                        .is_ok_and(|(_, a_rsne)| a_rsne.is_wpa3_rsn_compatible(security_support))
161                })
162        };
163
164        // Determine security protocol compatibility. This `match` expression does not use guard
165        // expressions to avoid implicit patterns like `_`, which may introduce bugs if
166        // `BssProtection` changes. This expression orders protocols from a loose notion of most
167        // secure to least secure, though the APIs that expose this data provide no such guarantee.
168        match bss.protection() {
169            BssProtection::Open => vec![SecurityDescriptor::OPEN],
170            BssProtection::Wep if has_wep_support() => vec![SecurityDescriptor::WEP],
171            BssProtection::Wep => vec![],
172            BssProtection::Wpa1 if has_wpa1_support() => vec![SecurityDescriptor::WPA1],
173            BssProtection::Wpa1 => vec![],
174            BssProtection::Wpa1Wpa2PersonalTkipOnly | BssProtection::Wpa1Wpa2Personal => {
175                has_wpa2_support()
176                    .then_some(SecurityDescriptor::WPA2_PERSONAL)
177                    .into_iter()
178                    .chain(has_wpa1_support().then_some(SecurityDescriptor::WPA1))
179                    .collect()
180            }
181            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal
182                if has_wpa2_support() =>
183            {
184                vec![SecurityDescriptor::WPA2_PERSONAL]
185            }
186            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal => vec![],
187            BssProtection::Wpa2Wpa3Personal => match (has_wpa3_support(), has_wpa2_support()) {
188                (true, true) => {
189                    vec![SecurityDescriptor::WPA3_PERSONAL, SecurityDescriptor::WPA2_PERSONAL]
190                }
191                (true, false) => vec![SecurityDescriptor::WPA3_PERSONAL],
192                (false, true) => vec![SecurityDescriptor::WPA2_PERSONAL],
193                (false, false) => vec![],
194            },
195            BssProtection::Wpa3Personal if has_wpa3_support() => {
196                vec![SecurityDescriptor::WPA3_PERSONAL]
197            }
198            BssProtection::Wpa3Personal => vec![],
199            // TODO(https://fxbug.dev/42174395): Implement conversions for WPA Enterprise protocols.
200            BssProtection::Wpa2Enterprise | BssProtection::Wpa3Enterprise => vec![],
201            BssProtection::Unknown => vec![],
202        }
203    }
204
205    fn security_protocols_by_mac_role(
206        &self,
207        bss: &BssDescription,
208    ) -> impl Iterator<Item = (SecurityDescriptor, MacRole)> {
209        let has_privacy = wlan_common::mac::CapabilityInfo(bss.capability_info).privacy();
210        let has_wep_support = || self.cfg.wep_supported;
211        let has_wpa1_support = || self.cfg.wpa1_supported;
212        let has_wpa2_support = || {
213            // TODO(https://fxbug.dev/42059694): Unlike other protocols, hardware and driver
214            //                                   support for WPA2 is assumed here. Query and track
215            //                                   this as with other security protocols.
216            has_privacy
217        };
218        let has_wpa3_support = || self.wpa3_supported && has_privacy;
219        let client_security_protocols = Some(SecurityDescriptor::OPEN)
220            .into_iter()
221            .chain(has_wep_support().then_some(SecurityDescriptor::WEP))
222            .chain(has_wpa1_support().then_some(SecurityDescriptor::WPA1))
223            .chain(has_wpa2_support().then_some(SecurityDescriptor::WPA2_PERSONAL))
224            .chain(has_wpa3_support().then_some(SecurityDescriptor::WPA3_PERSONAL))
225            .map(|descriptor| (descriptor, MacRole::Client));
226
227        let bss_security_protocols = match bss.protection() {
228            BssProtection::Open => &[SecurityDescriptor::OPEN][..],
229            BssProtection::Wep => &[SecurityDescriptor::WEP][..],
230            BssProtection::Wpa1 => &[SecurityDescriptor::WPA1][..],
231            BssProtection::Wpa1Wpa2PersonalTkipOnly | BssProtection::Wpa1Wpa2Personal => {
232                &[SecurityDescriptor::WPA1, SecurityDescriptor::WPA2_PERSONAL][..]
233            }
234            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal => {
235                &[SecurityDescriptor::WPA2_PERSONAL][..]
236            }
237            BssProtection::Wpa2Wpa3Personal => {
238                &[SecurityDescriptor::WPA3_PERSONAL, SecurityDescriptor::WPA2_PERSONAL][..]
239            }
240            BssProtection::Wpa3Personal => &[SecurityDescriptor::WPA3_PERSONAL][..],
241            // TODO(https://fxbug.dev/42174395): Implement conversions for WPA Enterprise protocols.
242            BssProtection::Wpa2Enterprise | BssProtection::Wpa3Enterprise => &[],
243            BssProtection::Unknown => &[],
244        }
245        .iter()
246        .cloned()
247        .map(|descriptor| (descriptor, MacRole::Ap));
248
249        client_security_protocols.chain(bss_security_protocols)
250    }
251
252    fn has_compatible_channel_and_data_rates(
253        &self,
254        bss: &BssDescription,
255        device_info: &fidl_mlme::DeviceInfo,
256    ) -> bool {
257        derive_join_capabilities(bss.channel, bss.rates(), device_info).is_ok()
258    }
259}
260
261pub struct ClientSme {
262    cfg: ClientConfig,
263    state: Option<ClientState>,
264    scan_sched: ScanScheduler<Responder<Result<Vec<ScanResult>, fidl_mlme::ScanResultCode>>>,
265    wmm_status_responders: Vec<Responder<fidl_sme::ClientSmeWmmStatusResult>>,
266    auto_persist_last_pulse: AutoPersist<()>,
267    context: Context,
268}
269
270#[derive(Debug, PartialEq)]
271pub enum ConnectResult {
272    Success,
273    Canceled,
274    Failed(ConnectFailure),
275}
276
277impl<T: Into<ConnectFailure>> From<T> for ConnectResult {
278    fn from(failure: T) -> Self {
279        ConnectResult::Failed(failure.into())
280    }
281}
282
283#[allow(clippy::large_enum_variant)] // TODO(https://fxbug.dev/401087337)
284#[derive(Debug, PartialEq)]
285pub enum RoamResult {
286    Success(Box<BssDescription>),
287    Failed(RoamFailure),
288}
289
290impl<T: Into<RoamFailure>> From<T> for RoamResult {
291    fn from(failure: T) -> Self {
292        RoamResult::Failed(failure.into())
293    }
294}
295
296#[derive(Debug)]
297pub struct ConnectTransactionSink {
298    sink: UnboundedSink<ConnectTransactionEvent>,
299    is_reconnecting: bool,
300}
301
302impl ConnectTransactionSink {
303    pub fn new_unbounded() -> (Self, ConnectTransactionStream) {
304        let (sender, receiver) = mpsc::unbounded();
305        let sink =
306            ConnectTransactionSink { sink: UnboundedSink::new(sender), is_reconnecting: false };
307        (sink, receiver)
308    }
309
310    pub fn is_reconnecting(&self) -> bool {
311        self.is_reconnecting
312    }
313
314    pub fn send_connect_result(&mut self, result: ConnectResult) {
315        let event =
316            ConnectTransactionEvent::OnConnectResult { result, is_reconnect: self.is_reconnecting };
317        self.send(event);
318    }
319
320    pub fn send_roam_result(&mut self, result: RoamResult) {
321        let event = ConnectTransactionEvent::OnRoamResult { result };
322        self.send(event);
323    }
324
325    pub fn send(&mut self, event: ConnectTransactionEvent) {
326        if let ConnectTransactionEvent::OnDisconnect { info } = &event {
327            self.is_reconnecting = info.is_sme_reconnecting;
328        };
329        self.sink.send(event);
330    }
331}
332
333pub type ConnectTransactionStream = mpsc::UnboundedReceiver<ConnectTransactionEvent>;
334
335#[allow(clippy::large_enum_variant)] // TODO(https://fxbug.dev/401087337)
336#[derive(Debug, PartialEq)]
337pub enum ConnectTransactionEvent {
338    OnConnectResult { result: ConnectResult, is_reconnect: bool },
339    OnRoamResult { result: RoamResult },
340    OnDisconnect { info: fidl_sme::DisconnectInfo },
341    OnSignalReport { ind: fidl_internal::SignalReportIndication },
342    OnChannelSwitched { info: fidl_internal::ChannelSwitchInfo },
343}
344
345#[derive(Debug, PartialEq)]
346pub enum ConnectFailure {
347    SelectNetworkFailure(SelectNetworkFailure),
348    // TODO(https://fxbug.dev/42147565): SME no longer performs scans when connecting. Remove the
349    //                        `ScanFailure` variant.
350    ScanFailure(fidl_mlme::ScanResultCode),
351    // TODO(https://fxbug.dev/42178810): `JoinFailure` and `AuthenticationFailure` no longer needed when
352    //                        state machine is fully transitioned to USME.
353    JoinFailure(fidl_ieee80211::StatusCode),
354    AuthenticationFailure(fidl_ieee80211::StatusCode),
355    AssociationFailure(AssociationFailure),
356    EstablishRsnaFailure(EstablishRsnaFailure),
357}
358
359impl ConnectFailure {
360    // TODO(https://fxbug.dev/42163244): ConnectFailure::is_timeout is not useful, remove it
361    #[allow(clippy::collapsible_match, reason = "mass allow for https://fxbug.dev/381896734")]
362    #[allow(
363        clippy::match_like_matches_macro,
364        reason = "mass allow for https://fxbug.dev/381896734"
365    )]
366    pub fn is_timeout(&self) -> bool {
367        // Note: For association, we don't have a failure type for timeout, so cannot deduce
368        //       whether an association failure is due to timeout.
369        match self {
370            ConnectFailure::AuthenticationFailure(failure) => match failure {
371                fidl_ieee80211::StatusCode::RejectedSequenceTimeout => true,
372                _ => false,
373            },
374            ConnectFailure::EstablishRsnaFailure(failure) => match failure {
375                EstablishRsnaFailure {
376                    reason: EstablishRsnaFailureReason::RsnaResponseTimeout(_),
377                    ..
378                }
379                | EstablishRsnaFailure {
380                    reason: EstablishRsnaFailureReason::RsnaCompletionTimeout(_),
381                    ..
382                } => true,
383                _ => false,
384            },
385            _ => false,
386        }
387    }
388
389    /// Returns true if failure was likely caused by rejected
390    /// credentials. In some cases, we cannot be 100% certain that
391    /// credentials were rejected, but it's worth noting when we
392    /// observe a failure event that was more than likely caused by
393    /// rejected credentials.
394    pub fn likely_due_to_credential_rejected(&self) -> bool {
395        match self {
396            // Assuming the correct type of credentials are given, a
397            // bad password will cause a variety of errors depending
398            // on the security type. All of the following cases assume
399            // no frames were dropped unintentionally. For example,
400            // it's possible to conflate a WPA2 bad password error
401            // with a dropped frame at just the right moment since the
402            // error itself is *caused by* a dropped frame.
403
404            // For WPA1 and WPA2, the error will be
405            // RsnaResponseTimeout or RsnaCompletionTimeout.  When
406            // the authenticator receives a bad MIC (derived from the
407            // password), it will silently drop the EAPOL handshake
408            // frame it received.
409            //
410            // NOTE: The alternative possibilities for seeing these
411            // errors are an error in our crypto parameter parsing and
412            // crypto implementation, or a lost connection with the AP.
413            ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
414                auth_method: Some(auth::MethodName::Psk),
415                reason:
416                    EstablishRsnaFailureReason::RsnaResponseTimeout(
417                        wlan_rsn::Error::LikelyWrongCredential,
418                    ),
419            })
420            | ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
421                auth_method: Some(auth::MethodName::Psk),
422                reason:
423                    EstablishRsnaFailureReason::RsnaCompletionTimeout(
424                        wlan_rsn::Error::LikelyWrongCredential,
425                    ),
426            }) => true,
427
428            // For WEP, the entire association is always handled by
429            // fullmac, so the best we can do is use
430            // fidl_mlme::AssociateResultCode. The code that arises
431            // when WEP fails with rejected credentials is
432            // RefusedReasonUnspecified. This is a catch-all error for
433            // a WEP authentication failure, but it is being
434            // considered good enough for catching rejected
435            // credentials for a deprecated WEP association.
436            ConnectFailure::AssociationFailure(AssociationFailure {
437                bss_protection: BssProtection::Wep,
438                code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
439            }) => true,
440
441            // For WPA3, the AP will not respond to SAE authentication frames
442            // if it detects an invalid credential, so we expect the connection
443            // attempt to time out.
444            ConnectFailure::AssociationFailure(AssociationFailure {
445                bss_protection: BssProtection::Wpa3Personal,
446                code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
447            })
448            | ConnectFailure::AssociationFailure(AssociationFailure {
449                bss_protection: BssProtection::Wpa2Wpa3Personal,
450                code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
451            }) => true,
452            _ => false,
453        }
454    }
455
456    pub fn status_code(&self) -> fidl_ieee80211::StatusCode {
457        match self {
458            ConnectFailure::JoinFailure(code)
459            | ConnectFailure::AuthenticationFailure(code)
460            | ConnectFailure::AssociationFailure(AssociationFailure { code, .. }) => *code,
461            ConnectFailure::EstablishRsnaFailure(..) => {
462                fidl_ieee80211::StatusCode::EstablishRsnaFailure
463            }
464            // SME no longer does join scan, so these two failures should no longer happen
465            ConnectFailure::ScanFailure(fidl_mlme::ScanResultCode::ShouldWait) => {
466                fidl_ieee80211::StatusCode::Canceled
467            }
468            ConnectFailure::SelectNetworkFailure(..) | ConnectFailure::ScanFailure(..) => {
469                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
470            }
471        }
472    }
473}
474
475#[derive(Debug, PartialEq)]
476pub enum RoamFailureType {
477    SelectNetworkFailure,
478    RoamStartMalformedFailure,
479    RoamResultMalformedFailure,
480    RoamRequestMalformedFailure,
481    RoamConfirmationMalformedFailure,
482    ReassociationFailure,
483    EstablishRsnaFailure,
484}
485
486#[derive(Debug, PartialEq)]
487pub struct RoamFailure {
488    failure_type: RoamFailureType,
489    pub selected_bssid: Bssid,
490    pub status_code: fidl_ieee80211::StatusCode,
491    pub disconnect_info: fidl_sme::DisconnectInfo,
492    auth_method: Option<auth::MethodName>,
493    pub selected_bss: Option<BssDescription>,
494    establish_rsna_failure_reason: Option<EstablishRsnaFailureReason>,
495}
496
497impl RoamFailure {
498    /// Returns true if failure was likely caused by rejected credentials.
499    /// Very similar to `ConnectFailure::likely_due_to_credential_rejected`.
500    #[allow(
501        clippy::match_like_matches_macro,
502        reason = "mass allow for https://fxbug.dev/381896734"
503    )]
504    pub fn likely_due_to_credential_rejected(&self) -> bool {
505        match self.failure_type {
506            // WPA1 and WPA2
507            RoamFailureType::EstablishRsnaFailure => match self.auth_method {
508                Some(auth::MethodName::Psk) => match self.establish_rsna_failure_reason {
509                    Some(EstablishRsnaFailureReason::RsnaResponseTimeout(
510                        wlan_rsn::Error::LikelyWrongCredential,
511                    ))
512                    | Some(EstablishRsnaFailureReason::RsnaCompletionTimeout(
513                        wlan_rsn::Error::LikelyWrongCredential,
514                    )) => true,
515                    _ => false,
516                },
517                _ => false,
518            },
519            RoamFailureType::ReassociationFailure => {
520                match &self.selected_bss {
521                    Some(selected_bss) => match selected_bss.protection() {
522                        // WEP
523                        BssProtection::Wep => match self.status_code {
524                            fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported => true,
525                            _ => false,
526                        },
527                        // WPA3
528                        BssProtection::Wpa3Personal
529                        | BssProtection::Wpa2Wpa3Personal => match self.status_code {
530                            fidl_ieee80211::StatusCode::RejectedSequenceTimeout => true,
531                            _ => false,
532                        },
533                        _ => false,
534                    },
535                    // If selected_bss is unavailable, there's a bigger problem with the roam
536                    // attempt than just a rejected credential.
537                    None => false,
538                }
539            }
540            _ => false,
541        }
542    }
543}
544
545#[derive(Debug, PartialEq)]
546pub enum SelectNetworkFailure {
547    NoScanResultWithSsid,
548    IncompatibleConnectRequest,
549    InternalProtectionError,
550}
551
552impl From<SelectNetworkFailure> for ConnectFailure {
553    fn from(failure: SelectNetworkFailure) -> Self {
554        ConnectFailure::SelectNetworkFailure(failure)
555    }
556}
557
558#[derive(Debug, PartialEq)]
559pub struct AssociationFailure {
560    pub bss_protection: BssProtection,
561    pub code: fidl_ieee80211::StatusCode,
562}
563
564impl From<AssociationFailure> for ConnectFailure {
565    fn from(failure: AssociationFailure) -> Self {
566        ConnectFailure::AssociationFailure(failure)
567    }
568}
569
570#[derive(Debug, PartialEq)]
571pub struct EstablishRsnaFailure {
572    pub auth_method: Option<auth::MethodName>,
573    pub reason: EstablishRsnaFailureReason,
574}
575
576#[derive(Debug, PartialEq)]
577pub enum EstablishRsnaFailureReason {
578    StartSupplicantFailed,
579    RsnaResponseTimeout(wlan_rsn::Error),
580    RsnaCompletionTimeout(wlan_rsn::Error),
581    InternalError,
582}
583
584impl From<EstablishRsnaFailure> for ConnectFailure {
585    fn from(failure: EstablishRsnaFailure) -> Self {
586        ConnectFailure::EstablishRsnaFailure(failure)
587    }
588}
589
590// Almost mirrors fidl_sme::ServingApInfo except that ServingApInfo
591// contains more info here than it does in fidl_sme.
592#[derive(Clone, Debug, PartialEq)]
593pub struct ServingApInfo {
594    pub bssid: Bssid,
595    pub ssid: Ssid,
596    pub rssi_dbm: i8,
597    pub snr_db: i8,
598    pub signal_report_time: zx::MonotonicInstant,
599    pub channel: wlan_common::channel::Channel,
600    pub protection: BssProtection,
601    pub ht_cap: Option<fidl_ieee80211::HtCapabilities>,
602    pub vht_cap: Option<fidl_ieee80211::VhtCapabilities>,
603    pub probe_resp_wsc: Option<wsc::ProbeRespWsc>,
604    pub wmm_param: Option<ie::WmmParam>,
605}
606
607impl From<ServingApInfo> for fidl_sme::ServingApInfo {
608    fn from(ap: ServingApInfo) -> fidl_sme::ServingApInfo {
609        fidl_sme::ServingApInfo {
610            bssid: ap.bssid.to_array(),
611            ssid: ap.ssid.to_vec(),
612            rssi_dbm: ap.rssi_dbm,
613            snr_db: ap.snr_db,
614            channel: ap.channel.into(),
615            protection: ap.protection.into(),
616        }
617    }
618}
619
620// TODO(https://fxbug.dev/324167674): fix.
621#[allow(clippy::large_enum_variant)]
622#[derive(Clone, Debug, PartialEq)]
623pub enum ClientSmeStatus {
624    Connected(ServingApInfo),
625    Connecting(Ssid),
626    Roaming(Bssid),
627    Idle,
628}
629
630impl ClientSmeStatus {
631    pub fn is_connecting(&self) -> bool {
632        matches!(self, ClientSmeStatus::Connecting(_))
633    }
634
635    pub fn is_connected(&self) -> bool {
636        matches!(self, ClientSmeStatus::Connected(_))
637    }
638}
639
640impl From<ClientSmeStatus> for fidl_sme::ClientStatusResponse {
641    fn from(client_sme_status: ClientSmeStatus) -> fidl_sme::ClientStatusResponse {
642        match client_sme_status {
643            ClientSmeStatus::Connected(serving_ap_info) => {
644                fidl_sme::ClientStatusResponse::Connected(serving_ap_info.into())
645            }
646            ClientSmeStatus::Connecting(ssid) => {
647                fidl_sme::ClientStatusResponse::Connecting(ssid.to_vec())
648            }
649            ClientSmeStatus::Roaming(bssid) => {
650                fidl_sme::ClientStatusResponse::Roaming(bssid.to_array())
651            }
652            ClientSmeStatus::Idle => fidl_sme::ClientStatusResponse::Idle(fidl_sme::Empty {}),
653        }
654    }
655}
656
657impl ClientSme {
658    #[allow(clippy::too_many_arguments, reason = "mass allow for https://fxbug.dev/381896734")]
659    pub fn new(
660        cfg: ClientConfig,
661        info: fidl_mlme::DeviceInfo,
662        inspector: fuchsia_inspect::Inspector,
663        inspect_node: fuchsia_inspect::Node,
664        persistence_req_sender: auto_persist::PersistenceReqSender,
665        security_support: fidl_common::SecuritySupport,
666        spectrum_management_support: fidl_common::SpectrumManagementSupport,
667    ) -> (Self, MlmeSink, MlmeStream, timer::EventStream<Event>) {
668        let device_info = Arc::new(info);
669        let (mlme_sink, mlme_stream) = mpsc::unbounded();
670        let (mut timer, time_stream) = timer::create_timer();
671        let inspect = Arc::new(inspect::SmeTree::new(
672            inspector,
673            inspect_node,
674            &device_info,
675            &spectrum_management_support,
676        ));
677        let _ = timer.schedule(event::InspectPulseCheck);
678        let _ = timer.schedule(event::InspectPulsePersist);
679        let mut auto_persist_last_pulse =
680            AutoPersist::new((), "wlanstack-last-pulse", persistence_req_sender);
681        {
682            // Request auto-persistence of pulse once on startup
683            let _guard = auto_persist_last_pulse.get_mut();
684        }
685
686        (
687            ClientSme {
688                cfg,
689                state: Some(ClientState::new(cfg)),
690                scan_sched: <ScanScheduler<
691                    Responder<Result<Vec<ScanResult>, fidl_mlme::ScanResultCode>>,
692                >>::new(
693                    Arc::clone(&device_info), spectrum_management_support
694                ),
695                wmm_status_responders: vec![],
696                auto_persist_last_pulse,
697                context: Context {
698                    mlme_sink: MlmeSink::new(mlme_sink.clone()),
699                    device_info,
700                    timer,
701                    att_id: 0,
702                    inspect,
703                    security_support,
704                },
705            },
706            MlmeSink::new(mlme_sink),
707            mlme_stream,
708            time_stream,
709        )
710    }
711
712    pub fn on_connect_command(
713        &mut self,
714        req: fidl_sme::ConnectRequest,
715    ) -> ConnectTransactionStream {
716        let (mut connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
717
718        // Cancel any ongoing connect attempt
719        self.state = self.state.take().map(|state| state.cancel_ongoing_connect(&mut self.context));
720
721        let bss_description: BssDescription = match req.bss_description.try_into() {
722            Ok(bss_description) => bss_description,
723            Err(e) => {
724                error!("Failed converting FIDL BssDescription in ConnectRequest: {:?}", e);
725                connect_txn_sink
726                    .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
727                return connect_txn_stream;
728            }
729        };
730
731        info!("Received ConnectRequest for {}", bss_description);
732
733        if self
734            .cfg
735            .bss_compatibility(
736                &bss_description,
737                &self.context.device_info,
738                &self.context.security_support,
739            )
740            .is_err()
741        {
742            warn!("BSS is incompatible");
743            connect_txn_sink
744                .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
745            return connect_txn_stream;
746        }
747
748        let authentication = req.authentication.clone();
749        let protection = match SecurityAuthenticator::try_from(req.authentication)
750            .map_err(From::from)
751            .and_then(|authenticator| {
752                Protection::try_from(SecurityContext {
753                    security: &authenticator,
754                    device: &self.context.device_info,
755                    security_support: &self.context.security_support,
756                    config: &self.cfg,
757                    bss: &bss_description,
758                })
759            }) {
760            Ok(protection) => protection,
761            Err(error) => {
762                warn!(
763                    "{:?}",
764                    format!(
765                        "Failed to configure protection for network {} ({}): {:?}",
766                        bss_description.ssid, bss_description.bssid, error
767                    )
768                );
769                connect_txn_sink
770                    .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
771                return connect_txn_stream;
772            }
773        };
774        let cmd = ConnectCommand {
775            bss: Box::new(bss_description),
776            connect_txn_sink,
777            protection,
778            authentication,
779        };
780
781        self.state = self.state.take().map(|state| state.connect(cmd, &mut self.context));
782        connect_txn_stream
783    }
784
785    pub fn on_roam_command(&mut self, req: fidl_sme::RoamRequest) {
786        if !self.status().is_connected() {
787            error!("SME ignoring roam request because client is not connected");
788        } else {
789            self.state =
790                self.state.take().map(|state| state.roam(&mut self.context, req.bss_description));
791        }
792    }
793
794    pub fn on_disconnect_command(
795        &mut self,
796        policy_disconnect_reason: fidl_sme::UserDisconnectReason,
797        responder: fidl_sme::ClientSmeDisconnectResponder,
798    ) {
799        self.state = self
800            .state
801            .take()
802            .map(|state| state.disconnect(&mut self.context, policy_disconnect_reason, responder));
803        self.context.inspect.update_pulse(self.status());
804    }
805
806    pub fn on_scan_command(
807        &mut self,
808        scan_request: fidl_sme::ScanRequest,
809    ) -> oneshot::Receiver<Result<Vec<wlan_common::scan::ScanResult>, fidl_mlme::ScanResultCode>>
810    {
811        let (responder, receiver) = Responder::new();
812        if self.status().is_connecting() {
813            info!("SME ignoring scan request because a connect is in progress");
814            responder.respond(Err(fidl_mlme::ScanResultCode::ShouldWait));
815        } else {
816            info!(
817                "SME received a scan command, initiating a{} discovery scan",
818                match scan_request {
819                    fidl_sme::ScanRequest::Active(_) => "n active",
820                    fidl_sme::ScanRequest::Passive(_) => " passive",
821                }
822            );
823            let scan = DiscoveryScan::new(responder, scan_request);
824            let req = self.scan_sched.enqueue_scan_to_discover(scan);
825            self.send_scan_request(req);
826        }
827        receiver
828    }
829
830    pub fn on_clone_inspect_vmo(&self) -> Option<fidl::Vmo> {
831        self.context.inspect.clone_vmo_data()
832    }
833
834    pub fn status(&self) -> ClientSmeStatus {
835        // `self.state` is always set to another state on transition and thus always present
836        #[expect(clippy::expect_used)]
837        self.state.as_ref().expect("expected state to be always present").status()
838    }
839
840    pub fn wmm_status(&mut self) -> oneshot::Receiver<fidl_sme::ClientSmeWmmStatusResult> {
841        let (responder, receiver) = Responder::new();
842        self.wmm_status_responders.push(responder);
843        self.context.mlme_sink.send(MlmeRequest::WmmStatusReq);
844        receiver
845    }
846
847    fn send_scan_request(&mut self, req: Option<fidl_mlme::ScanRequest>) {
848        if let Some(req) = req {
849            self.context.mlme_sink.send(MlmeRequest::Scan(req));
850        }
851    }
852
853    pub fn query_telemetry_support(
854        &mut self,
855    ) -> oneshot::Receiver<Result<fidl_stats::TelemetrySupport, i32>> {
856        let (responder, receiver) = Responder::new();
857        self.context.mlme_sink.send(MlmeRequest::QueryTelemetrySupport(responder));
858        receiver
859    }
860
861    pub fn iface_stats(&mut self) -> oneshot::Receiver<fidl_mlme::GetIfaceStatsResponse> {
862        let (responder, receiver) = Responder::new();
863        self.context.mlme_sink.send(MlmeRequest::GetIfaceStats(responder));
864        receiver
865    }
866
867    pub fn histogram_stats(
868        &mut self,
869    ) -> oneshot::Receiver<fidl_mlme::GetIfaceHistogramStatsResponse> {
870        let (responder, receiver) = Responder::new();
871        self.context.mlme_sink.send(MlmeRequest::GetIfaceHistogramStats(responder));
872        receiver
873    }
874}
875
876impl super::Station for ClientSme {
877    type Event = Event;
878
879    fn on_mlme_event(&mut self, event: fidl_mlme::MlmeEvent) {
880        match event {
881            fidl_mlme::MlmeEvent::OnScanResult { result } => self
882                .scan_sched
883                .on_mlme_scan_result(result)
884                .unwrap_or_else(|e| error!("scan result error: {:?}", e)),
885            fidl_mlme::MlmeEvent::OnScanEnd { end } => {
886                match self.scan_sched.on_mlme_scan_end(end, &self.context.inspect) {
887                    Err(e) => error!("scan end error: {:?}", e),
888                    Ok((scan_end, next_request)) => {
889                        // Finalize stats for previous scan before sending scan request for
890                        // the next one, which start stats collection for new scan.
891                        self.send_scan_request(next_request);
892
893                        match scan_end.result_code {
894                            fidl_mlme::ScanResultCode::Success => {
895                                let scan_result_list: Vec<ScanResult> = scan_end
896                                    .bss_description_list
897                                    .into_iter()
898                                    .map(|bss_description| {
899                                        self.cfg.create_scan_result(
900                                            // TODO(https://fxbug.dev/42164608): ScanEnd drops the timestamp from MLME
901                                            zx::MonotonicInstant::from_nanos(0),
902                                            bss_description,
903                                            &self.context.device_info,
904                                            &self.context.security_support,
905                                        )
906                                    })
907                                    .collect();
908                                for responder in scan_end.tokens {
909                                    responder.respond(Ok(scan_result_list.clone()));
910                                }
911                            }
912                            result_code => {
913                                let count = scan_end.bss_description_list.len();
914                                if count > 0 {
915                                    warn!("Incomplete scan with {} pending results.", count);
916                                }
917                                for responder in scan_end.tokens {
918                                    responder.respond(Err(result_code));
919                                }
920                            }
921                        }
922                    }
923                }
924            }
925            fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp } => {
926                for responder in self.wmm_status_responders.drain(..) {
927                    let result = if status == zx::sys::ZX_OK { Ok(resp) } else { Err(status) };
928                    responder.respond(result);
929                }
930                let event = fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp };
931                self.state =
932                    self.state.take().map(|state| state.on_mlme_event(event, &mut self.context));
933            }
934            other => {
935                self.state =
936                    self.state.take().map(|state| state.on_mlme_event(other, &mut self.context));
937            }
938        };
939
940        self.context.inspect.update_pulse(self.status());
941    }
942
943    fn on_timeout(&mut self, timed_event: timer::Event<Event>) {
944        self.state = self.state.take().map(|state| match timed_event.event {
945            event @ Event::RsnaCompletionTimeout(..)
946            | event @ Event::RsnaResponseTimeout(..)
947            | event @ Event::RsnaRetransmissionTimeout(..)
948            | event @ Event::SaeTimeout(..)
949            | event @ Event::DeauthenticateTimeout(..) => {
950                state.handle_timeout(event, &mut self.context)
951            }
952            Event::InspectPulseCheck(..) => {
953                self.context.mlme_sink.send(MlmeRequest::WmmStatusReq);
954                let _ = self.context.timer.schedule(event::InspectPulseCheck);
955                state
956            }
957            Event::InspectPulsePersist(..) => {
958                // Auto persist based on a timer to avoid log spam. The default approach is
959                // is to wrap AutoPersist around the Inspect PulseNode, but because the pulse
960                // is updated every second (due to SignalIndication event), we'd send a request
961                // to persistence service which'd log every second that it's queued until backoff.
962                let _guard = self.auto_persist_last_pulse.get_mut();
963                let _ = self.context.timer.schedule(event::InspectPulsePersist);
964                state
965            }
966        });
967
968        // Because `self.status()` relies on the value of `self.state` to be present, we cannot
969        // retrieve it and update pulse node inside the closure above.
970        self.context.inspect.update_pulse(self.status());
971    }
972}
973
974fn report_connect_finished(connect_txn_sink: &mut ConnectTransactionSink, result: ConnectResult) {
975    connect_txn_sink.send_connect_result(result);
976}
977
978fn report_roam_finished(connect_txn_sink: &mut ConnectTransactionSink, result: RoamResult) {
979    connect_txn_sink.send_roam_result(result);
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985    use crate::Config as SmeConfig;
986    use ieee80211::MacAddr;
987    use lazy_static::lazy_static;
988    use std::collections::HashSet;
989    use test_case::test_case;
990    use wlan_common::{
991        assert_variant,
992        channel::{Cbw, Channel},
993        fake_bss_description, fake_fidl_bss_description,
994        ie::{fake_ht_cap_bytes, fake_vht_cap_bytes, /*rsn::akm,*/ IeType},
995        security::{wep::WEP40_KEY_BYTES, wpa::credential::PSK_SIZE_BYTES},
996        test_utils::{
997            fake_features::{
998                fake_security_support, fake_security_support_empty,
999                fake_spectrum_management_support_empty,
1000            },
1001            fake_stas::{FakeProtectionCfg, IesOverrides},
1002        },
1003    };
1004    use {
1005        fidl_fuchsia_wlan_common as fidl_common,
1006        fidl_fuchsia_wlan_common_security as fidl_security, fidl_fuchsia_wlan_mlme as fidl_mlme,
1007        fuchsia_inspect as finspect,
1008    };
1009
1010    use super::test_utils::{create_on_wmm_status_resp, fake_wmm_param, fake_wmm_status_resp};
1011
1012    use crate::{test_utils, Station};
1013
1014    lazy_static! {
1015        static ref CLIENT_ADDR: MacAddr = [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into();
1016    }
1017
1018    fn authentication_open() -> fidl_security::Authentication {
1019        fidl_security::Authentication { protocol: fidl_security::Protocol::Open, credentials: None }
1020    }
1021
1022    fn authentication_wep40() -> fidl_security::Authentication {
1023        fidl_security::Authentication {
1024            protocol: fidl_security::Protocol::Wep,
1025            credentials: Some(Box::new(fidl_security::Credentials::Wep(
1026                fidl_security::WepCredentials { key: [1; WEP40_KEY_BYTES].into() },
1027            ))),
1028        }
1029    }
1030
1031    fn authentication_wpa1_passphrase() -> fidl_security::Authentication {
1032        fidl_security::Authentication {
1033            protocol: fidl_security::Protocol::Wpa1,
1034            credentials: Some(Box::new(fidl_security::Credentials::Wpa(
1035                fidl_security::WpaCredentials::Passphrase(b"password".as_slice().into()),
1036            ))),
1037        }
1038    }
1039
1040    fn authentication_wpa2_personal_psk() -> fidl_security::Authentication {
1041        fidl_security::Authentication {
1042            protocol: fidl_security::Protocol::Wpa2Personal,
1043            credentials: Some(Box::new(fidl_security::Credentials::Wpa(
1044                fidl_security::WpaCredentials::Psk([1; PSK_SIZE_BYTES]),
1045            ))),
1046        }
1047    }
1048
1049    fn authentication_wpa2_personal_passphrase() -> fidl_security::Authentication {
1050        fidl_security::Authentication {
1051            protocol: fidl_security::Protocol::Wpa2Personal,
1052            credentials: Some(Box::new(fidl_security::Credentials::Wpa(
1053                fidl_security::WpaCredentials::Passphrase(b"password".as_slice().into()),
1054            ))),
1055        }
1056    }
1057
1058    fn authentication_wpa3_personal_passphrase() -> fidl_security::Authentication {
1059        fidl_security::Authentication {
1060            protocol: fidl_security::Protocol::Wpa3Personal,
1061            credentials: Some(Box::new(fidl_security::Credentials::Wpa(
1062                fidl_security::WpaCredentials::Passphrase(b"password".as_slice().into()),
1063            ))),
1064        }
1065    }
1066
1067    fn report_fake_scan_result(
1068        sme: &mut ClientSme,
1069        timestamp_nanos: i64,
1070        bss: fidl_common::BssDescription,
1071    ) {
1072        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
1073            result: fidl_mlme::ScanResult { txn_id: 1, timestamp_nanos, bss },
1074        });
1075        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
1076            end: fidl_mlme::ScanEnd { txn_id: 1, code: fidl_mlme::ScanResultCode::Success },
1077        });
1078    }
1079
1080    #[test_case(FakeProtectionCfg::Open)]
1081    #[test_case(FakeProtectionCfg::Wpa1Wpa2TkipOnly)]
1082    #[test_case(FakeProtectionCfg::Wpa2TkipOnly)]
1083    #[test_case(FakeProtectionCfg::Wpa2)]
1084    #[test_case(FakeProtectionCfg::Wpa2Wpa3)]
1085    fn default_client_protection_is_bss_compatible(protection: FakeProtectionCfg) {
1086        let cfg = ClientConfig::default();
1087        let fake_device_info = test_utils::fake_device_info([1u8; 6].into());
1088        assert!(cfg
1089            .bss_compatibility(
1090                &fake_bss_description!(protection => protection),
1091                &fake_device_info,
1092                &fake_security_support_empty()
1093            )
1094            .is_ok(),);
1095    }
1096
1097    #[test_case(FakeProtectionCfg::Wpa1)]
1098    #[test_case(FakeProtectionCfg::Wpa3)]
1099    #[test_case(FakeProtectionCfg::Wpa3Transition)]
1100    #[test_case(FakeProtectionCfg::Eap)]
1101    fn default_client_protection_is_bss_incompatible(protection: FakeProtectionCfg) {
1102        let cfg = ClientConfig::default();
1103        let fake_device_info = test_utils::fake_device_info([1u8; 6].into());
1104        assert!(cfg
1105            .bss_compatibility(
1106                &fake_bss_description!(protection => protection),
1107                &fake_device_info,
1108                &fake_security_support_empty()
1109            )
1110            .is_err(),);
1111    }
1112
1113    #[test_case(FakeProtectionCfg::Open)]
1114    #[test_case(FakeProtectionCfg::Wpa1Wpa2TkipOnly)]
1115    #[test_case(FakeProtectionCfg::Wpa2TkipOnly)]
1116    #[test_case(FakeProtectionCfg::Wpa2)]
1117    #[test_case(FakeProtectionCfg::Wpa2Wpa3)]
1118    fn compatible_default_client_protection_security_protocol_intersection_is_non_empty(
1119        protection: FakeProtectionCfg,
1120    ) {
1121        let cfg = ClientConfig::default();
1122        assert!(!cfg
1123            .security_protocol_intersection(
1124                &fake_bss_description!(protection => protection),
1125                &fake_security_support_empty()
1126            )
1127            .is_empty());
1128    }
1129
1130    #[test_case(FakeProtectionCfg::Wpa1)]
1131    #[test_case(FakeProtectionCfg::Wpa3)]
1132    #[test_case(FakeProtectionCfg::Wpa3Transition)]
1133    #[test_case(FakeProtectionCfg::Eap)]
1134    fn incompatible_default_client_protection_security_protocol_intersection_is_empty(
1135        protection: FakeProtectionCfg,
1136    ) {
1137        let cfg = ClientConfig::default();
1138        assert!(cfg
1139            .security_protocol_intersection(
1140                &fake_bss_description!(protection => protection),
1141                &fake_security_support_empty()
1142            )
1143            .is_empty(),);
1144    }
1145
1146    #[test_case(FakeProtectionCfg::Wpa1, [SecurityDescriptor::WPA1])]
1147    #[test_case(FakeProtectionCfg::Wpa3, [SecurityDescriptor::WPA3_PERSONAL])]
1148    #[test_case(FakeProtectionCfg::Wpa3Transition, [SecurityDescriptor::WPA3_PERSONAL])]
1149    // This BSS configuration is not specific enough to detect security protocols.
1150    #[test_case(FakeProtectionCfg::Eap, [])]
1151    fn default_client_protection_security_protocols_by_mac_role_eq(
1152        protection: FakeProtectionCfg,
1153        expected: impl IntoIterator<Item = SecurityDescriptor>,
1154    ) {
1155        let cfg = ClientConfig::default();
1156        let security_protocols: HashSet<_> = cfg
1157            .security_protocols_by_mac_role(&fake_bss_description!(protection => protection))
1158            .collect();
1159        // The protocols here are not necessarily disjoint between client and AP. Note that
1160        // security descriptors are less specific than BSS fixtures.
1161        assert_eq!(
1162            security_protocols,
1163            HashSet::from_iter(
1164                [
1165                    (SecurityDescriptor::OPEN, MacRole::Client),
1166                    (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
1167                ]
1168                .into_iter()
1169                .chain(expected.into_iter().map(|protocol| (protocol, MacRole::Ap)))
1170            ),
1171        );
1172    }
1173
1174    #[test]
1175    fn configured_client_bss_wep_compatible() {
1176        // WEP support is configurable.
1177        let cfg = ClientConfig::from_config(Config::default().with_wep(), false);
1178        assert!(!cfg
1179            .security_protocol_intersection(
1180                &fake_bss_description!(Wep),
1181                &fake_security_support_empty()
1182            )
1183            .is_empty());
1184    }
1185
1186    #[test]
1187    fn configured_client_bss_wpa1_compatible() {
1188        // WPA1 support is configurable.
1189        let cfg = ClientConfig::from_config(Config::default().with_wpa1(), false);
1190        assert!(!cfg
1191            .security_protocol_intersection(
1192                &fake_bss_description!(Wpa1),
1193                &fake_security_support_empty()
1194            )
1195            .is_empty());
1196    }
1197
1198    #[test]
1199    fn configured_client_bss_wpa3_compatible() {
1200        // WPA3 support is configurable.
1201        let cfg = ClientConfig::from_config(Config::default(), true);
1202        let mut security_support = fake_security_support_empty();
1203        security_support.mfp.supported = true;
1204        assert!(!cfg
1205            .security_protocol_intersection(&fake_bss_description!(Wpa3), &security_support)
1206            .is_empty());
1207        assert!(!cfg
1208            .security_protocol_intersection(
1209                &fake_bss_description!(Wpa3Transition),
1210                &security_support,
1211            )
1212            .is_empty());
1213    }
1214
1215    #[test]
1216    fn verify_rates_compatibility() {
1217        // Compatible rates.
1218        let cfg = ClientConfig::default();
1219        let device_info = test_utils::fake_device_info([1u8; 6].into());
1220        assert!(
1221            cfg.has_compatible_channel_and_data_rates(&fake_bss_description!(Open), &device_info)
1222        );
1223
1224        // Compatible rates with HT BSS membership selector (`0xFF`).
1225        let bss = fake_bss_description!(Open, rates: vec![0x8C, 0xFF]);
1226        assert!(cfg.has_compatible_channel_and_data_rates(&bss, &device_info));
1227
1228        // Incompatible rates.
1229        let bss = fake_bss_description!(Open, rates: vec![0x81]);
1230        assert!(!cfg.has_compatible_channel_and_data_rates(&bss, &device_info));
1231    }
1232
1233    #[test]
1234    fn convert_scan_result() {
1235        let cfg = ClientConfig::default();
1236        let bss_description = fake_bss_description!(Wpa2,
1237            ssid: Ssid::empty(),
1238            bssid: [0u8; 6],
1239            rssi_dbm: -30,
1240            snr_db: 0,
1241            channel: Channel::new(1, Cbw::Cbw20),
1242            ies_overrides: IesOverrides::new()
1243                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1244                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1245        );
1246        let device_info = test_utils::fake_device_info([1u8; 6].into());
1247        let timestamp = zx::MonotonicInstant::get();
1248        let scan_result = cfg.create_scan_result(
1249            timestamp,
1250            bss_description.clone(),
1251            &device_info,
1252            &fake_security_support(),
1253        );
1254
1255        assert_eq!(
1256            scan_result,
1257            ScanResult {
1258                compatibility: Compatible::expect_ok([SecurityDescriptor::WPA2_PERSONAL]),
1259                timestamp,
1260                bss_description,
1261            }
1262        );
1263
1264        let wmm_param = *ie::parse_wmm_param(&fake_wmm_param().bytes[..])
1265            .expect("expect WMM param to be parseable");
1266        let bss_description = fake_bss_description!(Wpa2,
1267            ssid: Ssid::empty(),
1268            bssid: [0u8; 6],
1269            rssi_dbm: -30,
1270            snr_db: 0,
1271            channel: Channel::new(1, Cbw::Cbw20),
1272            wmm_param: Some(wmm_param),
1273            ies_overrides: IesOverrides::new()
1274                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1275                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1276        );
1277        let timestamp = zx::MonotonicInstant::get();
1278        let scan_result = cfg.create_scan_result(
1279            timestamp,
1280            bss_description.clone(),
1281            &device_info,
1282            &fake_security_support(),
1283        );
1284
1285        assert_eq!(
1286            scan_result,
1287            ScanResult {
1288                compatibility: Compatible::expect_ok([SecurityDescriptor::WPA2_PERSONAL]),
1289                timestamp,
1290                bss_description,
1291            }
1292        );
1293
1294        let bss_description = fake_bss_description!(Wep,
1295            ssid: Ssid::empty(),
1296            bssid: [0u8; 6],
1297            rssi_dbm: -30,
1298            snr_db: 0,
1299            channel: Channel::new(1, Cbw::Cbw20),
1300            ies_overrides: IesOverrides::new()
1301                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1302                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1303        );
1304        let timestamp = zx::MonotonicInstant::get();
1305        let scan_result = cfg.create_scan_result(
1306            timestamp,
1307            bss_description.clone(),
1308            &device_info,
1309            &fake_security_support(),
1310        );
1311        assert_eq!(
1312            scan_result,
1313            ScanResult {
1314                compatibility: Incompatible::expect_err(
1315                    "incompatible channel, PHY data rates, or security protocols",
1316                    Some([
1317                        (SecurityDescriptor::WEP, MacRole::Ap),
1318                        (SecurityDescriptor::OPEN, MacRole::Client),
1319                        (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
1320                    ])
1321                ),
1322                timestamp,
1323                bss_description,
1324            },
1325        );
1326
1327        let cfg = ClientConfig::from_config(Config::default().with_wep(), false);
1328        let bss_description = fake_bss_description!(Wep,
1329            ssid: Ssid::empty(),
1330            bssid: [0u8; 6],
1331            rssi_dbm: -30,
1332            snr_db: 0,
1333            channel: Channel::new(1, Cbw::Cbw20),
1334            ies_overrides: IesOverrides::new()
1335                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1336                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1337        );
1338        let timestamp = zx::MonotonicInstant::get();
1339        let scan_result = cfg.create_scan_result(
1340            timestamp,
1341            bss_description.clone(),
1342            &device_info,
1343            &fake_security_support(),
1344        );
1345        assert_eq!(
1346            scan_result,
1347            ScanResult {
1348                compatibility: Compatible::expect_ok([SecurityDescriptor::WEP]),
1349                timestamp,
1350                bss_description,
1351            }
1352        );
1353    }
1354
1355    #[test_case(EstablishRsnaFailureReason::RsnaResponseTimeout(
1356        wlan_rsn::Error::LikelyWrongCredential
1357    ))]
1358    #[test_case(EstablishRsnaFailureReason::RsnaCompletionTimeout(
1359        wlan_rsn::Error::LikelyWrongCredential
1360    ))]
1361    fn test_connect_detection_of_rejected_wpa1_or_wpa2_credentials(
1362        reason: EstablishRsnaFailureReason,
1363    ) {
1364        let failure = ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
1365            auth_method: Some(auth::MethodName::Psk),
1366            reason,
1367        });
1368        assert!(failure.likely_due_to_credential_rejected());
1369    }
1370
1371    #[test_case(fake_bss_description!(Wpa1), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1372    #[test_case(fake_bss_description!(Wpa1), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1373    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1374    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1375    #[test_case(fake_bss_description!(Wpa2), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1376    #[test_case(fake_bss_description!(Wpa2), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1377    fn test_roam_detection_of_rejected_wpa1_or_wpa2_credentials(
1378        selected_bss: BssDescription,
1379        failure_reason: EstablishRsnaFailureReason,
1380    ) {
1381        let disconnect_info = fidl_sme::DisconnectInfo {
1382            is_sme_reconnecting: false,
1383            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1384                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1385                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1386            }),
1387        };
1388        let failure = RoamFailure {
1389            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1390            failure_type: RoamFailureType::EstablishRsnaFailure,
1391            selected_bssid: selected_bss.bssid,
1392            disconnect_info,
1393            auth_method: Some(auth::MethodName::Psk),
1394            establish_rsna_failure_reason: Some(failure_reason),
1395            selected_bss: Some(selected_bss),
1396        };
1397        assert!(failure.likely_due_to_credential_rejected());
1398    }
1399
1400    #[test]
1401    fn test_connect_detection_of_rejected_wpa3_credentials() {
1402        let bss = fake_bss_description!(Wpa3);
1403        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1404            bss_protection: bss.protection(),
1405            code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
1406        });
1407
1408        assert!(failure.likely_due_to_credential_rejected());
1409    }
1410
1411    #[test]
1412    fn test_roam_detection_of_rejected_wpa3_credentials() {
1413        let selected_bss = fake_bss_description!(Wpa3);
1414        let disconnect_info = fidl_sme::DisconnectInfo {
1415            is_sme_reconnecting: false,
1416            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1417                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1418                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1419            }),
1420        };
1421        let failure = RoamFailure {
1422            status_code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
1423            failure_type: RoamFailureType::ReassociationFailure,
1424            selected_bssid: selected_bss.bssid,
1425            disconnect_info,
1426            auth_method: Some(auth::MethodName::Sae),
1427            establish_rsna_failure_reason: None,
1428            selected_bss: Some(selected_bss),
1429        };
1430        assert!(failure.likely_due_to_credential_rejected());
1431    }
1432
1433    #[test]
1434    fn test_connect_detection_of_rejected_wep_credentials() {
1435        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1436            bss_protection: BssProtection::Wep,
1437            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1438        });
1439        assert!(failure.likely_due_to_credential_rejected());
1440    }
1441
1442    #[test]
1443    fn test_roam_detection_of_rejected_wep_credentials() {
1444        let selected_bss = fake_bss_description!(Wep);
1445        let disconnect_info = fidl_sme::DisconnectInfo {
1446            is_sme_reconnecting: false,
1447            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1448                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1449                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1450            }),
1451        };
1452        let failure = RoamFailure {
1453            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1454            failure_type: RoamFailureType::ReassociationFailure,
1455            selected_bssid: selected_bss.bssid,
1456            disconnect_info,
1457            auth_method: Some(auth::MethodName::Psk),
1458            establish_rsna_failure_reason: None,
1459            selected_bss: Some(selected_bss),
1460        };
1461        assert!(failure.likely_due_to_credential_rejected());
1462    }
1463
1464    #[test]
1465    fn test_connect_no_detection_of_rejected_wpa1_or_wpa2_credentials() {
1466        let failure = ConnectFailure::ScanFailure(fidl_mlme::ScanResultCode::InternalError);
1467        assert!(!failure.likely_due_to_credential_rejected());
1468
1469        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1470            bss_protection: BssProtection::Wpa2Personal,
1471            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1472        });
1473        assert!(!failure.likely_due_to_credential_rejected());
1474    }
1475
1476    #[test_case(fake_bss_description!(Wpa1))]
1477    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly))]
1478    #[test_case(fake_bss_description!(Wpa2))]
1479    fn test_roam_no_detection_of_rejected_wpa1_or_wpa2_credentials(selected_bss: BssDescription) {
1480        let disconnect_info = fidl_sme::DisconnectInfo {
1481            is_sme_reconnecting: false,
1482            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1483                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1484                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1485            }),
1486        };
1487        let failure = RoamFailure {
1488            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1489            failure_type: RoamFailureType::EstablishRsnaFailure,
1490            selected_bssid: selected_bss.bssid,
1491            disconnect_info,
1492            auth_method: Some(auth::MethodName::Psk),
1493            establish_rsna_failure_reason: Some(EstablishRsnaFailureReason::StartSupplicantFailed),
1494            selected_bss: Some(selected_bss),
1495        };
1496        assert!(!failure.likely_due_to_credential_rejected());
1497    }
1498
1499    #[test]
1500    fn test_connect_no_detection_of_rejected_wpa3_credentials() {
1501        let bss = fake_bss_description!(Wpa3);
1502        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1503            bss_protection: bss.protection(),
1504            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1505        });
1506
1507        assert!(!failure.likely_due_to_credential_rejected());
1508    }
1509
1510    #[test]
1511    fn test_roam_no_detection_of_rejected_wpa3_credentials() {
1512        let selected_bss = fake_bss_description!(Wpa3);
1513        let disconnect_info = fidl_sme::DisconnectInfo {
1514            is_sme_reconnecting: false,
1515            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1516                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1517                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1518            }),
1519        };
1520        let failure = RoamFailure {
1521            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1522            failure_type: RoamFailureType::ReassociationFailure,
1523            selected_bssid: selected_bss.bssid,
1524            disconnect_info,
1525            auth_method: Some(auth::MethodName::Sae),
1526            establish_rsna_failure_reason: None,
1527            selected_bss: Some(selected_bss),
1528        };
1529        assert!(!failure.likely_due_to_credential_rejected());
1530    }
1531
1532    #[test]
1533    fn test_connect_no_detection_of_rejected_wep_credentials() {
1534        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1535            bss_protection: BssProtection::Wep,
1536            code: fidl_ieee80211::StatusCode::InvalidParameters,
1537        });
1538        assert!(!failure.likely_due_to_credential_rejected());
1539    }
1540
1541    #[test]
1542    fn test_roam_no_detection_of_rejected_wep_credentials() {
1543        let selected_bss = fake_bss_description!(Wep);
1544        let disconnect_info = fidl_sme::DisconnectInfo {
1545            is_sme_reconnecting: false,
1546            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1547                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1548                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1549            }),
1550        };
1551        let failure = RoamFailure {
1552            status_code: fidl_ieee80211::StatusCode::StatusInvalidElement,
1553            failure_type: RoamFailureType::ReassociationFailure,
1554            selected_bssid: selected_bss.bssid,
1555            disconnect_info,
1556            auth_method: Some(auth::MethodName::Psk),
1557            establish_rsna_failure_reason: None,
1558            selected_bss: Some(selected_bss),
1559        };
1560        assert!(!failure.likely_due_to_credential_rejected());
1561    }
1562
1563    #[test_case(fake_bss_description!(Open), authentication_open() => matches Ok(Protection::Open))]
1564    #[test_case(fake_bss_description!(Open), authentication_wpa2_personal_passphrase() => matches Err(_))]
1565    #[test_case(fake_bss_description!(Wpa2), authentication_wpa2_personal_passphrase() => matches Ok(Protection::Rsna(_)))]
1566    #[test_case(fake_bss_description!(Wpa2), authentication_wpa2_personal_psk() => matches Ok(Protection::Rsna(_)))]
1567    #[test_case(fake_bss_description!(Wpa2), authentication_open() => matches Err(_))]
1568    fn test_protection_from_authentication(
1569        bss: BssDescription,
1570        authentication: fidl_security::Authentication,
1571    ) -> Result<Protection, anyhow::Error> {
1572        let device = test_utils::fake_device_info(*CLIENT_ADDR);
1573        let security_support = fake_security_support();
1574        let config = Default::default();
1575
1576        // Open BSS with open authentication:
1577        let authenticator = SecurityAuthenticator::try_from(authentication).unwrap();
1578        Protection::try_from(SecurityContext {
1579            security: &authenticator,
1580            device: &device,
1581            security_support: &security_support,
1582            config: &config,
1583            bss: &bss,
1584        })
1585    }
1586
1587    #[fuchsia::test(allow_stalls = false)]
1588    async fn status_connecting() {
1589        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1590        assert_eq!(ClientSmeStatus::Idle, sme.status());
1591
1592        // Issue a connect command and expect the status to change appropriately.
1593        let bss_description =
1594            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1595        let _recv = sme.on_connect_command(connect_req(
1596            Ssid::try_from("foo").unwrap(),
1597            bss_description,
1598            authentication_open(),
1599        ));
1600        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1601
1602        // We should still be connecting to "foo", but the status should now come from the state
1603        // machine and not from the scanner.
1604        let ssid = assert_variant!(sme.state.as_ref().unwrap().status(), ClientSmeStatus::Connecting(ssid) => ssid);
1605        assert_eq!(Ssid::try_from("foo").unwrap(), ssid);
1606        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1607
1608        // As soon as connect command is issued for "bar", the status changes immediately
1609        let bss_description =
1610            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap());
1611        let _recv2 = sme.on_connect_command(connect_req(
1612            Ssid::try_from("bar").unwrap(),
1613            bss_description,
1614            authentication_open(),
1615        ));
1616        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bar").unwrap()), sme.status());
1617    }
1618
1619    #[test]
1620    fn connecting_to_wep_network_supported() {
1621        let _executor = fuchsia_async::TestExecutor::new();
1622        let inspector = finspect::Inspector::default();
1623        let sme_root_node = inspector.root().create_child("sme");
1624        let (persistence_req_sender, _persistence_receiver) =
1625            test_utils::create_inspect_persistence_channel();
1626        let (mut sme, _mlme_sink, mut mlme_stream, _time_stream) = ClientSme::new(
1627            ClientConfig::from_config(SmeConfig::default().with_wep(), false),
1628            test_utils::fake_device_info(*CLIENT_ADDR),
1629            inspector,
1630            sme_root_node,
1631            persistence_req_sender,
1632            fake_security_support(),
1633            fake_spectrum_management_support_empty(),
1634        );
1635        assert_eq!(ClientSmeStatus::Idle, sme.status());
1636
1637        // Issue a connect command and expect the status to change appropriately.
1638        let bss_description = fake_fidl_bss_description!(Wep, ssid: Ssid::try_from("foo").unwrap());
1639        let req =
1640            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_wep40());
1641        let _recv = sme.on_connect_command(req);
1642        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1643
1644        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1645    }
1646
1647    #[fuchsia::test(allow_stalls = false)]
1648    async fn connecting_to_wep_network_unsupported() {
1649        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1650        assert_eq!(ClientSmeStatus::Idle, sme.status());
1651
1652        // Issue a connect command and expect the status to change appropriately.
1653        let bss_description = fake_fidl_bss_description!(Wep, ssid: Ssid::try_from("foo").unwrap());
1654        let req =
1655            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_wep40());
1656        let mut _connect_fut = sme.on_connect_command(req);
1657        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1658    }
1659
1660    #[fuchsia::test(allow_stalls = false)]
1661    async fn connecting_password_supplied_for_protected_network() {
1662        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1663        assert_eq!(ClientSmeStatus::Idle, sme.status());
1664
1665        // Issue a connect command and expect the status to change appropriately.
1666        let bss_description =
1667            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
1668        let req = connect_req(
1669            Ssid::try_from("foo").unwrap(),
1670            bss_description,
1671            authentication_wpa2_personal_passphrase(),
1672        );
1673        let _recv = sme.on_connect_command(req);
1674        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1675
1676        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1677    }
1678
1679    #[fuchsia::test(allow_stalls = false)]
1680    async fn connecting_psk_supplied_for_protected_network() {
1681        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1682        assert_eq!(ClientSmeStatus::Idle, sme.status());
1683
1684        // Issue a connect command and expect the status to change appropriately.
1685        let bss_description =
1686            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("IEEE").unwrap());
1687        let req = connect_req(
1688            Ssid::try_from("IEEE").unwrap(),
1689            bss_description,
1690            authentication_wpa2_personal_psk(),
1691        );
1692        let _recv = sme.on_connect_command(req);
1693        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("IEEE").unwrap()), sme.status());
1694
1695        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1696    }
1697
1698    #[fuchsia::test(allow_stalls = false)]
1699    async fn connecting_password_supplied_for_unprotected_network() {
1700        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1701        assert_eq!(ClientSmeStatus::Idle, sme.status());
1702
1703        let bss_description =
1704            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1705        let req = connect_req(
1706            Ssid::try_from("foo").unwrap(),
1707            bss_description,
1708            authentication_wpa2_personal_passphrase(),
1709        );
1710        let mut connect_txn_stream = sme.on_connect_command(req);
1711        assert_eq!(ClientSmeStatus::Idle, sme.status());
1712
1713        // User should get a message that connection failed
1714        assert_variant!(
1715            connect_txn_stream.try_next(),
1716            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1717                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1718            }
1719        );
1720    }
1721
1722    #[fuchsia::test(allow_stalls = false)]
1723    async fn connecting_psk_supplied_for_unprotected_network() {
1724        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1725        assert_eq!(ClientSmeStatus::Idle, sme.status());
1726
1727        let bss_description =
1728            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1729        let req = connect_req(
1730            Ssid::try_from("foo").unwrap(),
1731            bss_description,
1732            authentication_wpa2_personal_psk(),
1733        );
1734        let mut connect_txn_stream = sme.on_connect_command(req);
1735        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1736
1737        // User should get a message that connection failed
1738        assert_variant!(
1739            connect_txn_stream.try_next(),
1740            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1741                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1742            }
1743        );
1744    }
1745
1746    #[fuchsia::test(allow_stalls = false)]
1747    async fn connecting_no_password_supplied_for_protected_network() {
1748        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1749        assert_eq!(ClientSmeStatus::Idle, sme.status());
1750
1751        let bss_description =
1752            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
1753        let req =
1754            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_open());
1755        let mut connect_txn_stream = sme.on_connect_command(req);
1756        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1757
1758        // No join request should be sent to MLME
1759        assert_no_connect(&mut mlme_stream);
1760
1761        // User should get a message that connection failed
1762        assert_variant!(
1763            connect_txn_stream.try_next(),
1764            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1765                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1766            }
1767        );
1768    }
1769
1770    #[fuchsia::test(allow_stalls = false)]
1771    async fn connecting_bypass_join_scan_open() {
1772        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1773        assert_eq!(ClientSmeStatus::Idle, sme.status());
1774
1775        let bss_description =
1776            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bssname").unwrap());
1777        let req =
1778            connect_req(Ssid::try_from("bssname").unwrap(), bss_description, authentication_open());
1779        let mut connect_txn_stream = sme.on_connect_command(req);
1780
1781        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bssname").unwrap()), sme.status());
1782        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1783        // There should be no message in the connect_txn_stream
1784        assert_variant!(connect_txn_stream.try_next(), Err(_));
1785    }
1786
1787    #[fuchsia::test(allow_stalls = false)]
1788    async fn connecting_bypass_join_scan_protected() {
1789        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1790        assert_eq!(ClientSmeStatus::Idle, sme.status());
1791
1792        let bss_description =
1793            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("bssname").unwrap());
1794        let req = connect_req(
1795            Ssid::try_from("bssname").unwrap(),
1796            bss_description,
1797            authentication_wpa2_personal_passphrase(),
1798        );
1799        let mut connect_txn_stream = sme.on_connect_command(req);
1800
1801        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bssname").unwrap()), sme.status());
1802        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1803        // There should be no message in the connect_txn_stream
1804        assert_variant!(connect_txn_stream.try_next(), Err(_));
1805    }
1806
1807    #[fuchsia::test(allow_stalls = false)]
1808    async fn connecting_bypass_join_scan_mismatched_credential() {
1809        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1810        assert_eq!(ClientSmeStatus::Idle, sme.status());
1811
1812        let bss_description =
1813            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("bssname").unwrap());
1814        let req =
1815            connect_req(Ssid::try_from("bssname").unwrap(), bss_description, authentication_open());
1816        let mut connect_txn_stream = sme.on_connect_command(req);
1817
1818        assert_eq!(ClientSmeStatus::Idle, sme.status());
1819        assert_no_connect(&mut mlme_stream);
1820
1821        // User should get a message that connection failed
1822        assert_variant!(
1823            connect_txn_stream.try_next(),
1824            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1825                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1826            }
1827        );
1828    }
1829
1830    #[fuchsia::test(allow_stalls = false)]
1831    async fn connecting_bypass_join_scan_unsupported_bss() {
1832        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1833        assert_eq!(ClientSmeStatus::Idle, sme.status());
1834
1835        let bss_description =
1836            fake_fidl_bss_description!(Wpa3Enterprise, ssid: Ssid::try_from("bssname").unwrap());
1837        let req = connect_req(
1838            Ssid::try_from("bssname").unwrap(),
1839            bss_description,
1840            authentication_wpa3_personal_passphrase(),
1841        );
1842        let mut connect_txn_stream = sme.on_connect_command(req);
1843
1844        assert_eq!(ClientSmeStatus::Idle, sme.status());
1845        assert_no_connect(&mut mlme_stream);
1846
1847        // User should get a message that connection failed
1848        assert_variant!(
1849            connect_txn_stream.try_next(),
1850            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1851                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1852            }
1853        );
1854    }
1855
1856    #[fuchsia::test(allow_stalls = false)]
1857    async fn connecting_right_credential_type_no_privacy() {
1858        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1859
1860        let bss_description = fake_fidl_bss_description!(
1861            Wpa2,
1862            ssid: Ssid::try_from("foo").unwrap(),
1863        );
1864        // Manually override the privacy bit since fake_fidl_bss_description!()
1865        // does not allow setting it directly.
1866        let bss_description = fidl_common::BssDescription {
1867            capability_info: wlan_common::mac::CapabilityInfo(bss_description.capability_info)
1868                .with_privacy(false)
1869                .0,
1870            ..bss_description
1871        };
1872        let mut connect_txn_stream = sme.on_connect_command(connect_req(
1873            Ssid::try_from("foo").unwrap(),
1874            bss_description,
1875            authentication_wpa2_personal_passphrase(),
1876        ));
1877
1878        assert_variant!(
1879            connect_txn_stream.try_next(),
1880            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1881                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1882            }
1883        );
1884    }
1885
1886    #[fuchsia::test(allow_stalls = false)]
1887    async fn connecting_mismatched_security_protocol() {
1888        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1889
1890        let bss_description =
1891            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap());
1892        let mut connect_txn_stream = sme.on_connect_command(connect_req(
1893            Ssid::try_from("wpa2").unwrap(),
1894            bss_description,
1895            authentication_wep40(),
1896        ));
1897        assert_variant!(
1898            connect_txn_stream.try_next(),
1899            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1900                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1901            }
1902        );
1903
1904        let bss_description =
1905            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap());
1906        let mut connect_txn_stream = sme.on_connect_command(connect_req(
1907            Ssid::try_from("wpa2").unwrap(),
1908            bss_description,
1909            authentication_wpa1_passphrase(),
1910        ));
1911        assert_variant!(
1912            connect_txn_stream.try_next(),
1913            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1914                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1915            }
1916        );
1917
1918        let bss_description =
1919            fake_fidl_bss_description!(Wpa3, ssid: Ssid::try_from("wpa3").unwrap());
1920        let mut connect_txn_stream = sme.on_connect_command(connect_req(
1921            Ssid::try_from("wpa3").unwrap(),
1922            bss_description,
1923            authentication_wpa2_personal_passphrase(),
1924        ));
1925        assert_variant!(
1926            connect_txn_stream.try_next(),
1927            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1928                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1929            }
1930        );
1931    }
1932
1933    // Disable logging to prevent failure from emitted error logs.
1934    #[fuchsia::test(allow_stalls = false, logging = false)]
1935    async fn connecting_right_credential_type_but_short_password() {
1936        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1937
1938        let bss_description =
1939            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
1940        let mut connect_txn_stream = sme.on_connect_command(connect_req(
1941            Ssid::try_from("foo").unwrap(),
1942            bss_description.clone(),
1943            fidl_security::Authentication {
1944                protocol: fidl_security::Protocol::Wpa2Personal,
1945                credentials: Some(Box::new(fidl_security::Credentials::Wpa(
1946                    fidl_security::WpaCredentials::Passphrase(b"nope".as_slice().into()),
1947                ))),
1948            },
1949        ));
1950        report_fake_scan_result(
1951            &mut sme,
1952            zx::MonotonicInstant::get().into_nanos(),
1953            bss_description,
1954        );
1955
1956        assert_variant!(
1957            connect_txn_stream.try_next(),
1958            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1959                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1960            }
1961        );
1962    }
1963
1964    // Disable logging to prevent failure from emitted error logs.
1965    #[fuchsia::test(allow_stalls = false, logging = false)]
1966    async fn new_connect_attempt_cancels_pending_connect() {
1967        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1968
1969        let bss_description =
1970            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1971        let req = connect_req(
1972            Ssid::try_from("foo").unwrap(),
1973            bss_description.clone(),
1974            authentication_open(),
1975        );
1976        let mut connect_txn_stream1 = sme.on_connect_command(req);
1977
1978        let req2 = connect_req(
1979            Ssid::try_from("foo").unwrap(),
1980            bss_description.clone(),
1981            authentication_open(),
1982        );
1983        let mut connect_txn_stream2 = sme.on_connect_command(req2);
1984
1985        // User should get a message that first connection attempt is canceled
1986        assert_variant!(
1987            connect_txn_stream1.try_next(),
1988            Ok(Some(ConnectTransactionEvent::OnConnectResult {
1989                result: ConnectResult::Canceled,
1990                is_reconnect: false
1991            }))
1992        );
1993
1994        // Report scan result to transition second connection attempt past scan. This is to verify
1995        // that connection attempt will be canceled even in the middle of joining the network
1996        report_fake_scan_result(
1997            &mut sme,
1998            zx::MonotonicInstant::get().into_nanos(),
1999            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap()),
2000        );
2001
2002        let req3 = connect_req(
2003            Ssid::try_from("foo").unwrap(),
2004            bss_description.clone(),
2005            authentication_open(),
2006        );
2007        let mut _connect_fut3 = sme.on_connect_command(req3);
2008
2009        // Verify that second connection attempt is canceled as new connect request comes in
2010        assert_variant!(
2011            connect_txn_stream2.try_next(),
2012            Ok(Some(ConnectTransactionEvent::OnConnectResult {
2013                result: ConnectResult::Canceled,
2014                is_reconnect: false
2015            }))
2016        );
2017    }
2018
2019    #[fuchsia::test(allow_stalls = false)]
2020    async fn test_simple_scan_error() {
2021        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2022        let mut recv =
2023            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}));
2024
2025        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2026            end: fidl_mlme::ScanEnd {
2027                txn_id: 1,
2028                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2029            },
2030        });
2031
2032        assert_eq!(
2033            recv.try_recv(),
2034            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2035        );
2036    }
2037
2038    #[fuchsia::test(allow_stalls = false)]
2039    async fn test_scan_error_after_some_results_returned() {
2040        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2041        let mut recv =
2042            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}));
2043
2044        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2045        bss.bssid = [3; 6];
2046        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2047            result: fidl_mlme::ScanResult {
2048                txn_id: 1,
2049                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2050                bss,
2051            },
2052        });
2053        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2054        bss.bssid = [4; 6];
2055        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2056            result: fidl_mlme::ScanResult {
2057                txn_id: 1,
2058                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2059                bss,
2060            },
2061        });
2062
2063        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2064            end: fidl_mlme::ScanEnd {
2065                txn_id: 1,
2066                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2067            },
2068        });
2069
2070        // Scan results are lost when an error occurs.
2071        assert_eq!(
2072            recv.try_recv(),
2073            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2074        );
2075    }
2076
2077    #[fuchsia::test(allow_stalls = false)]
2078    async fn test_scan_is_rejected_while_connecting() {
2079        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2080
2081        // Send a connect command to move SME into Connecting state
2082        let bss_description =
2083            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2084        let _recv = sme.on_connect_command(connect_req(
2085            Ssid::try_from("foo").unwrap(),
2086            bss_description,
2087            authentication_open(),
2088        ));
2089        assert_variant!(sme.status(), ClientSmeStatus::Connecting(_));
2090
2091        // Send a scan command and verify a ShouldWait response is returned
2092        let mut recv =
2093            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}));
2094        assert_eq!(recv.try_recv(), Ok(Some(Err(fidl_mlme::ScanResultCode::ShouldWait))));
2095    }
2096
2097    #[fuchsia::test(allow_stalls = false)]
2098    async fn test_wmm_status_success() {
2099        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2100        let mut receiver = sme.wmm_status();
2101
2102        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2103
2104        let resp = fake_wmm_status_resp();
2105        #[allow(
2106            clippy::redundant_field_names,
2107            reason = "mass allow for https://fxbug.dev/381896734"
2108        )]
2109        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnWmmStatusResp {
2110            status: zx::sys::ZX_OK,
2111            resp: resp,
2112        });
2113
2114        assert_eq!(receiver.try_recv(), Ok(Some(Ok(resp))));
2115    }
2116
2117    #[fuchsia::test(allow_stalls = false)]
2118    async fn test_wmm_status_failed() {
2119        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2120        let mut receiver = sme.wmm_status();
2121
2122        assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2123        sme.on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_ERR_IO));
2124        assert_eq!(receiver.try_recv(), Ok(Some(Err(zx::sys::ZX_ERR_IO))));
2125    }
2126
2127    #[test]
2128    fn test_inspect_pulse_persist() {
2129        let _executor = fuchsia_async::TestExecutor::new();
2130        let inspector = finspect::Inspector::default();
2131        let sme_root_node = inspector.root().create_child("sme");
2132        let (persistence_req_sender, mut persistence_receiver) =
2133            test_utils::create_inspect_persistence_channel();
2134        let (mut sme, _mlme_sink, _mlme_stream, mut time_stream) = ClientSme::new(
2135            ClientConfig::from_config(SmeConfig::default().with_wep(), false),
2136            test_utils::fake_device_info(*CLIENT_ADDR),
2137            inspector,
2138            sme_root_node,
2139            persistence_req_sender,
2140            fake_security_support(),
2141            fake_spectrum_management_support_empty(),
2142        );
2143        assert_eq!(ClientSmeStatus::Idle, sme.status());
2144
2145        // Verify we request persistence on startup
2146        assert_variant!(persistence_receiver.try_next(), Ok(Some(tag)) => {
2147            assert_eq!(&tag, "wlanstack-last-pulse");
2148        });
2149
2150        let mut persist_event = None;
2151        while let Ok(Some((_timeout, timed_event, _handle))) = time_stream.try_next() {
2152            if let Event::InspectPulsePersist(..) = timed_event.event {
2153                persist_event = Some(timed_event);
2154                break;
2155            }
2156        }
2157        assert!(persist_event.is_some());
2158        sme.on_timeout(persist_event.unwrap());
2159
2160        // Verify we request persistence again on timeout
2161        assert_variant!(persistence_receiver.try_next(), Ok(Some(tag)) => {
2162            assert_eq!(&tag, "wlanstack-last-pulse");
2163        });
2164    }
2165
2166    fn assert_no_connect(mlme_stream: &mut mpsc::UnboundedReceiver<MlmeRequest>) {
2167        loop {
2168            match mlme_stream.try_next() {
2169                Ok(event) => match event {
2170                    Some(MlmeRequest::Connect(..)) => {
2171                        panic!("unexpected connect request sent to MLME")
2172                    }
2173                    None => break,
2174                    _ => (),
2175                },
2176                Err(e) => {
2177                    assert_eq!(e.to_string(), "receiver channel is empty");
2178                    break;
2179                }
2180            }
2181        }
2182    }
2183
2184    fn connect_req(
2185        ssid: Ssid,
2186        bss_description: fidl_common::BssDescription,
2187        authentication: fidl_security::Authentication,
2188    ) -> fidl_sme::ConnectRequest {
2189        fidl_sme::ConnectRequest {
2190            ssid: ssid.to_vec(),
2191            bss_description,
2192            multiple_bss_candidates: true,
2193            authentication,
2194            deprecated_scan_type: fidl_common::ScanType::Passive,
2195        }
2196    }
2197
2198    // The unused _exec parameter ensures that an executor exists for the lifetime of the SME.
2199    // Our internal timer implementation relies on the existence of a local executor.
2200    //
2201    // TODO(https://fxbug.dev/327499461): This function is async to ensure SME functions will
2202    // run in an async context and not call `wlan_common::timer::Timer::now` without an
2203    // executor.
2204    async fn create_sme() -> (ClientSme, MlmeStream, timer::EventStream<Event>) {
2205        let inspector = finspect::Inspector::default();
2206        let sme_root_node = inspector.root().create_child("sme");
2207        let (persistence_req_sender, _persistence_receiver) =
2208            test_utils::create_inspect_persistence_channel();
2209        let (client_sme, _mlme_sink, mlme_stream, time_stream) = ClientSme::new(
2210            ClientConfig::default(),
2211            test_utils::fake_device_info(*CLIENT_ADDR),
2212            inspector,
2213            sme_root_node,
2214            persistence_req_sender,
2215            fake_security_support(),
2216            fake_spectrum_management_support_empty(),
2217        );
2218        (client_sme, mlme_stream, time_stream)
2219    }
2220}