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 update_mac_address(&mut self, mac_addr: [u8; 6]) {
911        let mut device_info = (*self.context.device_info).clone();
912        device_info.sta_addr = mac_addr;
913        self.context.device_info = Arc::new(device_info);
914    }
915
916    pub fn device_info(&self) -> Arc<fidl_mlme::DeviceInfo> {
917        Arc::clone(&self.context.device_info)
918    }
919
920    pub fn query_apf_packet_filter_support(
921        &mut self,
922    ) -> oneshot::Receiver<Result<fidl_common::ApfPacketFilterSupport, i32>> {
923        let (responder, receiver) = Responder::new();
924        self.context.mlme_sink.send(MlmeRequest::QueryApfPacketFilterSupport(responder));
925        receiver
926    }
927
928    pub fn install_apf_packet_filter(
929        &mut self,
930        program: Vec<u8>,
931    ) -> oneshot::Receiver<Result<(), i32>> {
932        let (responder, receiver) = Responder::new();
933        self.context.mlme_sink.send(MlmeRequest::InstallApfPacketFilter(
934            fidl_mlme::MlmeInstallApfPacketFilterRequest { program },
935            responder,
936        ));
937        receiver
938    }
939
940    pub fn read_apf_packet_filter_data(
941        &mut self,
942    ) -> oneshot::Receiver<Result<fidl_mlme::MlmeReadApfPacketFilterDataResponse, i32>> {
943        let (responder, receiver) = Responder::new();
944        self.context.mlme_sink.send(MlmeRequest::ReadApfPacketFilterData(responder));
945        receiver
946    }
947
948    pub fn set_apf_packet_filter_enabled(
949        &mut self,
950        enabled: bool,
951    ) -> oneshot::Receiver<Result<(), i32>> {
952        let (responder, receiver) = Responder::new();
953        self.context.mlme_sink.send(MlmeRequest::SetApfPacketFilterEnabled(
954            fidl_mlme::MlmeSetApfPacketFilterEnabledRequest { enabled },
955            responder,
956        ));
957        receiver
958    }
959
960    pub fn get_apf_packet_filter_enabled(
961        &mut self,
962    ) -> oneshot::Receiver<Result<fidl_mlme::MlmeGetApfPacketFilterEnabledResponse, i32>> {
963        let (responder, receiver) = Responder::new();
964        self.context.mlme_sink.send(MlmeRequest::GetApfPacketFilterEnabled(responder));
965        receiver
966    }
967}
968
969impl super::Station for ClientSme {
970    type Event = Event;
971
972    fn on_mlme_event(&mut self, event: fidl_mlme::MlmeEvent) {
973        match event {
974            fidl_mlme::MlmeEvent::OnScanResult { result } => {
975                self.scan_sched
976                    .on_mlme_scan_result(result)
977                    .unwrap_or_else(|e| error!("scan result error: {:?}", e));
978            }
979            fidl_mlme::MlmeEvent::OnScanEnd { end } => {
980                match self.scan_sched.on_mlme_scan_end(end, &self.context.inspect) {
981                    Err(e) => error!("scan end error: {:?}", e),
982                    Ok((scan_end, next_request)) => {
983                        // Finalize stats for previous scan before sending scan request for
984                        // the next one, which start stats collection for new scan.
985                        self.send_scan_request(next_request);
986
987                        match scan_end.result_code {
988                            fidl_mlme::ScanResultCode::Success => {
989                                let scan_result_list: Vec<ScanResult> = scan_end
990                                    .bss_description_list
991                                    .into_iter()
992                                    .map(|bss_description| {
993                                        self.cfg.create_scan_result(
994                                            // TODO(https://fxbug.dev/42164608): ScanEnd drops the timestamp from MLME
995                                            zx::MonotonicInstant::from_nanos(0),
996                                            bss_description,
997                                            &self.context.device_info,
998                                            &self.context.security_support,
999                                        )
1000                                    })
1001                                    .collect();
1002                                for responder in scan_end.tokens {
1003                                    responder.respond(Ok(scan_result_list.clone()));
1004                                }
1005                            }
1006                            result_code => {
1007                                let count = scan_end.bss_description_list.len();
1008                                if count > 0 {
1009                                    warn!("Incomplete scan with {} pending results.", count);
1010                                }
1011                                for responder in scan_end.tokens {
1012                                    responder.respond(Err(result_code));
1013                                }
1014                            }
1015                        }
1016                    }
1017                }
1018            }
1019            fidl_mlme::MlmeEvent::OnScheduledScanMatchesAvailable { txn_id } => {
1020                self.scan_sched.on_scheduled_scan_matches_available(
1021                    txn_id,
1022                    &self.context.inspect,
1023                    &self.cfg,
1024                    &self.context.device_info,
1025                    &self.context.security_support,
1026                );
1027            }
1028            fidl_mlme::MlmeEvent::OnScheduledScanStoppedByFirmware { txn_id } => {
1029                self.scan_sched.on_scheduled_scan_stopped_by_firmware(txn_id);
1030            }
1031            fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp } => {
1032                for responder in self.wmm_status_responders.drain(..) {
1033                    let result = if status == zx::sys::ZX_OK { Ok(resp) } else { Err(status) };
1034                    responder.respond(result);
1035                }
1036                let event = fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp };
1037                self.state =
1038                    self.state.take().map(|state| state.on_mlme_event(event, &mut self.context));
1039            }
1040            other => {
1041                self.state =
1042                    self.state.take().map(|state| state.on_mlme_event(other, &mut self.context));
1043            }
1044        };
1045
1046        self.context.inspect.update_pulse(self.status());
1047    }
1048
1049    fn on_timeout(&mut self, timed_event: timer::Event<Event>) {
1050        self.state = self.state.take().map(|state| match timed_event.event {
1051            event @ Event::RsnaCompletionTimeout(..)
1052            | event @ Event::RsnaResponseTimeout(..)
1053            | event @ Event::RsnaRetransmissionTimeout(..)
1054            | event @ Event::SaeTimeout(..)
1055            | event @ Event::DeauthenticateTimeout(..) => {
1056                state.handle_timeout(event, &mut self.context)
1057            }
1058            Event::InspectPulseCheck(..) => {
1059                self.context.mlme_sink.send(MlmeRequest::WmmStatusReq);
1060                let _ = self.context.timer.schedule(event::InspectPulseCheck);
1061                state
1062            }
1063        });
1064
1065        // Because `self.status()` relies on the value of `self.state` to be present, we cannot
1066        // retrieve it and update pulse node inside the closure above.
1067        self.context.inspect.update_pulse(self.status());
1068    }
1069}
1070
1071fn report_connect_finished(connect_txn_sink: &mut ConnectTransactionSink, result: ConnectResult) {
1072    connect_txn_sink.send_connect_result(result);
1073}
1074
1075fn report_roam_finished(connect_txn_sink: &mut ConnectTransactionSink, result: RoamResult) {
1076    connect_txn_sink.send_roam_result(result);
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082    use crate::Config as SmeConfig;
1083    use assert_matches::assert_matches;
1084    use fidl_fuchsia_wlan_common as fidl_common;
1085    use fidl_fuchsia_wlan_mlme as fidl_mlme;
1086    use fuchsia_inspect as finspect;
1087    use ieee80211::MacAddr;
1088    use std::collections::HashSet;
1089    use std::sync::LazyLock;
1090    use test_case::test_case;
1091    use wlan_common::{
1092        channel::{Cbw, Channel},
1093        fake_bss_description, fake_fidl_bss_description,
1094        ie::{/*rsn::akm,*/ IeType, fake_ht_cap_bytes, fake_vht_cap_bytes},
1095        security::{wep::WEP40_KEY_BYTES, wpa::credential::PSK_SIZE_BYTES},
1096        test_utils::{
1097            fake_features::{
1098                fake_security_support, fake_security_support_empty,
1099                fake_spectrum_management_support_empty,
1100            },
1101            fake_stas::{FakeProtectionCfg, IesOverrides},
1102        },
1103    };
1104
1105    use super::test_utils::{create_on_wmm_status_resp, fake_wmm_param, fake_wmm_status_resp};
1106
1107    use crate::{Station, test_utils};
1108
1109    static CLIENT_ADDR: LazyLock<MacAddr> =
1110        LazyLock::new(|| [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67].into());
1111
1112    fn authentication_open() -> fidl_internal::Authentication {
1113        fidl_internal::Authentication { protocol: fidl_internal::Protocol::Open, credentials: None }
1114    }
1115
1116    fn authentication_wep40() -> fidl_internal::Authentication {
1117        fidl_internal::Authentication {
1118            protocol: fidl_internal::Protocol::Wep,
1119            credentials: Some(Box::new(fidl_internal::Credentials::Wep(
1120                fidl_internal::WepCredentials { key: [1; WEP40_KEY_BYTES].into() },
1121            ))),
1122        }
1123    }
1124
1125    fn authentication_wpa1_passphrase() -> fidl_internal::Authentication {
1126        fidl_internal::Authentication {
1127            protocol: fidl_internal::Protocol::Wpa1,
1128            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1129                fidl_internal::WpaCredentials::Passphrase(b"password".as_slice().into()),
1130            ))),
1131        }
1132    }
1133
1134    fn authentication_wpa2_personal_psk() -> fidl_internal::Authentication {
1135        fidl_internal::Authentication {
1136            protocol: fidl_internal::Protocol::Wpa2Personal,
1137            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1138                fidl_internal::WpaCredentials::Psk([1; PSK_SIZE_BYTES]),
1139            ))),
1140        }
1141    }
1142
1143    fn authentication_wpa2_personal_passphrase() -> fidl_internal::Authentication {
1144        fidl_internal::Authentication {
1145            protocol: fidl_internal::Protocol::Wpa2Personal,
1146            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1147                fidl_internal::WpaCredentials::Passphrase(b"password".as_slice().into()),
1148            ))),
1149        }
1150    }
1151
1152    fn authentication_wpa3_personal_passphrase() -> fidl_internal::Authentication {
1153        fidl_internal::Authentication {
1154            protocol: fidl_internal::Protocol::Wpa3Personal,
1155            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1156                fidl_internal::WpaCredentials::Passphrase(b"password".as_slice().into()),
1157            ))),
1158        }
1159    }
1160
1161    fn report_fake_scan_result(
1162        sme: &mut ClientSme,
1163        timestamp_nanos: i64,
1164        bss: fidl_ieee80211::BssDescription,
1165    ) {
1166        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
1167            result: fidl_mlme::ScanResult { txn_id: 1, timestamp_nanos, bss },
1168        });
1169        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
1170            end: fidl_mlme::ScanEnd { txn_id: 1, code: fidl_mlme::ScanResultCode::Success },
1171        });
1172    }
1173
1174    #[test_case(FakeProtectionCfg::Open)]
1175    #[test_case(FakeProtectionCfg::Wpa1Wpa2TkipOnly)]
1176    #[test_case(FakeProtectionCfg::Wpa2TkipOnly)]
1177    #[test_case(FakeProtectionCfg::Wpa2)]
1178    #[test_case(FakeProtectionCfg::Wpa2Wpa3)]
1179    fn default_client_protection_is_bss_compatible(protection: FakeProtectionCfg) {
1180        let cfg = ClientConfig::default();
1181        let fake_device_info = test_utils::fake_device_info([1u8; 6].into());
1182        assert!(
1183            cfg.bss_compatibility(
1184                &fake_bss_description!(protection => protection),
1185                &fake_device_info,
1186                &fake_security_support_empty()
1187            )
1188            .is_ok(),
1189        );
1190    }
1191
1192    #[test_case(FakeProtectionCfg::Wpa1)]
1193    #[test_case(FakeProtectionCfg::Wpa3)]
1194    #[test_case(FakeProtectionCfg::Wpa3Transition)]
1195    #[test_case(FakeProtectionCfg::Eap)]
1196    fn default_client_protection_is_bss_incompatible(protection: FakeProtectionCfg) {
1197        let cfg = ClientConfig::default();
1198        let fake_device_info = test_utils::fake_device_info([1u8; 6].into());
1199        assert!(
1200            cfg.bss_compatibility(
1201                &fake_bss_description!(protection => protection),
1202                &fake_device_info,
1203                &fake_security_support_empty()
1204            )
1205            .is_err(),
1206        );
1207    }
1208
1209    #[test_case(FakeProtectionCfg::Open)]
1210    #[test_case(FakeProtectionCfg::OpenOweTransition)]
1211    #[test_case(FakeProtectionCfg::Wpa1Wpa2TkipOnly)]
1212    #[test_case(FakeProtectionCfg::Wpa2TkipOnly)]
1213    #[test_case(FakeProtectionCfg::Wpa2)]
1214    #[test_case(FakeProtectionCfg::Wpa2Wpa3)]
1215    fn compatible_default_client_protection_security_protocol_intersection_is_non_empty(
1216        protection: FakeProtectionCfg,
1217    ) {
1218        let cfg = ClientConfig::default();
1219        assert!(
1220            !cfg.security_protocol_intersection(
1221                &fake_bss_description!(protection => protection),
1222                &fake_security_support_empty()
1223            )
1224            .is_empty()
1225        );
1226    }
1227
1228    #[test_case(FakeProtectionCfg::Owe)]
1229    #[test_case(FakeProtectionCfg::Wpa1)]
1230    #[test_case(FakeProtectionCfg::Wpa3)]
1231    #[test_case(FakeProtectionCfg::Wpa3Transition)]
1232    #[test_case(FakeProtectionCfg::Eap)]
1233    fn incompatible_default_client_protection_security_protocol_intersection_is_empty(
1234        protection: FakeProtectionCfg,
1235    ) {
1236        let cfg = ClientConfig::default();
1237        assert!(
1238            cfg.security_protocol_intersection(
1239                &fake_bss_description!(protection => protection),
1240                &fake_security_support_empty()
1241            )
1242            .is_empty(),
1243        );
1244    }
1245
1246    #[test_case(FakeProtectionCfg::Wpa1, [SecurityDescriptor::WPA1])]
1247    #[test_case(FakeProtectionCfg::Wpa3, [SecurityDescriptor::WPA3_PERSONAL])]
1248    #[test_case(FakeProtectionCfg::Wpa3Transition, [SecurityDescriptor::WPA3_PERSONAL])]
1249    // This BSS configuration is not specific enough to detect security protocols.
1250    #[test_case(FakeProtectionCfg::Eap, [])]
1251    fn default_client_protection_security_protocols_by_mac_role_eq(
1252        protection: FakeProtectionCfg,
1253        expected: impl IntoIterator<Item = SecurityDescriptor>,
1254    ) {
1255        let cfg = ClientConfig::default();
1256        let security_protocols: HashSet<_> = cfg
1257            .security_protocols_by_mac_role(&fake_bss_description!(protection => protection))
1258            .collect();
1259        // The protocols here are not necessarily disjoint between client and AP. Note that
1260        // security descriptors are less specific than BSS fixtures.
1261        assert_eq!(
1262            security_protocols,
1263            HashSet::from_iter(
1264                [
1265                    (SecurityDescriptor::OPEN, MacRole::Client),
1266                    (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
1267                ]
1268                .into_iter()
1269                .chain(expected.into_iter().map(|protocol| (protocol, MacRole::Ap)))
1270            ),
1271        );
1272    }
1273
1274    #[test]
1275    fn configured_client_bss_owe_compatible() {
1276        // OWE support is configurable.
1277        let cfg = ClientConfig::from_config(Config::default(), false, true);
1278        let mut security_support = fake_security_support_empty();
1279        security_support.mfp.as_mut().unwrap().supported = Some(true);
1280        security_support.owe.as_mut().unwrap().supported = Some(true);
1281        assert!(
1282            !cfg.security_protocol_intersection(&fake_bss_description!(Owe), &security_support)
1283                .is_empty()
1284        );
1285    }
1286
1287    #[test]
1288    fn configured_client_bss_wep_compatible() {
1289        // WEP support is configurable.
1290        let cfg = ClientConfig::from_config(Config::default().with_wep(), false, false);
1291        assert!(
1292            !cfg.security_protocol_intersection(
1293                &fake_bss_description!(Wep),
1294                &fake_security_support_empty()
1295            )
1296            .is_empty()
1297        );
1298    }
1299
1300    #[test]
1301    fn configured_client_bss_wpa1_compatible() {
1302        // WPA1 support is configurable.
1303        let cfg = ClientConfig::from_config(Config::default().with_wpa1(), false, false);
1304        assert!(
1305            !cfg.security_protocol_intersection(
1306                &fake_bss_description!(Wpa1),
1307                &fake_security_support_empty()
1308            )
1309            .is_empty()
1310        );
1311    }
1312
1313    #[test]
1314    fn configured_client_bss_wpa3_compatible() {
1315        // WPA3 support is configurable.
1316        let cfg = ClientConfig::from_config(Config::default(), true, false);
1317        let mut security_support = fake_security_support_empty();
1318        security_support.mfp.as_mut().unwrap().supported = Some(true);
1319        assert!(
1320            !cfg.security_protocol_intersection(&fake_bss_description!(Wpa3), &security_support)
1321                .is_empty()
1322        );
1323        assert!(
1324            !cfg.security_protocol_intersection(
1325                &fake_bss_description!(Wpa3Transition),
1326                &security_support,
1327            )
1328            .is_empty()
1329        );
1330    }
1331
1332    #[test]
1333    fn verify_rates_compatibility() {
1334        // Compatible rates.
1335        let cfg = ClientConfig::default();
1336        let device_info = test_utils::fake_device_info([1u8; 6].into());
1337        assert!(
1338            cfg.has_compatible_channel_and_data_rates(&fake_bss_description!(Open), &device_info)
1339        );
1340
1341        // Compatible rates with HT BSS membership selector (`0xFF`).
1342        let bss = fake_bss_description!(Open, rates: vec![0x8C, 0xFF]);
1343        assert!(cfg.has_compatible_channel_and_data_rates(&bss, &device_info));
1344
1345        // Incompatible rates.
1346        let bss = fake_bss_description!(Open, rates: vec![0x81]);
1347        assert!(!cfg.has_compatible_channel_and_data_rates(&bss, &device_info));
1348    }
1349
1350    #[test]
1351    fn convert_scan_result() {
1352        let cfg = ClientConfig::default();
1353        let bss_description = fake_bss_description!(Wpa2,
1354            ssid: Ssid::empty(),
1355            bssid: [0u8; 6],
1356            rssi_dbm: -30,
1357            snr_db: 0,
1358            channel: Channel::new(1, Cbw::Cbw20),
1359            ies_overrides: IesOverrides::new()
1360                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1361                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1362        );
1363        let device_info = test_utils::fake_device_info([1u8; 6].into());
1364        let timestamp = zx::MonotonicInstant::get();
1365        let scan_result = cfg.create_scan_result(
1366            timestamp,
1367            bss_description.clone(),
1368            &device_info,
1369            &fake_security_support(),
1370        );
1371
1372        assert_eq!(
1373            scan_result,
1374            ScanResult {
1375                compatibility: Compatible::expect_ok([SecurityDescriptor::WPA2_PERSONAL]),
1376                timestamp,
1377                bss_description,
1378            }
1379        );
1380
1381        let wmm_param = *ie::parse_wmm_param(&fake_wmm_param().bytes[..])
1382            .expect("expect WMM param to be parseable");
1383        let bss_description = fake_bss_description!(Wpa2,
1384            ssid: Ssid::empty(),
1385            bssid: [0u8; 6],
1386            rssi_dbm: -30,
1387            snr_db: 0,
1388            channel: Channel::new(1, Cbw::Cbw20),
1389            wmm_param: Some(wmm_param),
1390            ies_overrides: IesOverrides::new()
1391                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1392                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1393        );
1394        let timestamp = zx::MonotonicInstant::get();
1395        let scan_result = cfg.create_scan_result(
1396            timestamp,
1397            bss_description.clone(),
1398            &device_info,
1399            &fake_security_support(),
1400        );
1401
1402        assert_eq!(
1403            scan_result,
1404            ScanResult {
1405                compatibility: Compatible::expect_ok([SecurityDescriptor::WPA2_PERSONAL]),
1406                timestamp,
1407                bss_description,
1408            }
1409        );
1410
1411        let bss_description = fake_bss_description!(Wep,
1412            ssid: Ssid::empty(),
1413            bssid: [0u8; 6],
1414            rssi_dbm: -30,
1415            snr_db: 0,
1416            channel: Channel::new(1, Cbw::Cbw20),
1417            ies_overrides: IesOverrides::new()
1418                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1419                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1420        );
1421        let timestamp = zx::MonotonicInstant::get();
1422        let scan_result = cfg.create_scan_result(
1423            timestamp,
1424            bss_description.clone(),
1425            &device_info,
1426            &fake_security_support(),
1427        );
1428        assert_eq!(
1429            scan_result,
1430            ScanResult {
1431                compatibility: Incompatible::expect_err(
1432                    "incompatible channel, PHY data rates, or security protocols",
1433                    Some([
1434                        (SecurityDescriptor::WEP, MacRole::Ap),
1435                        (SecurityDescriptor::OPEN, MacRole::Client),
1436                        (SecurityDescriptor::WPA2_PERSONAL, MacRole::Client),
1437                    ])
1438                ),
1439                timestamp,
1440                bss_description,
1441            },
1442        );
1443
1444        let cfg = ClientConfig::from_config(Config::default().with_wep(), false, false);
1445        let bss_description = fake_bss_description!(Wep,
1446            ssid: Ssid::empty(),
1447            bssid: [0u8; 6],
1448            rssi_dbm: -30,
1449            snr_db: 0,
1450            channel: Channel::new(1, Cbw::Cbw20),
1451            ies_overrides: IesOverrides::new()
1452                .set(IeType::HT_CAPABILITIES, fake_ht_cap_bytes().to_vec())
1453                .set(IeType::VHT_CAPABILITIES, fake_vht_cap_bytes().to_vec()),
1454        );
1455        let timestamp = zx::MonotonicInstant::get();
1456        let scan_result = cfg.create_scan_result(
1457            timestamp,
1458            bss_description.clone(),
1459            &device_info,
1460            &fake_security_support(),
1461        );
1462        assert_eq!(
1463            scan_result,
1464            ScanResult {
1465                compatibility: Compatible::expect_ok([SecurityDescriptor::WEP]),
1466                timestamp,
1467                bss_description,
1468            }
1469        );
1470    }
1471
1472    #[test_case(EstablishRsnaFailureReason::RsnaResponseTimeout(
1473        wlan_rsn::Error::LikelyWrongCredential
1474    ))]
1475    #[test_case(EstablishRsnaFailureReason::RsnaCompletionTimeout(
1476        wlan_rsn::Error::LikelyWrongCredential
1477    ))]
1478    fn test_connect_detection_of_rejected_wpa1_or_wpa2_credentials(
1479        reason: EstablishRsnaFailureReason,
1480    ) {
1481        let failure = ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
1482            auth_method: Some(auth::MethodName::Psk),
1483            reason,
1484        });
1485        assert!(failure.likely_due_to_credential_rejected());
1486    }
1487
1488    #[test_case(fake_bss_description!(Wpa1), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1489    #[test_case(fake_bss_description!(Wpa1), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1490    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1491    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1492    #[test_case(fake_bss_description!(Wpa2), EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1493    #[test_case(fake_bss_description!(Wpa2), EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::LikelyWrongCredential))]
1494    fn test_roam_detection_of_rejected_wpa1_or_wpa2_credentials(
1495        selected_bss: BssDescription,
1496        failure_reason: EstablishRsnaFailureReason,
1497    ) {
1498        let disconnect_info = fidl_sme::DisconnectInfo {
1499            is_sme_reconnecting: false,
1500            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1501                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1502                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1503            }),
1504        };
1505        let failure = RoamFailure {
1506            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1507            failure_type: RoamFailureType::EstablishRsnaFailure,
1508            selected_bssid: selected_bss.bssid,
1509            disconnect_info,
1510            auth_method: Some(auth::MethodName::Psk),
1511            establish_rsna_failure_reason: Some(failure_reason),
1512            selected_bss: Some(selected_bss),
1513        };
1514        assert!(failure.likely_due_to_credential_rejected());
1515    }
1516
1517    #[test]
1518    fn test_connect_detection_of_rejected_wpa3_credentials() {
1519        let bss = fake_bss_description!(Wpa3);
1520        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1521            bss_protection: bss.protection(),
1522            code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
1523        });
1524
1525        assert!(failure.likely_due_to_credential_rejected());
1526    }
1527
1528    #[test]
1529    fn test_roam_detection_of_rejected_wpa3_credentials() {
1530        let selected_bss = fake_bss_description!(Wpa3);
1531        let disconnect_info = fidl_sme::DisconnectInfo {
1532            is_sme_reconnecting: false,
1533            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1534                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1535                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1536            }),
1537        };
1538        let failure = RoamFailure {
1539            status_code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
1540            failure_type: RoamFailureType::ReassociationFailure,
1541            selected_bssid: selected_bss.bssid,
1542            disconnect_info,
1543            auth_method: Some(auth::MethodName::Sae),
1544            establish_rsna_failure_reason: None,
1545            selected_bss: Some(selected_bss),
1546        };
1547        assert!(failure.likely_due_to_credential_rejected());
1548    }
1549
1550    #[test]
1551    fn test_connect_detection_of_rejected_wep_credentials() {
1552        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1553            bss_protection: BssProtection::Wep,
1554            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1555        });
1556        assert!(failure.likely_due_to_credential_rejected());
1557    }
1558
1559    #[test]
1560    fn test_roam_detection_of_rejected_wep_credentials() {
1561        let selected_bss = fake_bss_description!(Wep);
1562        let disconnect_info = fidl_sme::DisconnectInfo {
1563            is_sme_reconnecting: false,
1564            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1565                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1566                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1567            }),
1568        };
1569        let failure = RoamFailure {
1570            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1571            failure_type: RoamFailureType::ReassociationFailure,
1572            selected_bssid: selected_bss.bssid,
1573            disconnect_info,
1574            auth_method: Some(auth::MethodName::Psk),
1575            establish_rsna_failure_reason: None,
1576            selected_bss: Some(selected_bss),
1577        };
1578        assert!(failure.likely_due_to_credential_rejected());
1579    }
1580
1581    #[test]
1582    fn test_connect_no_detection_of_rejected_wpa1_or_wpa2_credentials() {
1583        let failure = ConnectFailure::ScanFailure(fidl_mlme::ScanResultCode::InternalError);
1584        assert!(!failure.likely_due_to_credential_rejected());
1585
1586        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1587            bss_protection: BssProtection::Wpa2Personal,
1588            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1589        });
1590        assert!(!failure.likely_due_to_credential_rejected());
1591    }
1592
1593    #[test_case(fake_bss_description!(Wpa1))]
1594    #[test_case(fake_bss_description!(Wpa1Wpa2TkipOnly))]
1595    #[test_case(fake_bss_description!(Wpa2))]
1596    fn test_roam_no_detection_of_rejected_wpa1_or_wpa2_credentials(selected_bss: BssDescription) {
1597        let disconnect_info = fidl_sme::DisconnectInfo {
1598            is_sme_reconnecting: false,
1599            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1600                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1601                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1602            }),
1603        };
1604        let failure = RoamFailure {
1605            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1606            failure_type: RoamFailureType::EstablishRsnaFailure,
1607            selected_bssid: selected_bss.bssid,
1608            disconnect_info,
1609            auth_method: Some(auth::MethodName::Psk),
1610            establish_rsna_failure_reason: Some(EstablishRsnaFailureReason::StartSupplicantFailed),
1611            selected_bss: Some(selected_bss),
1612        };
1613        assert!(!failure.likely_due_to_credential_rejected());
1614    }
1615
1616    #[test]
1617    fn test_connect_no_detection_of_rejected_wpa3_credentials() {
1618        let bss = fake_bss_description!(Wpa3);
1619        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1620            bss_protection: bss.protection(),
1621            code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1622        });
1623
1624        assert!(!failure.likely_due_to_credential_rejected());
1625    }
1626
1627    #[test]
1628    fn test_roam_no_detection_of_rejected_wpa3_credentials() {
1629        let selected_bss = fake_bss_description!(Wpa3);
1630        let disconnect_info = fidl_sme::DisconnectInfo {
1631            is_sme_reconnecting: false,
1632            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1633                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1634                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1635            }),
1636        };
1637        let failure = RoamFailure {
1638            status_code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
1639            failure_type: RoamFailureType::ReassociationFailure,
1640            selected_bssid: selected_bss.bssid,
1641            disconnect_info,
1642            auth_method: Some(auth::MethodName::Sae),
1643            establish_rsna_failure_reason: None,
1644            selected_bss: Some(selected_bss),
1645        };
1646        assert!(!failure.likely_due_to_credential_rejected());
1647    }
1648
1649    #[test]
1650    fn test_connect_no_detection_of_rejected_wep_credentials() {
1651        let failure = ConnectFailure::AssociationFailure(AssociationFailure {
1652            bss_protection: BssProtection::Wep,
1653            code: fidl_ieee80211::StatusCode::InvalidParameters,
1654        });
1655        assert!(!failure.likely_due_to_credential_rejected());
1656    }
1657
1658    #[test]
1659    fn test_roam_no_detection_of_rejected_wep_credentials() {
1660        let selected_bss = fake_bss_description!(Wep);
1661        let disconnect_info = fidl_sme::DisconnectInfo {
1662            is_sme_reconnecting: false,
1663            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1664                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1665                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1666            }),
1667        };
1668        let failure = RoamFailure {
1669            status_code: fidl_ieee80211::StatusCode::StatusInvalidElement,
1670            failure_type: RoamFailureType::ReassociationFailure,
1671            selected_bssid: selected_bss.bssid,
1672            disconnect_info,
1673            auth_method: Some(auth::MethodName::Psk),
1674            establish_rsna_failure_reason: None,
1675            selected_bss: Some(selected_bss),
1676        };
1677        assert!(!failure.likely_due_to_credential_rejected());
1678    }
1679
1680    #[test_case(fake_bss_description!(Open), authentication_open() => matches Ok(Protection::Open))]
1681    #[test_case(fake_bss_description!(Open), authentication_wpa2_personal_passphrase() => matches Err(_))]
1682    #[test_case(fake_bss_description!(Wpa2), authentication_wpa2_personal_passphrase() => matches Ok(Protection::Rsna(_)))]
1683    #[test_case(fake_bss_description!(Wpa2), authentication_wpa2_personal_psk() => matches Ok(Protection::Rsna(_)))]
1684    #[test_case(fake_bss_description!(Wpa2), authentication_open() => matches Err(_))]
1685    fn test_protection_from_authentication(
1686        bss: BssDescription,
1687        authentication: fidl_internal::Authentication,
1688    ) -> Result<Protection, anyhow::Error> {
1689        let device = test_utils::fake_device_info(*CLIENT_ADDR);
1690        let security_support = fake_security_support();
1691        let config = Default::default();
1692
1693        // Open BSS with open authentication:
1694        let authenticator = SecurityAuthenticator::try_from(authentication).unwrap();
1695        Protection::try_from(SecurityContext {
1696            security: &authenticator,
1697            device: &device,
1698            security_support: &security_support,
1699            config: &config,
1700            bss: &bss,
1701        })
1702    }
1703
1704    #[fuchsia::test(allow_stalls = false)]
1705    async fn status_connecting() {
1706        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
1707        assert_eq!(ClientSmeStatus::Idle, sme.status());
1708
1709        // Issue a connect command and expect the status to change appropriately.
1710        let bss_description =
1711            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1712        let _recv = sme.on_connect_command(connect_req(
1713            Ssid::try_from("foo").unwrap(),
1714            bss_description,
1715            authentication_open(),
1716        ));
1717        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1718
1719        // We should still be connecting to "foo", but the status should now come from the state
1720        // machine and not from the scanner.
1721        let ssid = assert_matches!(sme.state.as_ref().unwrap().status(), ClientSmeStatus::Connecting(ssid) => ssid);
1722        assert_eq!(Ssid::try_from("foo").unwrap(), ssid);
1723        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1724
1725        // As soon as connect command is issued for "bar", the status changes immediately
1726        let bss_description =
1727            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap());
1728        let _recv2 = sme.on_connect_command(connect_req(
1729            Ssid::try_from("bar").unwrap(),
1730            bss_description,
1731            authentication_open(),
1732        ));
1733        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bar").unwrap()), sme.status());
1734    }
1735
1736    #[test]
1737    fn connecting_to_wep_network_supported() {
1738        let _executor = fuchsia_async::TestExecutor::new();
1739        let inspector = finspect::Inspector::default();
1740        let sme_root_node = inspector.root().create_child("sme");
1741        let (mut sme, _mlme_sink, mut mlme_stream, _time_stream) = ClientSme::new(
1742            ClientConfig::from_config(SmeConfig::default().with_wep(), false, false),
1743            test_utils::fake_device_info(*CLIENT_ADDR),
1744            inspector,
1745            sme_root_node,
1746            fake_security_support(),
1747            fake_spectrum_management_support_empty(),
1748        );
1749        assert_eq!(ClientSmeStatus::Idle, sme.status());
1750
1751        // Issue a connect command and expect the status to change appropriately.
1752        let bss_description = fake_fidl_bss_description!(Wep, ssid: Ssid::try_from("foo").unwrap());
1753        let req =
1754            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_wep40());
1755        let _recv = sme.on_connect_command(req);
1756        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1757
1758        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1759    }
1760
1761    #[fuchsia::test(allow_stalls = false)]
1762    async fn test_scheduled_scan_session_events() {
1763        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1764
1765        let req = fidl_common::ScheduledScanRequest { ..Default::default() };
1766
1767        let (receiver, mut session_event_stream) = sme.on_start_scheduled_scan_command(req.clone());
1768
1769        assert_matches!(
1770            mlme_stream.try_next(),
1771            Ok(Some(MlmeRequest::StartScheduledScan(fidl_mlme::MlmeStartScheduledScanRequest { txn_id: id, req: _ }, responder))) => {
1772                assert_eq!(id, 1);
1773                responder.respond(Ok(()));
1774            }
1775        );
1776
1777        let result = receiver.await.expect("receiver failed");
1778        assert!(result.is_ok());
1779
1780        let bss = fake_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1781        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
1782            result: fidl_mlme::ScanResult { txn_id: 1, timestamp_nanos: 1000, bss: bss.into() },
1783        });
1784
1785        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScheduledScanMatchesAvailable { txn_id: 1 });
1786
1787        assert_matches!(
1788            session_event_stream.try_next(),
1789            Ok(Some(scan_results)) => {
1790                let results = wlan_common::scan::read_vmo(scan_results).unwrap();
1791                assert_eq!(results.len(), 1);
1792                let parsed_bss = wlan_common::bss::BssDescription::try_from(results[0].bss_description.clone()).unwrap();
1793                assert_eq!(parsed_bss.ssid, Ssid::try_from("foo").unwrap());
1794            }
1795        );
1796
1797        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScheduledScanStoppedByFirmware { txn_id: 1 });
1798
1799        assert_matches!(session_event_stream.try_next(), Ok(None));
1800    }
1801
1802    #[fuchsia::test(allow_stalls = false)]
1803    async fn test_concurrent_scheduled_scan_sessions() {
1804        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1805        let req = fidl_common::ScheduledScanRequest { ..Default::default() };
1806
1807        // Start session 1
1808        let (receiver1, mut session_event_stream1) =
1809            sme.on_start_scheduled_scan_command(req.clone());
1810
1811        assert_matches!(
1812            mlme_stream.try_next(),
1813            Ok(Some(MlmeRequest::StartScheduledScan(fidl_mlme::MlmeStartScheduledScanRequest { txn_id: id, req: _ }, responder))) => {
1814                assert_eq!(id, 1);
1815                assert_eq!(session_event_stream1.txn_id, 1);
1816                responder.respond(Ok(()));
1817            }
1818        );
1819        let _ = receiver1.await.unwrap();
1820
1821        // Start start session 2
1822        let (receiver2, mut session_event_stream2) =
1823            sme.on_start_scheduled_scan_command(req.clone());
1824
1825        assert_matches!(
1826            mlme_stream.try_next(),
1827            Ok(Some(MlmeRequest::StartScheduledScan(fidl_mlme::MlmeStartScheduledScanRequest { txn_id: id, req: _ }, responder))) => {
1828                assert_eq!(id, 2);
1829                assert_eq!(session_event_stream2.txn_id, 2);
1830                responder.respond(Ok(()));
1831            }
1832        );
1833        let _ = receiver2.await.unwrap();
1834
1835        // Send results for session 1
1836        let bss1 = fake_bss_description!(Open, ssid: Ssid::try_from("session1").unwrap());
1837        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
1838            result: fidl_mlme::ScanResult { txn_id: 1, timestamp_nanos: 1000, bss: bss1.into() },
1839        });
1840        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScheduledScanMatchesAvailable { txn_id: 1 });
1841
1842        // Verify session 1 receives results
1843        assert_matches!(
1844            session_event_stream1.try_next(),
1845            Ok(Some(scan_results)) => {
1846                let results = wlan_common::scan::read_vmo(scan_results).unwrap();
1847                assert_eq!(results.len(), 1);
1848                let parsed_bss = wlan_common::bss::BssDescription::try_from(results[0].bss_description.clone()).unwrap();
1849                assert_eq!(parsed_bss.ssid, Ssid::try_from("session1").unwrap());
1850            }
1851        );
1852
1853        // Verify session 2 has not received results
1854        assert_matches!(session_event_stream2.try_next(), Err(_));
1855
1856        // Stop stop session 2
1857        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScheduledScanStoppedByFirmware { txn_id: 2 });
1858
1859        assert_matches!(session_event_stream2.try_next(), Ok(None));
1860
1861        // Verify session 1 is still alive
1862        assert!(sme.scan_sched.scheduled_scan_receivers.contains_key(&1));
1863    }
1864
1865    #[fuchsia::test(allow_stalls = false)]
1866    async fn connecting_to_wep_network_unsupported() {
1867        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1868        assert_eq!(ClientSmeStatus::Idle, sme.status());
1869
1870        // Issue a connect command and expect the status to change appropriately.
1871        let bss_description = fake_fidl_bss_description!(Wep, ssid: Ssid::try_from("foo").unwrap());
1872        let req =
1873            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_wep40());
1874        let mut _connect_fut = sme.on_connect_command(req);
1875        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1876    }
1877
1878    #[fuchsia::test(allow_stalls = false)]
1879    async fn connecting_password_supplied_for_protected_network() {
1880        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1881        assert_eq!(ClientSmeStatus::Idle, sme.status());
1882
1883        // Issue a connect command and expect the status to change appropriately.
1884        let bss_description =
1885            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
1886        let req = connect_req(
1887            Ssid::try_from("foo").unwrap(),
1888            bss_description,
1889            authentication_wpa2_personal_passphrase(),
1890        );
1891        let _recv = sme.on_connect_command(req);
1892        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("foo").unwrap()), sme.status());
1893
1894        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1895    }
1896
1897    #[fuchsia::test(allow_stalls = false)]
1898    async fn connecting_psk_supplied_for_protected_network() {
1899        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1900        assert_eq!(ClientSmeStatus::Idle, sme.status());
1901
1902        // Issue a connect command and expect the status to change appropriately.
1903        let bss_description =
1904            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("IEEE").unwrap());
1905        let req = connect_req(
1906            Ssid::try_from("IEEE").unwrap(),
1907            bss_description,
1908            authentication_wpa2_personal_psk(),
1909        );
1910        let _recv = sme.on_connect_command(req);
1911        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("IEEE").unwrap()), sme.status());
1912
1913        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
1914    }
1915
1916    #[fuchsia::test(allow_stalls = false)]
1917    async fn connecting_password_supplied_for_unprotected_network() {
1918        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1919        assert_eq!(ClientSmeStatus::Idle, sme.status());
1920
1921        let bss_description =
1922            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1923        let req = connect_req(
1924            Ssid::try_from("foo").unwrap(),
1925            bss_description,
1926            authentication_wpa2_personal_passphrase(),
1927        );
1928        let mut connect_txn_stream = sme.on_connect_command(req);
1929        assert_eq!(ClientSmeStatus::Idle, sme.status());
1930
1931        // User should get a message that connection failed
1932        assert_matches!(
1933            connect_txn_stream.try_next(),
1934            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1935                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1936            }
1937        );
1938    }
1939
1940    #[fuchsia::test(allow_stalls = false)]
1941    async fn connecting_psk_supplied_for_unprotected_network() {
1942        let (mut sme, mut _mlme_stream, _time_stream) = create_sme().await;
1943        assert_eq!(ClientSmeStatus::Idle, sme.status());
1944
1945        let bss_description =
1946            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
1947        let req = connect_req(
1948            Ssid::try_from("foo").unwrap(),
1949            bss_description,
1950            authentication_wpa2_personal_psk(),
1951        );
1952        let mut connect_txn_stream = sme.on_connect_command(req);
1953        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1954
1955        // User should get a message that connection failed
1956        assert_matches!(
1957            connect_txn_stream.try_next(),
1958            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1959                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1960            }
1961        );
1962    }
1963
1964    #[fuchsia::test(allow_stalls = false)]
1965    async fn connecting_no_password_supplied_for_protected_network() {
1966        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1967        assert_eq!(ClientSmeStatus::Idle, sme.status());
1968
1969        let bss_description =
1970            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
1971        let req =
1972            connect_req(Ssid::try_from("foo").unwrap(), bss_description, authentication_open());
1973        let mut connect_txn_stream = sme.on_connect_command(req);
1974        assert_eq!(ClientSmeStatus::Idle, sme.state.as_ref().unwrap().status());
1975
1976        // No join request should be sent to MLME
1977        assert_no_connect(&mut mlme_stream);
1978
1979        // User should get a message that connection failed
1980        assert_matches!(
1981            connect_txn_stream.try_next(),
1982            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
1983                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
1984            }
1985        );
1986    }
1987
1988    #[fuchsia::test(allow_stalls = false)]
1989    async fn connecting_bypass_join_scan_open() {
1990        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
1991        assert_eq!(ClientSmeStatus::Idle, sme.status());
1992
1993        let bss_description =
1994            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("bssname").unwrap());
1995        let req =
1996            connect_req(Ssid::try_from("bssname").unwrap(), bss_description, authentication_open());
1997        let mut connect_txn_stream = sme.on_connect_command(req);
1998
1999        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bssname").unwrap()), sme.status());
2000        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
2001        // There should be no message in the connect_txn_stream
2002        assert_matches!(connect_txn_stream.try_next(), Err(_));
2003    }
2004
2005    #[fuchsia::test(allow_stalls = false)]
2006    async fn connecting_bypass_join_scan_protected() {
2007        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2008        assert_eq!(ClientSmeStatus::Idle, sme.status());
2009
2010        let bss_description =
2011            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("bssname").unwrap());
2012        let req = connect_req(
2013            Ssid::try_from("bssname").unwrap(),
2014            bss_description,
2015            authentication_wpa2_personal_passphrase(),
2016        );
2017        let mut connect_txn_stream = sme.on_connect_command(req);
2018
2019        assert_eq!(ClientSmeStatus::Connecting(Ssid::try_from("bssname").unwrap()), sme.status());
2020        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(..))));
2021        // There should be no message in the connect_txn_stream
2022        assert_matches!(connect_txn_stream.try_next(), Err(_));
2023    }
2024
2025    #[fuchsia::test(allow_stalls = false)]
2026    async fn connecting_bypass_join_scan_mismatched_credential() {
2027        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2028        assert_eq!(ClientSmeStatus::Idle, sme.status());
2029
2030        let bss_description =
2031            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("bssname").unwrap());
2032        let req =
2033            connect_req(Ssid::try_from("bssname").unwrap(), bss_description, authentication_open());
2034        let mut connect_txn_stream = sme.on_connect_command(req);
2035
2036        assert_eq!(ClientSmeStatus::Idle, sme.status());
2037        assert_no_connect(&mut mlme_stream);
2038
2039        // User should get a message that connection failed
2040        assert_matches!(
2041            connect_txn_stream.try_next(),
2042            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2043                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2044            }
2045        );
2046    }
2047
2048    #[fuchsia::test(allow_stalls = false)]
2049    async fn connecting_bypass_join_scan_unsupported_bss() {
2050        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2051        assert_eq!(ClientSmeStatus::Idle, sme.status());
2052
2053        let bss_description =
2054            fake_fidl_bss_description!(Wpa3Enterprise, ssid: Ssid::try_from("bssname").unwrap());
2055        let req = connect_req(
2056            Ssid::try_from("bssname").unwrap(),
2057            bss_description,
2058            authentication_wpa3_personal_passphrase(),
2059        );
2060        let mut connect_txn_stream = sme.on_connect_command(req);
2061
2062        assert_eq!(ClientSmeStatus::Idle, sme.status());
2063        assert_no_connect(&mut mlme_stream);
2064
2065        // User should get a message that connection failed
2066        assert_matches!(
2067            connect_txn_stream.try_next(),
2068            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2069                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2070            }
2071        );
2072    }
2073
2074    #[fuchsia::test(allow_stalls = false)]
2075    async fn connecting_right_credential_type_no_privacy() {
2076        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
2077
2078        let bss_description = fake_fidl_bss_description!(
2079            Wpa2,
2080            ssid: Ssid::try_from("foo").unwrap(),
2081        );
2082        // Manually override the privacy bit since fake_fidl_bss_description!()
2083        // does not allow setting it directly.
2084        let bss_description = fidl_ieee80211::BssDescription {
2085            capability_info: wlan_common::mac::CapabilityInfo(bss_description.capability_info)
2086                .with_privacy(false)
2087                .0,
2088            ..bss_description
2089        };
2090        let mut connect_txn_stream = sme.on_connect_command(connect_req(
2091            Ssid::try_from("foo").unwrap(),
2092            bss_description,
2093            authentication_wpa2_personal_passphrase(),
2094        ));
2095
2096        assert_matches!(
2097            connect_txn_stream.try_next(),
2098            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2099                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2100            }
2101        );
2102    }
2103
2104    #[fuchsia::test(allow_stalls = false)]
2105    async fn connecting_mismatched_security_protocol() {
2106        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
2107
2108        let bss_description =
2109            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap());
2110        let mut connect_txn_stream = sme.on_connect_command(connect_req(
2111            Ssid::try_from("wpa2").unwrap(),
2112            bss_description,
2113            authentication_wep40(),
2114        ));
2115        assert_matches!(
2116            connect_txn_stream.try_next(),
2117            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2118                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2119            }
2120        );
2121
2122        let bss_description =
2123            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap());
2124        let mut connect_txn_stream = sme.on_connect_command(connect_req(
2125            Ssid::try_from("wpa2").unwrap(),
2126            bss_description,
2127            authentication_wpa1_passphrase(),
2128        ));
2129        assert_matches!(
2130            connect_txn_stream.try_next(),
2131            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2132                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2133            }
2134        );
2135
2136        let bss_description =
2137            fake_fidl_bss_description!(Wpa3, ssid: Ssid::try_from("wpa3").unwrap());
2138        let mut connect_txn_stream = sme.on_connect_command(connect_req(
2139            Ssid::try_from("wpa3").unwrap(),
2140            bss_description,
2141            authentication_wpa2_personal_passphrase(),
2142        ));
2143        assert_matches!(
2144            connect_txn_stream.try_next(),
2145            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2146                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2147            }
2148        );
2149    }
2150
2151    // Disable logging to prevent failure from emitted error logs.
2152    #[fuchsia::test(allow_stalls = false, logging = false)]
2153    async fn connecting_right_credential_type_but_short_password() {
2154        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
2155
2156        let bss_description =
2157            fake_fidl_bss_description!(Wpa2, ssid: Ssid::try_from("foo").unwrap());
2158        let mut connect_txn_stream = sme.on_connect_command(connect_req(
2159            Ssid::try_from("foo").unwrap(),
2160            bss_description.clone(),
2161            fidl_internal::Authentication {
2162                protocol: fidl_internal::Protocol::Wpa2Personal,
2163                credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
2164                    fidl_internal::WpaCredentials::Passphrase(b"nope".as_slice().into()),
2165                ))),
2166            },
2167        ));
2168        report_fake_scan_result(
2169            &mut sme,
2170            zx::MonotonicInstant::get().into_nanos(),
2171            bss_description,
2172        );
2173
2174        assert_matches!(
2175            connect_txn_stream.try_next(),
2176            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2177                assert_eq!(result, SelectNetworkFailure::IncompatibleConnectRequest.into());
2178            }
2179        );
2180    }
2181
2182    // Disable logging to prevent failure from emitted error logs.
2183    #[fuchsia::test(allow_stalls = false, logging = false)]
2184    async fn new_connect_attempt_cancels_pending_connect() {
2185        let (mut sme, _mlme_stream, _time_stream) = create_sme().await;
2186
2187        let bss_description =
2188            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2189        let req = connect_req(
2190            Ssid::try_from("foo").unwrap(),
2191            bss_description.clone(),
2192            authentication_open(),
2193        );
2194        let mut connect_txn_stream1 = sme.on_connect_command(req);
2195
2196        let req2 = connect_req(
2197            Ssid::try_from("foo").unwrap(),
2198            bss_description.clone(),
2199            authentication_open(),
2200        );
2201        let mut connect_txn_stream2 = sme.on_connect_command(req2);
2202
2203        // User should get a message that first connection attempt is canceled
2204        assert_matches!(
2205            connect_txn_stream1.try_next(),
2206            Ok(Some(ConnectTransactionEvent::OnConnectResult {
2207                result: ConnectResult::Canceled,
2208                is_reconnect: false
2209            }))
2210        );
2211
2212        // Report scan result to transition second connection attempt past scan. This is to verify
2213        // that connection attempt will be canceled even in the middle of joining the network
2214        report_fake_scan_result(
2215            &mut sme,
2216            zx::MonotonicInstant::get().into_nanos(),
2217            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap()),
2218        );
2219
2220        let req3 = connect_req(
2221            Ssid::try_from("foo").unwrap(),
2222            bss_description.clone(),
2223            authentication_open(),
2224        );
2225        let mut _connect_fut3 = sme.on_connect_command(req3);
2226
2227        // Verify that second connection attempt is canceled as new connect request comes in
2228        assert_matches!(
2229            connect_txn_stream2.try_next(),
2230            Ok(Some(ConnectTransactionEvent::OnConnectResult {
2231                result: ConnectResult::Canceled,
2232                is_reconnect: false
2233            }))
2234        );
2235    }
2236
2237    #[fuchsia::test(allow_stalls = false)]
2238    async fn test_simple_scan_error() {
2239        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2240        let mut recv =
2241            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
2242                channels: vec![],
2243            }));
2244
2245        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2246            end: fidl_mlme::ScanEnd {
2247                txn_id: 1,
2248                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2249            },
2250        });
2251
2252        assert_eq!(
2253            recv.try_recv(),
2254            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2255        );
2256    }
2257
2258    #[fuchsia::test(allow_stalls = false)]
2259    async fn test_scan_error_after_some_results_returned() {
2260        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2261        let mut recv =
2262            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
2263                channels: vec![],
2264            }));
2265
2266        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2267        bss.bssid = [3; 6];
2268        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2269            result: fidl_mlme::ScanResult {
2270                txn_id: 1,
2271                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2272                bss,
2273            },
2274        });
2275        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2276        bss.bssid = [4; 6];
2277        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2278            result: fidl_mlme::ScanResult {
2279                txn_id: 1,
2280                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2281                bss,
2282            },
2283        });
2284
2285        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2286            end: fidl_mlme::ScanEnd {
2287                txn_id: 1,
2288                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2289            },
2290        });
2291
2292        // Scan results are lost when an error occurs.
2293        assert_eq!(
2294            recv.try_recv(),
2295            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2296        );
2297    }
2298
2299    #[fuchsia::test(allow_stalls = false)]
2300    async fn test_scan_is_rejected_while_connecting() {
2301        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2302
2303        // Send a connect command to move SME into Connecting state
2304        let bss_description =
2305            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2306        let _recv = sme.on_connect_command(connect_req(
2307            Ssid::try_from("foo").unwrap(),
2308            bss_description,
2309            authentication_open(),
2310        ));
2311        assert_matches!(sme.status(), ClientSmeStatus::Connecting(_));
2312
2313        // Send a scan command and verify a ShouldWait response is returned
2314        let mut recv =
2315            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
2316                channels: vec![],
2317            }));
2318        assert_eq!(recv.try_recv(), Ok(Some(Err(fidl_mlme::ScanResultCode::ShouldWait))));
2319    }
2320
2321    #[fuchsia::test(allow_stalls = false)]
2322    async fn test_wmm_status_success() {
2323        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2324        let mut receiver = sme.wmm_status();
2325
2326        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2327
2328        let resp = fake_wmm_status_resp();
2329        #[allow(
2330            clippy::redundant_field_names,
2331            reason = "mass allow for https://fxbug.dev/381896734"
2332        )]
2333        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnWmmStatusResp {
2334            status: zx::sys::ZX_OK,
2335            resp: resp,
2336        });
2337
2338        assert_eq!(receiver.try_recv(), Ok(Some(Ok(resp))));
2339    }
2340
2341    #[fuchsia::test(allow_stalls = false)]
2342    async fn test_wmm_status_failed() {
2343        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2344        let mut receiver = sme.wmm_status();
2345
2346        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2347        sme.on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_ERR_IO));
2348        assert_eq!(receiver.try_recv(), Ok(Some(Err(zx::sys::ZX_ERR_IO))));
2349    }
2350
2351    #[fuchsia::test(allow_stalls = false)]
2352    async fn test_query_apf_packet_filter_support() {
2353        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2354        let mut _receiver = sme.query_apf_packet_filter_support();
2355        assert_matches!(
2356            mlme_stream.try_next(),
2357            Ok(Some(MlmeRequest::QueryApfPacketFilterSupport(..)))
2358        );
2359    }
2360
2361    #[fuchsia::test(allow_stalls = false)]
2362    async fn test_install_apf_packet_filter() {
2363        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2364        let program = vec![1, 2, 3];
2365        let mut _receiver = sme.install_apf_packet_filter(program.clone());
2366        let req = assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::InstallApfPacketFilter(req, ..))) => req);
2367        assert_eq!(req.program, program);
2368    }
2369
2370    #[fuchsia::test(allow_stalls = false)]
2371    async fn test_read_apf_packet_filter_data() {
2372        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2373        let mut _receiver = sme.read_apf_packet_filter_data();
2374        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::ReadApfPacketFilterData(..))));
2375    }
2376
2377    #[fuchsia::test(allow_stalls = false)]
2378    async fn test_set_apf_packet_filter_enabled() {
2379        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2380        let mut _receiver = sme.set_apf_packet_filter_enabled(true);
2381        let req = assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetApfPacketFilterEnabled(req, ..))) => req);
2382        assert!(req.enabled);
2383    }
2384
2385    #[fuchsia::test(allow_stalls = false)]
2386    async fn test_get_apf_packet_filter_enabled() {
2387        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2388        let mut _receiver = sme.get_apf_packet_filter_enabled();
2389        assert_matches!(
2390            mlme_stream.try_next(),
2391            Ok(Some(MlmeRequest::GetApfPacketFilterEnabled(..)))
2392        );
2393    }
2394
2395    fn assert_no_connect(mlme_stream: &mut mpsc::UnboundedReceiver<MlmeRequest>) {
2396        loop {
2397            match mlme_stream.try_next() {
2398                Ok(event) => match event {
2399                    Some(MlmeRequest::Connect(..)) => {
2400                        panic!("unexpected connect request sent to MLME")
2401                    }
2402                    None => break,
2403                    _ => (),
2404                },
2405                Err(e) => {
2406                    assert_eq!(e.to_string(), "receiver channel is empty");
2407                    break;
2408                }
2409            }
2410        }
2411    }
2412
2413    fn connect_req(
2414        ssid: Ssid,
2415        bss_description: fidl_ieee80211::BssDescription,
2416        authentication: fidl_internal::Authentication,
2417    ) -> fidl_sme::ConnectRequest {
2418        fidl_sme::ConnectRequest {
2419            ssid: ssid.to_vec(),
2420            bss_description,
2421            multiple_bss_candidates: true,
2422            authentication,
2423            deprecated_scan_type: fidl_common::ScanType::Passive,
2424        }
2425    }
2426
2427    // The unused _exec parameter ensures that an executor exists for the lifetime of the SME.
2428    // Our internal timer implementation relies on the existence of a local executor.
2429    //
2430    // TODO(https://fxbug.dev/327499461): This function is async to ensure SME functions will
2431    // run in an async context and not call `wlan_common::timer::Timer::now` without an
2432    // executor.
2433    async fn create_sme() -> (ClientSme, MlmeStream, timer::EventStream<Event>) {
2434        let inspector = finspect::Inspector::default();
2435        let sme_root_node = inspector.root().create_child("sme");
2436        let (client_sme, _mlme_sink, mlme_stream, time_stream) = ClientSme::new(
2437            ClientConfig::default(),
2438            test_utils::fake_device_info(*CLIENT_ADDR),
2439            inspector,
2440            sme_root_node,
2441            fake_security_support(),
2442            fake_spectrum_management_support_empty(),
2443        );
2444        (client_sme, mlme_stream, time_stream)
2445    }
2446}