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