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