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