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