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