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 fidl_fuchsia_wlan_common as fidl_common;
24use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
25use fidl_fuchsia_wlan_internal as fidl_internal;
26use fidl_fuchsia_wlan_mlme as fidl_mlme;
27use fidl_fuchsia_wlan_sme as fidl_sme;
28use fidl_fuchsia_wlan_stats as fidl_stats;
29use futures::channel::{mpsc, oneshot};
30use ieee80211::{Bssid, MacAddrBytes, Ssid};
31use log::{error, info, warn};
32use std::sync::Arc;
33use wlan_common::bss::{BssDescription, Protection as BssProtection};
34use wlan_common::capabilities::derive_join_capabilities;
35use wlan_common::ie::rsn::rsne;
36use wlan_common::ie::{self, wsc};
37use wlan_common::mac::MacRole;
38use wlan_common::scan::{Compatibility, Compatible, Incompatible, ScanResult};
39use wlan_common::security::{SecurityAuthenticator, SecurityDescriptor};
40use wlan_common::sink::UnboundedSink;
41use wlan_common::timer;
42use wlan_rsn::auth;
43
44// This is necessary to trick the private-in-public checker.
45// A private module is not allowed to include private types in its interface,
46// even though the module itself is private and will never be exported.
47// As a workaround, we add another private module with public types.
48mod internal {
49    use crate::MlmeSink;
50    use crate::client::event::Event;
51    use crate::client::{ConnectionAttemptId, inspect};
52    use fidl_fuchsia_wlan_common as fidl_common;
53    use fidl_fuchsia_wlan_mlme as fidl_mlme;
54    use std::sync::Arc;
55    use wlan_common::timer::Timer;
56
57    pub struct Context {
58        pub device_info: Arc<fidl_mlme::DeviceInfo>,
59        pub mlme_sink: MlmeSink,
60        pub(crate) timer: Timer<Event>,
61        pub att_id: ConnectionAttemptId,
62        pub(crate) inspect: Arc<inspect::SmeTree>,
63        pub security_support: fidl_common::SecuritySupport,
64    }
65}
66
67use self::internal::*;
68
69// An automatically increasing sequence number that uniquely identifies a logical
70// connection attempt. For example, a new connection attempt can be triggered
71// by a DisassociateInd message from the MLME.
72pub type ConnectionAttemptId = u64;
73
74pub type ScanTxnId = u64;
75
76#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
77pub struct ClientConfig {
78    cfg: Config,
79    pub wpa3_supported: bool,
80    pub owe_supported: bool,
81}
82
83impl ClientConfig {
84    pub fn from_config(cfg: Config, wpa3_supported: bool, owe_supported: bool) -> Self {
85        Self { cfg, wpa3_supported, owe_supported }
86    }
87
88    /// Converts a given BssDescription into a ScanResult.
89    pub fn create_scan_result(
90        &self,
91        timestamp: zx::MonotonicInstant,
92        bss_description: BssDescription,
93        device_info: &fidl_mlme::DeviceInfo,
94        security_support: &fidl_common::SecuritySupport,
95    ) -> ScanResult {
96        ScanResult {
97            compatibility: self.bss_compatibility(&bss_description, device_info, security_support),
98            timestamp,
99            bss_description,
100        }
101    }
102
103    /// Gets the compatible modes of operation of the BSS with respect to driver and hardware
104    /// support.
105    ///
106    /// Returns `None` if the BSS is not supported by the client.
107    pub fn bss_compatibility(
108        &self,
109        bss: &BssDescription,
110        device_info: &fidl_mlme::DeviceInfo,
111        security_support: &fidl_common::SecuritySupport,
112    ) -> Compatibility {
113        // TODO(https://fxbug.dev/384797729): Include information about disjoint channels and data
114        //                                    rates in `Incompatible`.
115        self.has_compatible_channel_and_data_rates(bss, device_info)
116            .then(|| {
117                Compatible::try_from_features(
118                    self.security_protocol_intersection(bss, security_support),
119                )
120            })
121            .flatten()
122            .ok_or_else(|| {
123                Incompatible::try_from_features(
124                    "incompatible channel, PHY data rates, or security protocols",
125                    Some(self.security_protocols_by_mac_role(bss)),
126                )
127                .unwrap_or_else(|| {
128                    Incompatible::from_description("incompatible channel or PHY data rates")
129                })
130            })
131    }
132
133    /// Gets the intersection of security protocols supported by the BSS and local interface.
134    ///
135    /// Security protocol support of the local interface is determined by the given
136    /// `SecuritySupport`. The set of mutually supported protocols may be empty.
137    fn security_protocol_intersection(
138        &self,
139        bss: &BssDescription,
140        security_support: &fidl_common::SecuritySupport,
141    ) -> Vec<SecurityDescriptor> {
142        // Construct queries for security protocol support based on hardware, driver, and BSS
143        // compatibility.
144        let has_privacy = wlan_common::mac::CapabilityInfo(bss.capability_info).privacy();
145        let has_owe_support = || {
146            self.owe_supported
147                && has_privacy
148                && bss.rsne().is_some_and(|rsne| {
149                    rsne::from_bytes(rsne)
150                        .is_ok_and(|(_, a_rsne)| a_rsne.is_owe_rsn_compatible(security_support))
151                })
152        };
153        let has_wep_support = || self.cfg.wep_supported;
154        let has_wpa1_support = || self.cfg.wpa1_supported;
155        let has_wpa2_support = || {
156            // TODO(https://fxbug.dev/42059694): Unlike other protocols, hardware and driver
157            //                                   support for WPA2 is assumed here. Query and track
158            //                                   this as with other security protocols.
159            has_privacy
160                && bss.rsne().is_some_and(|rsne| {
161                    rsne::from_bytes(rsne)
162                        .is_ok_and(|(_, a_rsne)| a_rsne.is_wpa2_rsn_compatible(security_support))
163                })
164        };
165        let has_wpa3_support = || {
166            self.wpa3_supported
167                && has_privacy
168                && bss.rsne().is_some_and(|rsne| {
169                    rsne::from_bytes(rsne)
170                        .is_ok_and(|(_, a_rsne)| a_rsne.is_wpa3_rsn_compatible(security_support))
171                })
172        };
173
174        // Determine security protocol compatibility. This `match` expression does not use guard
175        // expressions to avoid implicit patterns like `_`, which may introduce bugs if
176        // `BssProtection` changes.
177        match bss.protection() {
178            BssProtection::Open => vec![SecurityDescriptor::OPEN],
179            BssProtection::OpenOweTransition if has_owe_support() => {
180                vec![SecurityDescriptor::OWE, SecurityDescriptor::OPEN]
181            }
182            BssProtection::OpenOweTransition => vec![SecurityDescriptor::OPEN],
183            BssProtection::Owe if has_owe_support() => vec![SecurityDescriptor::OWE],
184            BssProtection::Owe => vec![],
185            BssProtection::Wep if has_wep_support() => vec![SecurityDescriptor::WEP],
186            BssProtection::Wep => vec![],
187            BssProtection::Wpa1 if has_wpa1_support() => vec![SecurityDescriptor::WPA1],
188            BssProtection::Wpa1 => vec![],
189            BssProtection::Wpa1Wpa2PersonalTkipOnly | BssProtection::Wpa1Wpa2Personal => {
190                has_wpa2_support()
191                    .then_some(SecurityDescriptor::WPA2_PERSONAL)
192                    .into_iter()
193                    .chain(has_wpa1_support().then_some(SecurityDescriptor::WPA1))
194                    .collect()
195            }
196            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal
197                if has_wpa2_support() =>
198            {
199                vec![SecurityDescriptor::WPA2_PERSONAL]
200            }
201            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal => vec![],
202            BssProtection::Wpa2Wpa3Personal => match (has_wpa3_support(), has_wpa2_support()) {
203                (true, true) => {
204                    vec![SecurityDescriptor::WPA3_PERSONAL, SecurityDescriptor::WPA2_PERSONAL]
205                }
206                (true, false) => vec![SecurityDescriptor::WPA3_PERSONAL],
207                (false, true) => vec![SecurityDescriptor::WPA2_PERSONAL],
208                (false, false) => vec![],
209            },
210            BssProtection::Wpa3Personal if has_wpa3_support() => {
211                vec![SecurityDescriptor::WPA3_PERSONAL]
212            }
213            BssProtection::Wpa3Personal => vec![],
214            // TODO(https://fxbug.dev/42174395): Implement conversions for WPA Enterprise protocols.
215            BssProtection::Wpa2Enterprise | BssProtection::Wpa3Enterprise => vec![],
216            BssProtection::Unknown => vec![],
217        }
218    }
219
220    fn security_protocols_by_mac_role(
221        &self,
222        bss: &BssDescription,
223    ) -> impl Iterator<Item = (SecurityDescriptor, MacRole)> {
224        let has_privacy = wlan_common::mac::CapabilityInfo(bss.capability_info).privacy();
225        let has_wep_support = || self.cfg.wep_supported;
226        let has_wpa1_support = || self.cfg.wpa1_supported;
227        let has_wpa2_support = || {
228            // TODO(https://fxbug.dev/42059694): Unlike other protocols, hardware and driver
229            //                                   support for WPA2 is assumed here. Query and track
230            //                                   this as with other security protocols.
231            has_privacy
232        };
233        let has_wpa3_support = || self.wpa3_supported && has_privacy;
234        let client_security_protocols = Some(SecurityDescriptor::OPEN)
235            .into_iter()
236            .chain(has_wep_support().then_some(SecurityDescriptor::WEP))
237            .chain(has_wpa1_support().then_some(SecurityDescriptor::WPA1))
238            .chain(has_wpa2_support().then_some(SecurityDescriptor::WPA2_PERSONAL))
239            .chain(has_wpa3_support().then_some(SecurityDescriptor::WPA3_PERSONAL))
240            .map(|descriptor| (descriptor, MacRole::Client));
241
242        let bss_security_protocols = match bss.protection() {
243            BssProtection::Open => &[SecurityDescriptor::OPEN][..],
244            BssProtection::OpenOweTransition => &[SecurityDescriptor::OPEN][..],
245            BssProtection::Owe => &[SecurityDescriptor::OWE][..],
246            BssProtection::Wep => &[SecurityDescriptor::WEP][..],
247            BssProtection::Wpa1 => &[SecurityDescriptor::WPA1][..],
248            BssProtection::Wpa1Wpa2PersonalTkipOnly | BssProtection::Wpa1Wpa2Personal => {
249                &[SecurityDescriptor::WPA1, SecurityDescriptor::WPA2_PERSONAL][..]
250            }
251            BssProtection::Wpa2PersonalTkipOnly | BssProtection::Wpa2Personal => {
252                &[SecurityDescriptor::WPA2_PERSONAL][..]
253            }
254            BssProtection::Wpa2Wpa3Personal => {
255                &[SecurityDescriptor::WPA3_PERSONAL, SecurityDescriptor::WPA2_PERSONAL][..]
256            }
257            BssProtection::Wpa3Personal => &[SecurityDescriptor::WPA3_PERSONAL][..],
258            // TODO(https://fxbug.dev/42174395): Implement conversions for WPA Enterprise protocols.
259            BssProtection::Wpa2Enterprise | BssProtection::Wpa3Enterprise => &[],
260            BssProtection::Unknown => &[],
261        }
262        .iter()
263        .cloned()
264        .map(|descriptor| (descriptor, MacRole::Ap));
265
266        client_security_protocols.chain(bss_security_protocols)
267    }
268
269    fn has_compatible_channel_and_data_rates(
270        &self,
271        bss: &BssDescription,
272        device_info: &fidl_mlme::DeviceInfo,
273    ) -> bool {
274        derive_join_capabilities(bss.channel, bss.rates(), device_info).is_ok()
275    }
276}
277
278pub struct ClientSme {
279    cfg: ClientConfig,
280    state: Option<ClientState>,
281    scan_sched: ScanScheduler<Responder<Result<Vec<ScanResult>, fidl_mlme::ScanResultCode>>>,
282    wmm_status_responders: Vec<Responder<fidl_sme::ClientSmeWmmStatusResult>>,
283    context: Context,
284}
285
286#[derive(Debug, PartialEq)]
287pub enum ConnectResult {
288    Success,
289    Canceled,
290    Failed(ConnectFailure),
291}
292
293impl<T: Into<ConnectFailure>> From<T> for ConnectResult {
294    fn from(failure: T) -> Self {
295        ConnectResult::Failed(failure.into())
296    }
297}
298
299#[derive(Debug, PartialEq)]
300pub enum RoamResult {
301    Success(Box<BssDescription>),
302    Failed(Box<RoamFailure>),
303}
304
305impl<T: Into<RoamFailure>> From<T> for RoamResult {
306    fn from(failure: T) -> Self {
307        RoamResult::Failed(Box::new(failure.into()))
308    }
309}
310
311#[derive(Debug)]
312pub struct ConnectTransactionSink {
313    sink: UnboundedSink<ConnectTransactionEvent>,
314    is_reconnecting: bool,
315}
316
317impl ConnectTransactionSink {
318    pub fn new_unbounded() -> (Self, ConnectTransactionStream) {
319        let (sender, receiver) = mpsc::unbounded();
320        let sink =
321            ConnectTransactionSink { sink: UnboundedSink::new(sender), is_reconnecting: false };
322        (sink, receiver)
323    }
324
325    pub fn is_reconnecting(&self) -> bool {
326        self.is_reconnecting
327    }
328
329    pub fn send_connect_result(&mut self, result: ConnectResult) {
330        let event =
331            ConnectTransactionEvent::OnConnectResult { result, is_reconnect: self.is_reconnecting };
332        self.send(event);
333    }
334
335    pub fn send_roam_result(&mut self, result: RoamResult) {
336        let event = ConnectTransactionEvent::OnRoamResult { result };
337        self.send(event);
338    }
339
340    pub fn send(&mut self, event: ConnectTransactionEvent) {
341        if let ConnectTransactionEvent::OnDisconnect { info } = &event {
342            self.is_reconnecting = info.is_sme_reconnecting;
343        };
344        self.sink.send(event);
345    }
346}
347
348pub type ConnectTransactionStream = mpsc::UnboundedReceiver<ConnectTransactionEvent>;
349
350#[derive(Debug, PartialEq)]
351pub enum ConnectTransactionEvent {
352    OnConnectResult { result: ConnectResult, is_reconnect: bool },
353    OnRoamResult { result: RoamResult },
354    OnDisconnect { info: fidl_sme::DisconnectInfo },
355    OnSignalReport { ind: fidl_internal::SignalReportIndication },
356    OnChannelSwitched { info: fidl_internal::ChannelSwitchInfo },
357}
358
359#[derive(Debug, PartialEq)]
360pub enum ConnectFailure {
361    SelectNetworkFailure(SelectNetworkFailure),
362    // TODO(https://fxbug.dev/42147565): SME no longer performs scans when connecting. Remove the
363    //                        `ScanFailure` variant.
364    ScanFailure(fidl_mlme::ScanResultCode),
365    // TODO(https://fxbug.dev/42178810): `JoinFailure` and `AuthenticationFailure` no longer needed when
366    //                        state machine is fully transitioned to USME.
367    JoinFailure(fidl_ieee80211::StatusCode),
368    AuthenticationFailure(fidl_ieee80211::StatusCode),
369    AssociationFailure(AssociationFailure),
370    EstablishRsnaFailure(EstablishRsnaFailure),
371}
372
373impl ConnectFailure {
374    // TODO(https://fxbug.dev/42163244): ConnectFailure::is_timeout is not useful, remove it
375    #[allow(clippy::collapsible_match, reason = "mass allow for https://fxbug.dev/381896734")]
376    #[allow(
377        clippy::match_like_matches_macro,
378        reason = "mass allow for https://fxbug.dev/381896734"
379    )]
380    pub fn is_timeout(&self) -> bool {
381        // Note: For association, we don't have a failure type for timeout, so cannot deduce
382        //       whether an association failure is due to timeout.
383        match self {
384            ConnectFailure::AuthenticationFailure(failure) => match failure {
385                fidl_ieee80211::StatusCode::RejectedSequenceTimeout => true,
386                _ => false,
387            },
388            ConnectFailure::EstablishRsnaFailure(failure) => match failure {
389                EstablishRsnaFailure {
390                    reason: EstablishRsnaFailureReason::RsnaResponseTimeout(_),
391                    ..
392                }
393                | EstablishRsnaFailure {
394                    reason: EstablishRsnaFailureReason::RsnaCompletionTimeout(_),
395                    ..
396                } => true,
397                _ => false,
398            },
399            _ => false,
400        }
401    }
402
403    /// Returns true if failure was likely caused by rejected
404    /// credentials. In some cases, we cannot be 100% certain that
405    /// credentials were rejected, but it's worth noting when we
406    /// observe a failure event that was more than likely caused by
407    /// rejected credentials.
408    pub fn likely_due_to_credential_rejected(&self) -> bool {
409        match self {
410            // Assuming the correct type of credentials are given, a
411            // bad password will cause a variety of errors depending
412            // on the security type. All of the following cases assume
413            // no frames were dropped unintentionally. For example,
414            // it's possible to conflate a WPA2 bad password error
415            // with a dropped frame at just the right moment since the
416            // error itself is *caused by* a dropped frame.
417
418            // For WPA1 and WPA2, the error will be
419            // RsnaResponseTimeout or RsnaCompletionTimeout.  When
420            // the authenticator receives a bad MIC (derived from the
421            // password), it will silently drop the EAPOL handshake
422            // frame it received.
423            //
424            // NOTE: The alternative possibilities for seeing these
425            // errors are an error in our crypto parameter parsing and
426            // crypto implementation, or a lost connection with the AP.
427            ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
428                auth_method: Some(auth::MethodName::Psk),
429                reason:
430                    EstablishRsnaFailureReason::RsnaResponseTimeout(
431                        wlan_rsn::Error::LikelyWrongCredential,
432                    ),
433            })
434            | ConnectFailure::EstablishRsnaFailure(EstablishRsnaFailure {
435                auth_method: Some(auth::MethodName::Psk),
436                reason:
437                    EstablishRsnaFailureReason::RsnaCompletionTimeout(
438                        wlan_rsn::Error::LikelyWrongCredential,
439                    ),
440            }) => true,
441
442            // For WEP, the entire association is always handled by
443            // fullmac, so the best we can do is use
444            // fidl_mlme::AssociateResultCode. The code that arises
445            // when WEP fails with rejected credentials is
446            // RefusedReasonUnspecified. This is a catch-all error for
447            // a WEP authentication failure, but it is being
448            // considered good enough for catching rejected
449            // credentials for a deprecated WEP association.
450            ConnectFailure::AssociationFailure(AssociationFailure {
451                bss_protection: BssProtection::Wep,
452                code: fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported,
453            }) => true,
454
455            // For WPA3, the AP will not respond to SAE authentication frames
456            // if it detects an invalid credential, so we expect the connection
457            // attempt to time out.
458            ConnectFailure::AssociationFailure(AssociationFailure {
459                bss_protection: BssProtection::Wpa3Personal,
460                code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
461            })
462            | ConnectFailure::AssociationFailure(AssociationFailure {
463                bss_protection: BssProtection::Wpa2Wpa3Personal,
464                code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
465            }) => true,
466            _ => false,
467        }
468    }
469
470    pub fn status_code(&self) -> fidl_ieee80211::StatusCode {
471        match self {
472            ConnectFailure::JoinFailure(code)
473            | ConnectFailure::AuthenticationFailure(code)
474            | ConnectFailure::AssociationFailure(AssociationFailure { code, .. }) => *code,
475            ConnectFailure::EstablishRsnaFailure(..) => {
476                fidl_ieee80211::StatusCode::EstablishRsnaFailure
477            }
478            // SME no longer does join scan, so these two failures should no longer happen
479            ConnectFailure::ScanFailure(fidl_mlme::ScanResultCode::ShouldWait) => {
480                fidl_ieee80211::StatusCode::Canceled
481            }
482            ConnectFailure::SelectNetworkFailure(..) | ConnectFailure::ScanFailure(..) => {
483                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
484            }
485        }
486    }
487}
488
489#[derive(Debug, PartialEq)]
490pub enum RoamFailureType {
491    SelectNetworkFailure,
492    RoamStartMalformedFailure,
493    RoamResultMalformedFailure,
494    RoamRequestMalformedFailure,
495    RoamConfirmationMalformedFailure,
496    ReassociationFailure,
497    EstablishRsnaFailure,
498}
499
500#[derive(Debug, PartialEq)]
501pub struct RoamFailure {
502    failure_type: RoamFailureType,
503    pub selected_bssid: Bssid,
504    pub status_code: fidl_ieee80211::StatusCode,
505    pub disconnect_info: fidl_sme::DisconnectInfo,
506    auth_method: Option<auth::MethodName>,
507    pub selected_bss: Option<BssDescription>,
508    establish_rsna_failure_reason: Option<EstablishRsnaFailureReason>,
509}
510
511impl RoamFailure {
512    /// Returns true if failure was likely caused by rejected credentials.
513    /// Very similar to `ConnectFailure::likely_due_to_credential_rejected`.
514    #[allow(
515        clippy::match_like_matches_macro,
516        reason = "mass allow for https://fxbug.dev/381896734"
517    )]
518    pub fn likely_due_to_credential_rejected(&self) -> bool {
519        match self.failure_type {
520            // WPA1 and WPA2
521            RoamFailureType::EstablishRsnaFailure => match self.auth_method {
522                Some(auth::MethodName::Psk) => match self.establish_rsna_failure_reason {
523                    Some(EstablishRsnaFailureReason::RsnaResponseTimeout(
524                        wlan_rsn::Error::LikelyWrongCredential,
525                    ))
526                    | Some(EstablishRsnaFailureReason::RsnaCompletionTimeout(
527                        wlan_rsn::Error::LikelyWrongCredential,
528                    )) => true,
529                    _ => false,
530                },
531                _ => false,
532            },
533            RoamFailureType::ReassociationFailure => {
534                match &self.selected_bss {
535                    Some(selected_bss) => match selected_bss.protection() {
536                        // WEP
537                        BssProtection::Wep => match self.status_code {
538                            fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported => true,
539                            _ => false,
540                        },
541                        // WPA3
542                        BssProtection::Wpa3Personal
543                        | BssProtection::Wpa2Wpa3Personal => match self.status_code {
544                            fidl_ieee80211::StatusCode::RejectedSequenceTimeout => true,
545                            _ => false,
546                        },
547                        _ => false,
548                    },
549                    // If selected_bss is unavailable, there's a bigger problem with the roam
550                    // attempt than just a rejected credential.
551                    None => false,
552                }
553            }
554            _ => false,
555        }
556    }
557}
558
559#[derive(Debug, PartialEq)]
560pub enum SelectNetworkFailure {
561    NoScanResultWithSsid,
562    IncompatibleConnectRequest,
563    InternalProtectionError,
564}
565
566impl From<SelectNetworkFailure> for ConnectFailure {
567    fn from(failure: SelectNetworkFailure) -> Self {
568        ConnectFailure::SelectNetworkFailure(failure)
569    }
570}
571
572#[derive(Debug, PartialEq)]
573pub struct AssociationFailure {
574    pub bss_protection: BssProtection,
575    pub code: fidl_ieee80211::StatusCode,
576}
577
578impl From<AssociationFailure> for ConnectFailure {
579    fn from(failure: AssociationFailure) -> Self {
580        ConnectFailure::AssociationFailure(failure)
581    }
582}
583
584#[derive(Debug, PartialEq)]
585pub struct EstablishRsnaFailure {
586    pub auth_method: Option<auth::MethodName>,
587    pub reason: EstablishRsnaFailureReason,
588}
589
590#[derive(Debug, PartialEq)]
591pub enum EstablishRsnaFailureReason {
592    StartSupplicantFailed,
593    RsnaResponseTimeout(wlan_rsn::Error),
594    RsnaCompletionTimeout(wlan_rsn::Error),
595    InternalError,
596}
597
598impl From<EstablishRsnaFailure> for ConnectFailure {
599    fn from(failure: EstablishRsnaFailure) -> Self {
600        ConnectFailure::EstablishRsnaFailure(failure)
601    }
602}
603
604// Almost mirrors fidl_sme::ServingApInfo except that ServingApInfo
605// contains more info here than it does in fidl_sme.
606#[derive(Clone, Debug, PartialEq)]
607pub struct ServingApInfo {
608    pub bssid: Bssid,
609    pub ssid: Ssid,
610    pub rssi_dbm: i8,
611    pub snr_db: i8,
612    pub signal_report_time: zx::MonotonicInstant,
613    pub channel: wlan_common::channel::Channel,
614    pub protection: BssProtection,
615    pub ht_cap: Option<fidl_ieee80211::HtCapabilities>,
616    pub vht_cap: Option<fidl_ieee80211::VhtCapabilities>,
617    pub probe_resp_wsc: Option<wsc::ProbeRespWsc>,
618    pub wmm_param: Option<ie::WmmParam>,
619}
620
621impl From<ServingApInfo> for fidl_sme::ServingApInfo {
622    fn from(ap: ServingApInfo) -> fidl_sme::ServingApInfo {
623        fidl_sme::ServingApInfo {
624            bssid: ap.bssid.to_array(),
625            ssid: ap.ssid.to_vec(),
626            rssi_dbm: ap.rssi_dbm,
627            snr_db: ap.snr_db,
628            channel: ap.channel.into(),
629            protection: ap.protection.into(),
630        }
631    }
632}
633
634// TODO(https://fxbug.dev/324167674): fix.
635#[derive(Clone, Debug, PartialEq)]
636pub enum ClientSmeStatus {
637    Connected(Box<ServingApInfo>),
638    Connecting(Ssid),
639    Roaming(Bssid),
640    Idle,
641}
642
643impl ClientSmeStatus {
644    pub fn is_connecting(&self) -> bool {
645        matches!(self, ClientSmeStatus::Connecting(_))
646    }
647
648    pub fn is_connected(&self) -> bool {
649        matches!(self, ClientSmeStatus::Connected(_))
650    }
651}
652
653impl From<ClientSmeStatus> for fidl_sme::ClientStatusResponse {
654    fn from(client_sme_status: ClientSmeStatus) -> fidl_sme::ClientStatusResponse {
655        match client_sme_status {
656            ClientSmeStatus::Connected(serving_ap_info) => {
657                fidl_sme::ClientStatusResponse::Connected((*serving_ap_info).into())
658            }
659            ClientSmeStatus::Connecting(ssid) => {
660                fidl_sme::ClientStatusResponse::Connecting(ssid.to_vec())
661            }
662            ClientSmeStatus::Roaming(bssid) => {
663                fidl_sme::ClientStatusResponse::Roaming(bssid.to_array())
664            }
665            ClientSmeStatus::Idle => fidl_sme::ClientStatusResponse::Idle(fidl_sme::Empty {}),
666        }
667    }
668}
669
670impl ClientSme {
671    #[allow(clippy::too_many_arguments, reason = "mass allow for https://fxbug.dev/381896734")]
672    pub fn new(
673        cfg: ClientConfig,
674        info: fidl_mlme::DeviceInfo,
675        inspector: fuchsia_inspect::Inspector,
676        inspect_node: fuchsia_inspect::Node,
677        security_support: fidl_common::SecuritySupport,
678        spectrum_management_support: fidl_common::SpectrumManagementSupport,
679    ) -> (Self, MlmeSink, MlmeStream, timer::EventStream<Event>) {
680        let device_info = Arc::new(info);
681        let (mlme_sink, mlme_stream) = mpsc::unbounded();
682        let (mut timer, time_stream) = timer::create_timer();
683        let inspect = Arc::new(inspect::SmeTree::new(
684            inspector,
685            inspect_node,
686            &device_info,
687            &spectrum_management_support,
688        ));
689        let _ = timer.schedule(event::InspectPulseCheck);
690
691        (
692            ClientSme {
693                cfg,
694                state: Some(ClientState::new(cfg)),
695                scan_sched: <ScanScheduler<
696                    Responder<Result<Vec<ScanResult>, fidl_mlme::ScanResultCode>>,
697                >>::new(
698                    Arc::clone(&device_info), spectrum_management_support
699                ),
700                wmm_status_responders: vec![],
701                context: Context {
702                    mlme_sink: MlmeSink::new(mlme_sink.clone()),
703                    device_info,
704                    timer,
705                    att_id: 0,
706                    inspect,
707                    security_support,
708                },
709            },
710            MlmeSink::new(mlme_sink),
711            mlme_stream,
712            time_stream,
713        )
714    }
715
716    pub fn on_connect_command(
717        &mut self,
718        req: fidl_sme::ConnectRequest,
719    ) -> ConnectTransactionStream {
720        let (mut connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
721
722        // Cancel any ongoing connect attempt
723        self.state = self.state.take().map(|state| state.cancel_ongoing_connect(&mut self.context));
724
725        let bss_description: BssDescription = match req.bss_description.try_into() {
726            Ok(bss_description) => bss_description,
727            Err(e) => {
728                error!("Failed converting FIDL BssDescription in ConnectRequest: {:?}", e);
729                connect_txn_sink
730                    .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
731                return connect_txn_stream;
732            }
733        };
734
735        info!("Received ConnectRequest for {}", bss_description);
736
737        if self
738            .cfg
739            .bss_compatibility(
740                &bss_description,
741                &self.context.device_info,
742                &self.context.security_support,
743            )
744            .is_err()
745        {
746            warn!("BSS is incompatible");
747            connect_txn_sink
748                .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
749            return connect_txn_stream;
750        }
751
752        let authentication = req.authentication.clone();
753        let protection = match SecurityAuthenticator::try_from(req.authentication)
754            .map_err(From::from)
755            .and_then(|authenticator| {
756                Protection::try_from(SecurityContext {
757                    security: &authenticator,
758                    device: &self.context.device_info,
759                    security_support: &self.context.security_support,
760                    config: &self.cfg,
761                    bss: &bss_description,
762                })
763            }) {
764            Ok(protection) => protection,
765            Err(error) => {
766                warn!(
767                    "{:?}",
768                    format!(
769                        "Failed to configure protection for network {} ({}): {:?}",
770                        bss_description.ssid, bss_description.bssid, error
771                    )
772                );
773                connect_txn_sink
774                    .send_connect_result(SelectNetworkFailure::IncompatibleConnectRequest.into());
775                return connect_txn_stream;
776            }
777        };
778        let cmd = ConnectCommand {
779            bss: Box::new(bss_description),
780            connect_txn_sink,
781            protection,
782            authentication,
783        };
784
785        self.state = self.state.take().map(|state| state.connect(cmd, &mut self.context));
786        connect_txn_stream
787    }
788
789    pub fn on_roam_command(&mut self, req: fidl_sme::RoamRequest) {
790        if !self.status().is_connected() {
791            error!("SME ignoring roam request because client is not connected");
792        } else {
793            self.state =
794                self.state.take().map(|state| state.roam(&mut self.context, req.bss_description));
795        }
796    }
797
798    pub fn on_disconnect_command(
799        &mut self,
800        policy_disconnect_reason: fidl_sme::UserDisconnectReason,
801        responder: fidl_sme::ClientSmeDisconnectResponder,
802    ) {
803        self.state = self
804            .state
805            .take()
806            .map(|state| state.disconnect(&mut self.context, policy_disconnect_reason, responder));
807        self.context.inspect.update_pulse(self.status());
808    }
809
810    pub fn on_scan_command(
811        &mut self,
812        scan_request: fidl_sme::ScanRequest,
813    ) -> oneshot::Receiver<Result<Vec<wlan_common::scan::ScanResult>, fidl_mlme::ScanResultCode>>
814    {
815        let (responder, receiver) = Responder::new();
816        if self.status().is_connecting() {
817            info!("SME ignoring scan request because a connect is in progress");
818            responder.respond(Err(fidl_mlme::ScanResultCode::ShouldWait));
819        } else {
820            info!(
821                "SME received a scan command, initiating a{} discovery scan",
822                match scan_request {
823                    fidl_sme::ScanRequest::Active(_) => "n active",
824                    fidl_sme::ScanRequest::Passive(_) => " passive",
825                }
826            );
827            let scan = DiscoveryScan::new(responder, scan_request);
828            let req = self.scan_sched.enqueue_scan_to_discover(scan);
829            self.send_scan_request(req);
830        }
831        receiver
832    }
833
834    pub fn on_clone_inspect_vmo(&self) -> Option<fidl::Vmo> {
835        self.context.inspect.clone_vmo_data()
836    }
837
838    pub fn status(&self) -> ClientSmeStatus {
839        // `self.state` is always set to another state on transition and thus always present
840        #[expect(clippy::expect_used)]
841        self.state.as_ref().expect("expected state to be always present").status()
842    }
843
844    pub fn wmm_status(&mut self) -> oneshot::Receiver<fidl_sme::ClientSmeWmmStatusResult> {
845        let (responder, receiver) = Responder::new();
846        self.wmm_status_responders.push(responder);
847        self.context.mlme_sink.send(MlmeRequest::WmmStatusReq);
848        receiver
849    }
850
851    fn send_scan_request(&mut self, req: Option<fidl_mlme::ScanRequest>) {
852        if let Some(req) = req {
853            self.context.mlme_sink.send(MlmeRequest::Scan(req));
854        }
855    }
856
857    pub fn query_telemetry_support(
858        &mut self,
859    ) -> oneshot::Receiver<Result<fidl_stats::TelemetrySupport, i32>> {
860        let (responder, receiver) = Responder::new();
861        self.context.mlme_sink.send(MlmeRequest::QueryTelemetrySupport(responder));
862        receiver
863    }
864
865    pub fn iface_stats(&mut self) -> oneshot::Receiver<fidl_mlme::GetIfaceStatsResponse> {
866        let (responder, receiver) = Responder::new();
867        self.context.mlme_sink.send(MlmeRequest::GetIfaceStats(responder));
868        receiver
869    }
870
871    pub fn histogram_stats(
872        &mut self,
873    ) -> oneshot::Receiver<fidl_mlme::GetIfaceHistogramStatsResponse> {
874        let (responder, receiver) = Responder::new();
875        self.context.mlme_sink.send(MlmeRequest::GetIfaceHistogramStats(responder));
876        receiver
877    }
878
879    pub fn signal_report(&mut self) -> oneshot::Receiver<Result<fidl_stats::SignalReport, i32>> {
880        let (responder, receiver) = Responder::new();
881        self.context.mlme_sink.send(MlmeRequest::GetSignalReport(responder));
882        receiver
883    }
884
885    pub fn set_mac_address(&mut self, mac_addr: [u8; 6]) -> oneshot::Receiver<Result<(), i32>> {
886        let (responder, receiver) = Responder::new();
887        self.context.mlme_sink.send(MlmeRequest::SetMacAddress(mac_addr, responder));
888        receiver
889    }
890
891    pub fn query_apf_packet_filter_support(
892        &mut self,
893    ) -> oneshot::Receiver<Result<fidl_common::ApfPacketFilterSupport, i32>> {
894        let (responder, receiver) = Responder::new();
895        self.context.mlme_sink.send(MlmeRequest::QueryApfPacketFilterSupport(responder));
896        receiver
897    }
898
899    pub fn install_apf_packet_filter(
900        &mut self,
901        program: Vec<u8>,
902    ) -> oneshot::Receiver<Result<(), i32>> {
903        let (responder, receiver) = Responder::new();
904        self.context.mlme_sink.send(MlmeRequest::InstallApfPacketFilter(
905            fidl_mlme::MlmeInstallApfPacketFilterRequest { program },
906            responder,
907        ));
908        receiver
909    }
910
911    pub fn read_apf_packet_filter_data(
912        &mut self,
913    ) -> oneshot::Receiver<Result<fidl_mlme::MlmeReadApfPacketFilterDataResponse, i32>> {
914        let (responder, receiver) = Responder::new();
915        self.context.mlme_sink.send(MlmeRequest::ReadApfPacketFilterData(responder));
916        receiver
917    }
918
919    pub fn set_apf_packet_filter_enabled(
920        &mut self,
921        enabled: bool,
922    ) -> oneshot::Receiver<Result<(), i32>> {
923        let (responder, receiver) = Responder::new();
924        self.context.mlme_sink.send(MlmeRequest::SetApfPacketFilterEnabled(
925            fidl_mlme::MlmeSetApfPacketFilterEnabledRequest { enabled },
926            responder,
927        ));
928        receiver
929    }
930
931    pub fn get_apf_packet_filter_enabled(
932        &mut self,
933    ) -> oneshot::Receiver<Result<fidl_mlme::MlmeGetApfPacketFilterEnabledResponse, i32>> {
934        let (responder, receiver) = Responder::new();
935        self.context.mlme_sink.send(MlmeRequest::GetApfPacketFilterEnabled(responder));
936        receiver
937    }
938}
939
940impl super::Station for ClientSme {
941    type Event = Event;
942
943    fn on_mlme_event(&mut self, event: fidl_mlme::MlmeEvent) {
944        match event {
945            fidl_mlme::MlmeEvent::OnScanResult { result } => self
946                .scan_sched
947                .on_mlme_scan_result(result)
948                .unwrap_or_else(|e| error!("scan result error: {:?}", e)),
949            fidl_mlme::MlmeEvent::OnScanEnd { end } => {
950                match self.scan_sched.on_mlme_scan_end(end, &self.context.inspect) {
951                    Err(e) => error!("scan end error: {:?}", e),
952                    Ok((scan_end, next_request)) => {
953                        // Finalize stats for previous scan before sending scan request for
954                        // the next one, which start stats collection for new scan.
955                        self.send_scan_request(next_request);
956
957                        match scan_end.result_code {
958                            fidl_mlme::ScanResultCode::Success => {
959                                let scan_result_list: Vec<ScanResult> = scan_end
960                                    .bss_description_list
961                                    .into_iter()
962                                    .map(|bss_description| {
963                                        self.cfg.create_scan_result(
964                                            // TODO(https://fxbug.dev/42164608): ScanEnd drops the timestamp from MLME
965                                            zx::MonotonicInstant::from_nanos(0),
966                                            bss_description,
967                                            &self.context.device_info,
968                                            &self.context.security_support,
969                                        )
970                                    })
971                                    .collect();
972                                for responder in scan_end.tokens {
973                                    responder.respond(Ok(scan_result_list.clone()));
974                                }
975                            }
976                            result_code => {
977                                let count = scan_end.bss_description_list.len();
978                                if count > 0 {
979                                    warn!("Incomplete scan with {} pending results.", count);
980                                }
981                                for responder in scan_end.tokens {
982                                    responder.respond(Err(result_code));
983                                }
984                            }
985                        }
986                    }
987                }
988            }
989            fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp } => {
990                for responder in self.wmm_status_responders.drain(..) {
991                    let result = if status == zx::sys::ZX_OK { Ok(resp) } else { Err(status) };
992                    responder.respond(result);
993                }
994                let event = fidl_mlme::MlmeEvent::OnWmmStatusResp { status, resp };
995                self.state =
996                    self.state.take().map(|state| state.on_mlme_event(event, &mut self.context));
997            }
998            other => {
999                self.state =
1000                    self.state.take().map(|state| state.on_mlme_event(other, &mut self.context));
1001            }
1002        };
1003
1004        self.context.inspect.update_pulse(self.status());
1005    }
1006
1007    fn on_timeout(&mut self, timed_event: timer::Event<Event>) {
1008        self.state = self.state.take().map(|state| match timed_event.event {
1009            event @ Event::RsnaCompletionTimeout(..)
1010            | event @ Event::RsnaResponseTimeout(..)
1011            | event @ Event::RsnaRetransmissionTimeout(..)
1012            | event @ Event::SaeTimeout(..)
1013            | event @ Event::DeauthenticateTimeout(..) => {
1014                state.handle_timeout(event, &mut self.context)
1015            }
1016            Event::InspectPulseCheck(..) => {
1017                self.context.mlme_sink.send(MlmeRequest::WmmStatusReq);
1018                let _ = self.context.timer.schedule(event::InspectPulseCheck);
1019                state
1020            }
1021        });
1022
1023        // Because `self.status()` relies on the value of `self.state` to be present, we cannot
1024        // retrieve it and update pulse node inside the closure above.
1025        self.context.inspect.update_pulse(self.status());
1026    }
1027}
1028
1029fn report_connect_finished(connect_txn_sink: &mut ConnectTransactionSink, result: ConnectResult) {
1030    connect_txn_sink.send_connect_result(result);
1031}
1032
1033fn report_roam_finished(connect_txn_sink: &mut ConnectTransactionSink, result: RoamResult) {
1034    connect_txn_sink.send_roam_result(result);
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039    use super::*;
1040    use crate::Config as SmeConfig;
1041    use assert_matches::assert_matches;
1042    use fidl_fuchsia_wlan_common as fidl_common;
1043    use fidl_fuchsia_wlan_mlme as fidl_mlme;
1044    use fuchsia_inspect as finspect;
1045    use ieee80211::MacAddr;
1046    use std::collections::HashSet;
1047    use std::sync::LazyLock;
1048    use test_case::test_case;
1049    use wlan_common::{
1050        channel::{Cbw, Channel},
1051        fake_bss_description, fake_fidl_bss_description,
1052        ie::{/*rsn::akm,*/ IeType, fake_ht_cap_bytes, fake_vht_cap_bytes},
1053        security::{wep::WEP40_KEY_BYTES, wpa::credential::PSK_SIZE_BYTES},
1054        test_utils::{
1055            fake_features::{
1056                fake_security_support, fake_security_support_empty,
1057                fake_spectrum_management_support_empty,
1058            },
1059            fake_stas::{FakeProtectionCfg, IesOverrides},
1060        },
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_internal::Authentication {
1071        fidl_internal::Authentication { protocol: fidl_internal::Protocol::Open, credentials: None }
1072    }
1073
1074    fn authentication_wep40() -> fidl_internal::Authentication {
1075        fidl_internal::Authentication {
1076            protocol: fidl_internal::Protocol::Wep,
1077            credentials: Some(Box::new(fidl_internal::Credentials::Wep(
1078                fidl_internal::WepCredentials { key: [1; WEP40_KEY_BYTES].into() },
1079            ))),
1080        }
1081    }
1082
1083    fn authentication_wpa1_passphrase() -> fidl_internal::Authentication {
1084        fidl_internal::Authentication {
1085            protocol: fidl_internal::Protocol::Wpa1,
1086            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1087                fidl_internal::WpaCredentials::Passphrase(b"password".as_slice().into()),
1088            ))),
1089        }
1090    }
1091
1092    fn authentication_wpa2_personal_psk() -> fidl_internal::Authentication {
1093        fidl_internal::Authentication {
1094            protocol: fidl_internal::Protocol::Wpa2Personal,
1095            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1096                fidl_internal::WpaCredentials::Psk([1; PSK_SIZE_BYTES]),
1097            ))),
1098        }
1099    }
1100
1101    fn authentication_wpa2_personal_passphrase() -> fidl_internal::Authentication {
1102        fidl_internal::Authentication {
1103            protocol: fidl_internal::Protocol::Wpa2Personal,
1104            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1105                fidl_internal::WpaCredentials::Passphrase(b"password".as_slice().into()),
1106            ))),
1107        }
1108    }
1109
1110    fn authentication_wpa3_personal_passphrase() -> fidl_internal::Authentication {
1111        fidl_internal::Authentication {
1112            protocol: fidl_internal::Protocol::Wpa3Personal,
1113            credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
1114                fidl_internal::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_ieee80211::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_internal::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_ieee80211::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_internal::Authentication {
2016                protocol: fidl_internal::Protocol::Wpa2Personal,
2017                credentials: Some(Box::new(fidl_internal::Credentials::Wpa(
2018                    fidl_internal::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                channels: vec![],
2097            }));
2098
2099        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2100            end: fidl_mlme::ScanEnd {
2101                txn_id: 1,
2102                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2103            },
2104        });
2105
2106        assert_eq!(
2107            recv.try_recv(),
2108            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2109        );
2110    }
2111
2112    #[fuchsia::test(allow_stalls = false)]
2113    async fn test_scan_error_after_some_results_returned() {
2114        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2115        let mut recv =
2116            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
2117                channels: vec![],
2118            }));
2119
2120        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2121        bss.bssid = [3; 6];
2122        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2123            result: fidl_mlme::ScanResult {
2124                txn_id: 1,
2125                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2126                bss,
2127            },
2128        });
2129        let mut bss = fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2130        bss.bssid = [4; 6];
2131        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanResult {
2132            result: fidl_mlme::ScanResult {
2133                txn_id: 1,
2134                timestamp_nanos: zx::MonotonicInstant::get().into_nanos(),
2135                bss,
2136            },
2137        });
2138
2139        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnScanEnd {
2140            end: fidl_mlme::ScanEnd {
2141                txn_id: 1,
2142                code: fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware,
2143            },
2144        });
2145
2146        // Scan results are lost when an error occurs.
2147        assert_eq!(
2148            recv.try_recv(),
2149            Ok(Some(Err(fidl_mlme::ScanResultCode::CanceledByDriverOrFirmware)))
2150        );
2151    }
2152
2153    #[fuchsia::test(allow_stalls = false)]
2154    async fn test_scan_is_rejected_while_connecting() {
2155        let (mut sme, _mlme_strem, _time_stream) = create_sme().await;
2156
2157        // Send a connect command to move SME into Connecting state
2158        let bss_description =
2159            fake_fidl_bss_description!(Open, ssid: Ssid::try_from("foo").unwrap());
2160        let _recv = sme.on_connect_command(connect_req(
2161            Ssid::try_from("foo").unwrap(),
2162            bss_description,
2163            authentication_open(),
2164        ));
2165        assert_matches!(sme.status(), ClientSmeStatus::Connecting(_));
2166
2167        // Send a scan command and verify a ShouldWait response is returned
2168        let mut recv =
2169            sme.on_scan_command(fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {
2170                channels: vec![],
2171            }));
2172        assert_eq!(recv.try_recv(), Ok(Some(Err(fidl_mlme::ScanResultCode::ShouldWait))));
2173    }
2174
2175    #[fuchsia::test(allow_stalls = false)]
2176    async fn test_wmm_status_success() {
2177        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2178        let mut receiver = sme.wmm_status();
2179
2180        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2181
2182        let resp = fake_wmm_status_resp();
2183        #[allow(
2184            clippy::redundant_field_names,
2185            reason = "mass allow for https://fxbug.dev/381896734"
2186        )]
2187        sme.on_mlme_event(fidl_mlme::MlmeEvent::OnWmmStatusResp {
2188            status: zx::sys::ZX_OK,
2189            resp: resp,
2190        });
2191
2192        assert_eq!(receiver.try_recv(), Ok(Some(Ok(resp))));
2193    }
2194
2195    #[fuchsia::test(allow_stalls = false)]
2196    async fn test_wmm_status_failed() {
2197        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2198        let mut receiver = sme.wmm_status();
2199
2200        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::WmmStatusReq)));
2201        sme.on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_ERR_IO));
2202        assert_eq!(receiver.try_recv(), Ok(Some(Err(zx::sys::ZX_ERR_IO))));
2203    }
2204
2205    #[fuchsia::test(allow_stalls = false)]
2206    async fn test_query_apf_packet_filter_support() {
2207        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2208        let mut _receiver = sme.query_apf_packet_filter_support();
2209        assert_matches!(
2210            mlme_stream.try_next(),
2211            Ok(Some(MlmeRequest::QueryApfPacketFilterSupport(..)))
2212        );
2213    }
2214
2215    #[fuchsia::test(allow_stalls = false)]
2216    async fn test_install_apf_packet_filter() {
2217        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2218        let program = vec![1, 2, 3];
2219        let mut _receiver = sme.install_apf_packet_filter(program.clone());
2220        let req = assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::InstallApfPacketFilter(req, ..))) => req);
2221        assert_eq!(req.program, program);
2222    }
2223
2224    #[fuchsia::test(allow_stalls = false)]
2225    async fn test_read_apf_packet_filter_data() {
2226        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2227        let mut _receiver = sme.read_apf_packet_filter_data();
2228        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::ReadApfPacketFilterData(..))));
2229    }
2230
2231    #[fuchsia::test(allow_stalls = false)]
2232    async fn test_set_apf_packet_filter_enabled() {
2233        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2234        let mut _receiver = sme.set_apf_packet_filter_enabled(true);
2235        let req = assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetApfPacketFilterEnabled(req, ..))) => req);
2236        assert!(req.enabled);
2237    }
2238
2239    #[fuchsia::test(allow_stalls = false)]
2240    async fn test_get_apf_packet_filter_enabled() {
2241        let (mut sme, mut mlme_stream, _time_stream) = create_sme().await;
2242        let mut _receiver = sme.get_apf_packet_filter_enabled();
2243        assert_matches!(
2244            mlme_stream.try_next(),
2245            Ok(Some(MlmeRequest::GetApfPacketFilterEnabled(..)))
2246        );
2247    }
2248
2249    fn assert_no_connect(mlme_stream: &mut mpsc::UnboundedReceiver<MlmeRequest>) {
2250        loop {
2251            match mlme_stream.try_next() {
2252                Ok(event) => match event {
2253                    Some(MlmeRequest::Connect(..)) => {
2254                        panic!("unexpected connect request sent to MLME")
2255                    }
2256                    None => break,
2257                    _ => (),
2258                },
2259                Err(e) => {
2260                    assert_eq!(e.to_string(), "receiver channel is empty");
2261                    break;
2262                }
2263            }
2264        }
2265    }
2266
2267    fn connect_req(
2268        ssid: Ssid,
2269        bss_description: fidl_ieee80211::BssDescription,
2270        authentication: fidl_internal::Authentication,
2271    ) -> fidl_sme::ConnectRequest {
2272        fidl_sme::ConnectRequest {
2273            ssid: ssid.to_vec(),
2274            bss_description,
2275            multiple_bss_candidates: true,
2276            authentication,
2277            deprecated_scan_type: fidl_common::ScanType::Passive,
2278        }
2279    }
2280
2281    // The unused _exec parameter ensures that an executor exists for the lifetime of the SME.
2282    // Our internal timer implementation relies on the existence of a local executor.
2283    //
2284    // TODO(https://fxbug.dev/327499461): This function is async to ensure SME functions will
2285    // run in an async context and not call `wlan_common::timer::Timer::now` without an
2286    // executor.
2287    async fn create_sme() -> (ClientSme, MlmeStream, timer::EventStream<Event>) {
2288        let inspector = finspect::Inspector::default();
2289        let sme_root_node = inspector.root().create_child("sme");
2290        let (client_sme, _mlme_sink, mlme_stream, time_stream) = ClientSme::new(
2291            ClientConfig::default(),
2292            test_utils::fake_device_info(*CLIENT_ADDR),
2293            inspector,
2294            sme_root_node,
2295            fake_security_support(),
2296            fake_spectrum_management_support_empty(),
2297        );
2298        (client_sme, mlme_stream, time_stream)
2299    }
2300}