wlan_sme/client/
mod.rs

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