Skip to main content

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