wlan_mlme/client/
state.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
5//! A state machine for associating a Client to a BSS.
6//! Note: This implementation only supports simultaneous authentication with exactly one STA, the
7//! AP. While 802.11 explicitly allows - and sometime requires - authentication with more than one
8//! STA, Fuchsia does intentionally not yet support this use-case.
9
10use crate::akm_algorithm as akm;
11use crate::block_ack::{BlockAckState, Closed};
12use crate::client::lost_bss::LostBssCounter;
13use crate::client::{BoundClient, ParsedAssociateResp, TimedEvent};
14use crate::ddk_converter::{get_rssi_dbm, softmac_key_configuration_from_mlme};
15use crate::device::DeviceOps;
16use crate::disconnect::LocallyInitiated;
17use crate::error::Error;
18use fuchsia_trace::Id as TraceId;
19use ieee80211::{Bssid, MacAddr, MacAddrBytes};
20use log::{debug, error, info, trace, warn};
21use wlan_common::buffer_reader::BufferReader;
22use wlan_common::capabilities::{ApCapabilities, StaCapabilities, intersect_with_ap_as_client};
23use wlan_common::energy::DecibelMilliWatt;
24use wlan_common::mac::{self, BeaconHdr};
25use wlan_common::stats::SignalStrengthAverage;
26use wlan_common::timer::{EventHandle, Timer};
27use wlan_common::{ie, tim};
28use wlan_statemachine::*;
29use zerocopy::SplitByteSlice;
30use {
31    fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_internal as fidl_internal,
32    fidl_fuchsia_wlan_mlme as fidl_mlme, fidl_fuchsia_wlan_softmac as fidl_softmac,
33    wlan_trace as wtrace,
34};
35
36/// Reconnect timeout in Beacon periods.
37/// If no association response was received from the BSS within this time window, an association is
38/// considered to have failed.
39const RECONNECT_TIMEOUT_BCN_PERIODS: u16 = 10;
40
41/// Number of beacon intervals which beacon is not seen before we declare BSS as lost
42pub const DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT: u32 = 100;
43
44/// Number of beacon intervals between association status check (signal report or auto-deatuh).
45pub const ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT: u32 = 10;
46
47/// Client joined a BSS (synchronized timers and prepared its underlying hardware).
48/// At this point the Client is able to listen to frames on the BSS' channel.
49#[derive(Debug)]
50pub struct Joined;
51
52impl Joined {
53    /// Initiates an open authentication with the currently joined BSS.
54    /// The returned state is unchanged in an error case. Otherwise, the state transitions into
55    /// "Authenticating".
56    /// Returns Ok(AkmAlgorithm) if authentication request was sent successfully, Err(()) otherwise.
57    async fn start_authenticating<D: DeviceOps>(
58        &self,
59        sta: &mut BoundClient<'_, D>,
60    ) -> Result<akm::AkmAlgorithm, ()> {
61        let auth_type = sta.sta.connect_req.auth_type;
62        let algorithm_candidate = match auth_type {
63            fidl_mlme::AuthenticationTypes::OpenSystem => Ok(akm::AkmAlgorithm::OpenSupplicant),
64            fidl_mlme::AuthenticationTypes::Sae => Ok(akm::AkmAlgorithm::Sae),
65            _ => {
66                error!("Unhandled authentication algorithm: {:?}", auth_type);
67                Err(fidl_ieee80211::StatusCode::UnsupportedAuthAlgorithm)
68            }
69        };
70
71        let result = match algorithm_candidate {
72            Err(e) => Err(e),
73            Ok(mut algorithm) => match algorithm.initiate(sta) {
74                Ok(akm::AkmState::Failed) => {
75                    Err(fidl_ieee80211::StatusCode::UnsupportedAuthAlgorithm)
76                }
77                Err(_) => Err(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
78                Ok(_) => Ok(algorithm),
79            },
80        };
81
82        match result {
83            Ok(algorithm) => Ok(algorithm),
84            Err(status_code) => {
85                sta.send_connect_conf_failure(status_code);
86                let _ = sta
87                    .clear_association()
88                    .await
89                    .map_err(|e| error!("Failed to clear association: {}", e));
90                Err(())
91            }
92        }
93    }
94
95    async fn on_sme_deauthenticate<D: DeviceOps>(&mut self, sta: &mut BoundClient<'_, D>) {
96        let _ =
97            sta.clear_association().await.map_err(|e| warn!("Failed to clear association: {}", e));
98    }
99}
100
101#[derive(Debug)]
102enum AuthProgress {
103    Complete,
104    InProgress,
105    Failed,
106}
107
108/// Client issued an authentication request frame to its joined BSS prior to joining this state.
109/// At this point the client is waiting for an authentication response frame from the client.
110/// Note: This assumes Open System authentication.
111#[derive(Debug)]
112pub struct Authenticating {
113    algorithm: akm::AkmAlgorithm,
114}
115
116impl Authenticating {
117    fn new(algorithm: akm::AkmAlgorithm) -> Self {
118        Self { algorithm }
119    }
120
121    async fn akm_state_update_notify_sme<D: DeviceOps>(
122        &self,
123        sta: &mut BoundClient<'_, D>,
124        state: Result<akm::AkmState, anyhow::Error>,
125    ) -> AuthProgress {
126        match state {
127            Ok(akm::AkmState::AuthComplete) => match self.start_associating(sta) {
128                Ok(()) => AuthProgress::Complete,
129                Err(()) => AuthProgress::Failed,
130            },
131            Ok(akm::AkmState::InProgress) => AuthProgress::InProgress,
132            Ok(akm::AkmState::Failed) => {
133                error!("authentication with BSS failed");
134                // TODO(https://fxbug.dev/42164548): pass the status code from the original auth frame
135                sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedReasonUnspecified);
136                let _ = sta
137                    .clear_association()
138                    .await
139                    .map_err(|e| error!("Failed to clear association: {}", e));
140                AuthProgress::Failed
141            }
142            Err(e) => {
143                error!("Internal error while authenticating: {}", e);
144                // TODO(https://fxbug.dev/42164548): pass the status code from the original auth frame
145                sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedReasonUnspecified);
146                let _ = sta
147                    .clear_association()
148                    .await
149                    .map_err(|e| error!("Failed to clear association: {}", e));
150                AuthProgress::Failed
151            }
152        }
153    }
154
155    /// Initiates an association with the currently joined BSS.
156    /// Returns Ok(()) if association request was sent successfully.
157    /// Otherwise an Err(()) is returned and a CONNECT.confirm message to its SME peer.
158    fn start_associating<D: DeviceOps>(&self, sta: &mut BoundClient<'_, D>) -> Result<(), ()> {
159        sta.send_assoc_req_frame().map_err(|e| {
160            error!("Error sending association request frame: {}", e);
161            sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedTemporarily);
162        })
163    }
164
165    /// Processes an inbound authentication frame.
166    /// SME will be notified via a CONNECT.confirm message if there's a failure.
167    /// Returns AuthProgress::Complete if the authentication was successful and
168    //  association has started.
169    /// Returns AuthProgress::InProgress if authentication is still ongoing.
170    /// Returns AuthProgress::Failed if failed to authenticate or start association request.
171    async fn on_auth_frame<B: SplitByteSlice, D: DeviceOps>(
172        &mut self,
173        sta: &mut BoundClient<'_, D>,
174        auth_frame: mac::AuthFrame<B>,
175    ) -> AuthProgress {
176        wtrace::duration!("Authenticating::on_auth_frame");
177
178        let state = self.algorithm.handle_auth_frame(sta, auth_frame);
179        self.akm_state_update_notify_sme(sta, state).await
180    }
181
182    /// Processes an SAE response from SME.
183    /// This indicates that an SAE handshake has completed, successful or otherwise.
184    /// On success, authentication is complete and association has started.
185    async fn on_sme_sae_resp<D: DeviceOps>(
186        &mut self,
187        sta: &mut BoundClient<'_, D>,
188        resp: fidl_mlme::SaeHandshakeResponse,
189    ) -> AuthProgress {
190        let state = self.algorithm.handle_sae_resp(sta, resp.status_code);
191        self.akm_state_update_notify_sme(sta, state).await
192    }
193
194    /// Processes a request from SME to transmit an SAE authentication frame to a peer.
195    async fn on_sme_sae_tx<D: DeviceOps>(
196        &mut self,
197        sta: &mut BoundClient<'_, D>,
198        tx: fidl_mlme::SaeFrame,
199    ) -> AuthProgress {
200        let state =
201            self.algorithm.handle_sme_sae_tx(sta, tx.seq_num, tx.status_code, &tx.sae_fields[..]);
202        self.akm_state_update_notify_sme(sta, state).await
203    }
204
205    /// Processes an inbound deauthentication frame.
206    /// This always results in an MLME-AUTHENTICATE.confirm message to MLME's SME peer.
207    /// The pending authentication timeout will be canceled in this process.
208    async fn on_deauth_frame<D: DeviceOps>(
209        &mut self,
210        sta: &mut BoundClient<'_, D>,
211        deauth_hdr: &mac::DeauthHdr,
212    ) {
213        wtrace::duration!("Authenticating::on_deauth_frame");
214
215        info!(
216            "received spurious deauthentication frame while authenticating with BSS (unusual); \
217             authentication failed: {:?}",
218            { deauth_hdr.reason_code }
219        );
220
221        sta.sta.connect_timeout.take();
222        sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc);
223        let _ =
224            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
225    }
226
227    async fn on_sme_deauthenticate<D: DeviceOps>(&mut self, sta: &mut BoundClient<'_, D>) {
228        sta.sta.connect_timeout.take();
229        let _ =
230            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
231    }
232}
233
234#[derive(Debug)]
235pub struct Authenticated;
236
237impl Authenticated {
238    fn on_sme_reconnect<D: DeviceOps>(
239        &self,
240        sta: &mut BoundClient<'_, D>,
241        req: fidl_mlme::ReconnectRequest,
242    ) -> Result<EventHandle, ()> {
243        let peer_sta_address: Bssid = req.peer_sta_address.into();
244        if peer_sta_address == sta.sta.connect_req.selected_bss.bssid {
245            match sta.send_assoc_req_frame() {
246                Ok(()) => {
247                    // Setting timeout in term of beacon period allows us to adjust the realtime
248                    // timeout in hw-sim, where we set a longer duration in case of a slowbot.
249                    let duration = sta.sta.beacon_period() * RECONNECT_TIMEOUT_BCN_PERIODS;
250                    Ok(sta.ctx.timer.schedule_after(duration, TimedEvent::Reassociating))
251                }
252                Err(e) => {
253                    error!("Error sending association request frame: {}", e);
254                    sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedTemporarily);
255                    Err(())
256                }
257            }
258        } else {
259            info!("received reconnect request for a different BSSID, ignoring");
260            sta.send_connect_conf_failure_with_bssid(
261                peer_sta_address,
262                fidl_ieee80211::StatusCode::NotInSameBss,
263            );
264            Err(())
265        }
266    }
267}
268
269/// Client has sent an association request frame to the AP.
270/// At this point, client is waiting for an association response frame from the AP.
271#[derive(Debug, Default)]
272pub struct Associating {
273    /// This field is only populated when MLME is reconnecting after a disassociation.
274    reconnect_timeout: Option<EventHandle>,
275}
276
277impl Associating {
278    fn new_with_reconnect_timeout(reconnect_timeout: EventHandle) -> Self {
279        Self { reconnect_timeout: Some(reconnect_timeout) }
280    }
281
282    /// Processes an inbound association response frame.
283    /// SME will be notified via an MLME-ASSOCIATE.confirm message whether the association
284    /// with the BSS was successful.
285    /// Returns Ok(()) if the association was successful, otherwise Err(()).
286    /// Note: The pending authentication timeout will be canceled in any case.
287    async fn on_assoc_resp_frame<B: SplitByteSlice, D: DeviceOps>(
288        &mut self,
289        sta: &mut BoundClient<'_, D>,
290        assoc_resp_frame: mac::AssocRespFrame<B>,
291    ) -> Result<Association, ()> {
292        wtrace::duration!("Associating::on_assoc_resp_frame");
293
294        // TODO(https://fxbug.dev/42172907): All reserved values mapped to REFUSED_REASON_UNSPECIFIED.
295        match Option::<fidl_ieee80211::StatusCode>::from(
296            assoc_resp_frame.assoc_resp_hdr.status_code,
297        )
298        .unwrap_or(fidl_ieee80211::StatusCode::RefusedReasonUnspecified)
299        {
300            fidl_ieee80211::StatusCode::Success => (),
301            status_code => {
302                error!("association with BSS failed: {:?}", status_code);
303                sta.send_connect_conf_failure(status_code);
304                return Err(());
305            }
306        }
307
308        let aid = assoc_resp_frame.assoc_resp_hdr.aid;
309        let parsed_assoc_resp = ParsedAssociateResp::parse(&assoc_resp_frame);
310        let ap_capabilities = ApCapabilities(StaCapabilities {
311            capability_info: parsed_assoc_resp.capabilities,
312            rates: parsed_assoc_resp.rates.clone(),
313            ht_cap: parsed_assoc_resp.ht_cap.clone(),
314            vht_cap: parsed_assoc_resp.vht_cap.clone(),
315        });
316        let negotiated_cap =
317            match intersect_with_ap_as_client(&sta.sta.client_capabilities, &ap_capabilities) {
318                Ok(cap) => cap,
319                Err(e) => {
320                    // This is unlikely to happen with any spec-compliant AP. In case the
321                    // user somehow decided to connect to a malicious AP, reject and reset.
322                    // Log at ERROR level to raise visibility of when this event occurs.
323                    error!(
324                        "Associate terminated because AP's capabilities in association \
325                            response is different from beacon. Error: {}",
326                        e
327                    );
328                    sta.send_connect_conf_failure(
329                        fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch,
330                    );
331                    return Err(());
332                }
333            };
334
335        let (ap_ht_op, ap_vht_op) = extract_ht_vht_op(&assoc_resp_frame);
336
337        let main_channel = match sta.channel_state.get_main_channel() {
338            Some(main_channel) => main_channel,
339            None => {
340                error!("MLME in associating state but no main channel is set");
341                sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedReasonUnspecified);
342                return Err(());
343            }
344        };
345
346        // TODO(https://fxbug.dev/42104064): Determine for each outbound data frame,
347        // given the result of the dynamic capability negotiation, data frame
348        // classification, and QoS policy.
349        //
350        // Aruba / Ubiquiti are confirmed to be compatible with QoS field for the
351        // BlockAck session, independently of 40MHz operation.
352        let qos = negotiated_cap.ht_cap.is_some();
353
354        let assoc_cfg = fidl_softmac::WlanAssociationConfig {
355            bssid: Some(sta.sta.bssid().to_array()),
356            aid: Some(aid),
357            // In the association request we sent out earlier, listen_interval is always set to 0,
358            // indicating the client never enters power save mode.
359            listen_interval: Some(0),
360            channel: Some(main_channel),
361            qos: Some(qos),
362            wmm_params: None,
363            rates: Some(negotiated_cap.rates.iter().map(|r| r.0).collect()),
364            capability_info: Some(negotiated_cap.capability_info.raw()),
365            ht_cap: negotiated_cap.ht_cap.map(Into::into),
366            vht_cap: negotiated_cap.vht_cap.map(Into::into),
367            ht_op: ap_ht_op.clone().map(From::from),
368            vht_op: ap_vht_op.clone().map(From::from),
369            ..Default::default()
370        };
371
372        if let Err(status) = sta.ctx.device.notify_association_complete(assoc_cfg).await {
373            // Device cannot handle this association. Something is seriously wrong.
374            error!("device failed to configure association: {}", status);
375            sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RefusedReasonUnspecified);
376            return Err(());
377        }
378
379        let (_, assoc_resp_body) = assoc_resp_frame.into_assoc_resp_body();
380        sta.send_connect_conf_success(aid, assoc_resp_body.deref());
381        let controlled_port_open = !sta.sta.eapol_required();
382        if controlled_port_open {
383            if let Err(e) = sta.ctx.device.set_ethernet_up().await {
384                // TODO(https://fxbug.dev/42175857) - Consider returning an Err here.
385                error!("Cannot set ethernet to UP. Status: {}", e);
386            }
387        }
388        let lost_bss_counter = LostBssCounter::start(
389            sta.sta.beacon_period(),
390            DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT,
391        );
392
393        let status_check_timeout =
394            schedule_association_status_timeout(sta.sta.beacon_period(), &mut sta.ctx.timer);
395
396        Ok(Association {
397            aid,
398            assoc_resp_ies: assoc_resp_body.to_vec(),
399            controlled_port_open,
400            ap_ht_op,
401            ap_vht_op,
402            qos: Qos::from(qos),
403            lost_bss_counter,
404            status_check_timeout,
405            signal_strength_average: SignalStrengthAverage::new(),
406            block_ack_state: StateMachine::new(BlockAckState::from(State::new(Closed))),
407        })
408    }
409
410    /// Processes an inbound disassociation frame.
411    /// Note: APs should never send disassociation frames without having established a valid
412    /// association with the Client. However, to maximize interoperability disassociation frames
413    /// are handled in this state as well and treated similar to unsuccessful association responses.
414    /// This always results in an MLME-ASSOCIATE.confirm message to MLME's SME peer.
415    fn on_disassoc_frame<D: DeviceOps>(
416        &mut self,
417        sta: &mut BoundClient<'_, D>,
418        _disassoc_hdr: &mac::DisassocHdr,
419    ) {
420        wtrace::duration!("Associating::on_disassoc_frame");
421        warn!("received unexpected disassociation frame while associating");
422        sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc);
423    }
424
425    /// Processes an inbound deauthentication frame.
426    /// This always results in an MLME-ASSOCIATE.confirm message to MLME's SME peer.
427    /// The pending association timeout will be canceled in this process.
428    async fn on_deauth_frame<D: DeviceOps>(
429        &mut self,
430        sta: &mut BoundClient<'_, D>,
431        deauth_hdr: &mac::DeauthHdr,
432    ) {
433        wtrace::duration!("Associating::on_deauth_frame");
434        info!(
435            "received spurious deauthentication frame while associating with BSS (unusual); \
436             association failed: {:?}",
437            { deauth_hdr.reason_code }
438        );
439        sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc);
440        let _ =
441            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
442    }
443
444    async fn on_sme_deauthenticate<D: DeviceOps>(&mut self, sta: &mut BoundClient<'_, D>) {
445        sta.sta.connect_timeout.take();
446        let _ =
447            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
448    }
449}
450
451/// Extract HT Operation and VHT Operation IEs from the association response frame.
452/// If either IE is of an incorrect length, it will be ignored.
453fn extract_ht_vht_op<B: SplitByteSlice>(
454    assoc_resp_frame: &mac::AssocRespFrame<B>,
455) -> (Option<ie::HtOperation>, Option<ie::VhtOperation>) {
456    let mut ht_op = None;
457    let mut vht_op = None;
458    for (id, body) in assoc_resp_frame.ies() {
459        match id {
460            ie::Id::HT_OPERATION => match ie::parse_ht_operation(body) {
461                Ok(parsed_ht_op) => ht_op = Some(zerocopy::Ref::read(&parsed_ht_op)),
462                Err(e) => {
463                    error!("Invalid HT Operation: {}", e);
464                    continue;
465                }
466            },
467            ie::Id::VHT_OPERATION => match ie::parse_vht_operation(body) {
468                Ok(parsed_vht_op) => vht_op = Some(zerocopy::Ref::read(&parsed_vht_op)),
469                Err(e) => {
470                    error!("Invalid VHT Operation: {}", e);
471                    continue;
472                }
473            },
474            _ => (),
475        }
476    }
477    (ht_op, vht_op)
478}
479
480pub fn schedule_association_status_timeout(
481    beacon_period: zx::MonotonicDuration,
482    timer: &mut Timer<TimedEvent>,
483) -> StatusCheckTimeout {
484    let duration = beacon_period * ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT;
485    StatusCheckTimeout {
486        next_event: Some(timer.schedule_after(duration, TimedEvent::AssociationStatusCheck)),
487    }
488}
489
490#[derive(Debug, PartialEq)]
491pub enum Qos {
492    Enabled,
493    Disabled,
494}
495
496impl From<bool> for Qos {
497    fn from(b: bool) -> Self {
498        if b { Self::Enabled } else { Self::Disabled }
499    }
500}
501
502impl Qos {
503    fn is_enabled(&self) -> bool {
504        *self == Self::Enabled
505    }
506}
507
508#[derive(Debug)]
509pub struct StatusCheckTimeout {
510    next_event: Option<EventHandle>,
511}
512
513#[derive(Debug)]
514pub struct Association {
515    pub aid: mac::Aid,
516    pub assoc_resp_ies: Vec<u8>,
517
518    /// Represents an 802.1X controlled port.
519    /// A closed controlled port only processes EAP frames while an open one processes any frames.
520    pub controlled_port_open: bool,
521
522    // TODO(https://fxbug.dev/335283785): Remove or explain unused code.
523    #[allow(dead_code)]
524    pub ap_ht_op: Option<ie::HtOperation>,
525    // TODO(https://fxbug.dev/335283785): Remove or explain unused code.
526    #[allow(dead_code)]
527    pub ap_vht_op: Option<ie::VhtOperation>,
528
529    /// Whether to set QoS bit when MLME constructs an outgoing WLAN data frame.
530    /// Currently, QoS is enabled if the associated PHY is HT or VHT.
531    pub qos: Qos,
532
533    /// `lost_bss_counter` is used to determine if the BSS is still alive nearby. It is started
534    /// when the client is associated.
535    pub lost_bss_counter: LostBssCounter,
536
537    /// |timeout| is the timeout that is scheduled for the association status check, which includes
538    /// a) sending signal strength report to SME and b) triggering auto-deauth if necessary.
539    /// It will be cancelled when the client go off-channel for scanning and scheduled again when
540    /// back on channel.
541    pub status_check_timeout: StatusCheckTimeout,
542    pub signal_strength_average: SignalStrengthAverage,
543
544    // TODO(https://fxbug.dev/335283785): Remove or explain unused code.
545    #[allow(dead_code)]
546    pub block_ack_state: StateMachine<BlockAckState>,
547}
548
549/// Client received a "successful" association response from the BSS.
550#[derive(Debug)]
551pub struct Associated(pub Association);
552
553impl Associated {
554    /// Processes an inbound disassociation frame.
555    /// This always results in an MLME-DISASSOCIATE.indication message to MLME's SME peer.
556    async fn on_disassoc_frame<D: DeviceOps>(
557        &mut self,
558        sta: &mut BoundClient<'_, D>,
559        disassoc_hdr: &mac::DisassocHdr,
560    ) {
561        wtrace::duration!("Associated::on_disassoc_frame");
562        self.pre_leaving_associated_state(sta).await;
563        let reason_code = fidl_ieee80211::ReasonCode::from_primitive(disassoc_hdr.reason_code.0)
564            .unwrap_or(fidl_ieee80211::ReasonCode::UnspecifiedReason);
565        sta.send_disassoc_ind(reason_code, LocallyInitiated(false));
566    }
567
568    /// Sends an MLME-DEAUTHENTICATE.indication message to MLME's SME peer.
569    async fn on_deauth_frame<D: DeviceOps>(
570        &mut self,
571        sta: &mut BoundClient<'_, D>,
572        deauth_hdr: &mac::DeauthHdr,
573    ) {
574        wtrace::duration!("Associated::on_deauth_frame");
575        self.pre_leaving_associated_state(sta).await;
576        let reason_code = fidl_ieee80211::ReasonCode::from_primitive(deauth_hdr.reason_code.0)
577            .unwrap_or(fidl_ieee80211::ReasonCode::UnspecifiedReason);
578        sta.send_deauthenticate_ind(reason_code, LocallyInitiated(false));
579        let _ =
580            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
581    }
582
583    /// Process every inbound management frame before its being handed off to a more specific
584    /// handler.
585    fn on_any_mgmt_frame<D: DeviceOps>(
586        &self,
587        sta: &mut BoundClient<'_, D>,
588        mgmt_hdr: &mac::MgmtHdr,
589    ) {
590        self.request_bu_if_available(sta, mgmt_hdr.frame_ctrl, mgmt_hdr.addr1);
591    }
592
593    /// Sends PS-POLL requests if the FrameControl's more_data bit is set, and the received frame
594    /// was addressed for this STA. No-op if the controlled port is closed.
595    fn request_bu_if_available<D: DeviceOps>(
596        &self,
597        sta: &mut BoundClient<'_, D>,
598        fc: mac::FrameControl,
599        dst_addr: MacAddr,
600    ) {
601        if !self.0.controlled_port_open {
602            return;
603        }
604        // IEEE Std. 802.11-2016, 9.2.4.1.8
605        if fc.more_data() && dst_addr == sta.sta.iface_mac {
606            let _result = sta.send_ps_poll_frame(self.0.aid);
607        }
608    }
609
610    fn extract_and_record_signal_dbm(&mut self, rx_info: fidl_softmac::WlanRxInfo) {
611        get_rssi_dbm(rx_info)
612            .map(|rssi_dbm| self.0.signal_strength_average.add(DecibelMilliWatt(rssi_dbm)));
613    }
614
615    /// Process and inbound beacon frame.
616    /// Resets LostBssCounter, check buffered frame if available.
617    async fn on_beacon_frame<B: SplitByteSlice, D: DeviceOps>(
618        &mut self,
619        sta: &mut BoundClient<'_, D>,
620        header: &BeaconHdr,
621        elements: B,
622    ) {
623        wtrace::duration!("Associated::on_beacon_frame");
624        self.0.lost_bss_counter.reset();
625        // TODO(b/253637931): Add metrics to track channel switch counts and success rates.
626        if let Err(e) =
627            sta.channel_state.bind(sta.ctx, sta.scanner).handle_beacon(header, &elements[..]).await
628        {
629            warn!("Failed to handle channel switch announcement: {}", e);
630        }
631        for (id, body) in ie::Reader::new(elements) {
632            match id {
633                ie::Id::TIM => match ie::parse_tim(body) {
634                    Ok(ie::TimView { header, bitmap }) => {
635                        if tim::is_traffic_buffered(header.bmp_ctrl.offset(), &bitmap, self.0.aid) {
636                            let _result = sta.send_ps_poll_frame(self.0.aid);
637                        }
638                    }
639                    _ => (),
640                },
641
642                _ => (),
643            }
644        }
645    }
646
647    /// Extracts aggregated and non-aggregated MSDUs from the data frame.
648    /// Handles all data subtypes.
649    /// EAPoL MSDUs are forwarded to SME via an MLME-EAPOL.indication message independent of the
650    /// STA's current controlled port status.
651    /// All other MSDUs are converted into Ethernet II frames and forwarded via the device to
652    /// Fuchsia's Netstack if the STA's controlled port is open.
653    /// NULL-Data frames are interpreted as "Keep Alive" requests and responded with NULL data
654    /// frames if the STA's controlled port is open.
655    fn on_data_frame<B: SplitByteSlice, D: DeviceOps>(
656        &self,
657        sta: &mut BoundClient<'_, D>,
658        data_frame: mac::DataFrame<B>,
659        async_id: TraceId,
660    ) {
661        const MSDU_TRACE_NAME: &'static str = "States::on_data_frame => MSDU";
662
663        wtrace::duration!("States::on_data_frame");
664
665        self.request_bu_if_available(
666            sta,
667            data_frame.frame_ctrl(),
668            mac::data_dst_addr(&data_frame.fixed_fields),
669        );
670
671        // Handle NULL data frames independently of the controlled port's status.
672        if data_frame.data_subtype().null() {
673            if let Err(e) = sta.send_keep_alive_resp_frame() {
674                error!("error sending keep alive frame: {}", e);
675            }
676        }
677
678        // Handle aggregated and non-aggregated MSDUs.
679        for msdu in data_frame {
680            wtrace::duration_begin!(MSDU_TRACE_NAME);
681
682            match msdu.llc_frame.hdr.protocol_id.get() {
683                // Forward EAPoL frames to SME independently of the controlled port's status.
684                mac::ETHER_TYPE_EAPOL => {
685                    let mac::Msdu { dst_addr, src_addr, llc_frame } = msdu;
686                    if let Err(e) =
687                        sta.send_eapol_indication(src_addr, dst_addr, &llc_frame.body[..])
688                    {
689                        wtrace::duration_end!(
690                            MSDU_TRACE_NAME,
691                            "status" => "failure sending EAPOL indication",
692                        );
693                        error!("error sending MLME-EAPOL.indication: {}", e);
694                    } else {
695                        wtrace::duration_end!(
696                            MSDU_TRACE_NAME,
697                            "status" => "sent EAPOL indication",
698                        );
699                    }
700                }
701                // Deliver non-EAPoL MSDUs only if the controlled port is open.
702                _ if self.0.controlled_port_open => {
703                    if let Err(e) = sta.deliver_msdu(msdu) {
704                        wtrace::duration_end!(
705                            MSDU_TRACE_NAME,
706                            "status" => "failure delivering MSDU",
707                        );
708                        error!("error while handling data frame: {}", e);
709                    } else {
710                        wtrace::duration_end!(
711                            MSDU_TRACE_NAME,
712                            "status" => "delivered MSDU",
713                        );
714                    }
715                }
716                // Drop all non-EAPoL MSDUs if the controlled port is closed.
717                _ => {
718                    wtrace::duration_end!(
719                        MSDU_TRACE_NAME,
720                        "status" => "dropping MSDU. controlled port closed.",
721                    );
722                }
723            }
724        }
725        wtrace::async_end_wlansoftmac_rx(async_id, "completed data frame processing");
726    }
727
728    fn on_eth_frame<B: SplitByteSlice, D: DeviceOps>(
729        &self,
730        sta: &mut BoundClient<'_, D>,
731        frame: B,
732        async_id: TraceId,
733    ) -> Result<(), Error> {
734        wtrace::duration!("Associated::on_eth_frame");
735        let mac::EthernetFrame { hdr, body } = match mac::EthernetFrame::parse(frame) {
736            Some(eth_frame) => eth_frame,
737            None => {
738                return Err(Error::Status(
739                    format!("Ethernet frame too short"),
740                    zx::Status::IO_DATA_INTEGRITY,
741                ));
742            }
743        };
744
745        if !self.0.controlled_port_open {
746            return Err(Error::Status(
747                format!("Ethernet dropped. RSN not established"),
748                zx::Status::BAD_STATE,
749            ));
750        }
751
752        sta.send_data_frame(
753            hdr.sa,
754            hdr.da,
755            sta.sta.eapol_required(),
756            self.0.qos.is_enabled(),
757            hdr.ether_type.get(),
758            &body,
759            Some(async_id),
760        )
761    }
762
763    fn on_block_ack_frame<B: SplitByteSlice, D>(
764        &mut self,
765        _sta: &mut BoundClient<'_, D>,
766        _action: mac::BlockAckAction,
767        _body: B,
768    ) {
769        // TODO(https://fxbug.dev/42104687): Handle BlockAck frames. The following code has been disabled as a
770        //                        fix for https://fxbug.dev/42180615. Without this code, the BlockAck state
771        //                        machine is dormant and, importantly, never transmits BlockAck
772        //                        frames.
773        //self.0.block_ack_state.replace_state(|state| state.on_block_ack_frame(sta, action, body));
774    }
775
776    async fn on_spectrum_mgmt_frame<B: SplitByteSlice, D: DeviceOps>(
777        &mut self,
778        sta: &mut BoundClient<'_, D>,
779        action: mac::SpectrumMgmtAction,
780        body: B,
781    ) {
782        match action {
783            mac::SpectrumMgmtAction::CHANNEL_SWITCH_ANNOUNCEMENT => {
784                if let Err(e) = sta
785                    .channel_state
786                    .bind(sta.ctx, sta.scanner)
787                    .handle_announcement_frame(&body[..])
788                    .await
789                {
790                    warn!("Failed to handle channel switch announcement: {}", e);
791                }
792            }
793            _ => (),
794        }
795    }
796
797    fn on_sme_eapol<D: DeviceOps>(
798        &self,
799        sta: &mut BoundClient<'_, D>,
800        req: fidl_mlme::EapolRequest,
801    ) {
802        // Drop EAPoL frame if it is not a protected network.
803        if !sta.sta.eapol_required() {
804            error!("Unexpected MLME-EAPOL.request message: BSS not protected");
805            return;
806        }
807        // There may be more EAPoL frames (such as key rotation) coming after EAPoL established.
808        // They need to be protected.
809        let protected = sta.sta.eapol_required() && self.0.controlled_port_open;
810        sta.send_eapol_frame(req.src_addr.into(), req.dst_addr.into(), protected, &req.data);
811    }
812
813    async fn on_sme_set_keys<D: DeviceOps>(
814        &self,
815        sta: &mut BoundClient<'_, D>,
816        req: fidl_mlme::SetKeysRequest,
817    ) {
818        if !sta.sta.eapol_required() {
819            error!("Unexpected MLME-SetKeys.request message: BSS not protected");
820            return;
821        }
822        let mut results = Vec::with_capacity(req.keylist.len());
823        for key_descriptor in req.keylist {
824            let key_id = key_descriptor.key_id;
825
826            match sta
827                .ctx
828                .device
829                .install_key(&softmac_key_configuration_from_mlme(key_descriptor))
830                .await
831            {
832                Ok(()) => results
833                    .push(fidl_mlme::SetKeyResult { key_id, status: zx::Status::OK.into_raw() }),
834                Err(e) => {
835                    error!("failed to set key: {}", e);
836                    results.push(fidl_mlme::SetKeyResult { key_id, status: e.into_raw() })
837                }
838            }
839        }
840        if let Err(e) = sta.ctx.device.send_mlme_event(fidl_mlme::MlmeEvent::SetKeysConf {
841            conf: fidl_mlme::SetKeysConfirm { results },
842        }) {
843            error!("Error sending SetKeysConfirm: {}", e);
844        }
845    }
846
847    async fn on_sme_set_controlled_port<D: DeviceOps>(
848        &mut self,
849        sta: &mut BoundClient<'_, D>,
850        req: fidl_mlme::SetControlledPortRequest,
851    ) {
852        if !sta.sta.eapol_required() {
853            error!("Unexpected MLME-SetControlledPort.request message: BSS not protected.");
854            return;
855        }
856        let should_open_controlled_port = req.state == fidl_mlme::ControlledPortState::Open;
857        if should_open_controlled_port == self.0.controlled_port_open {
858            return;
859        }
860        self.0.controlled_port_open = should_open_controlled_port;
861        if let Err(e) = sta.ctx.device.set_ethernet_status(req.state.into()).await {
862            error!(
863                "Error setting Ethernet port to {}: {}",
864                if should_open_controlled_port { "OPEN" } else { "CLOSED" },
865                e
866            );
867        }
868    }
869
870    async fn on_sme_deauthenticate<D: DeviceOps>(
871        &mut self,
872        sta: &mut BoundClient<'_, D>,
873        req: fidl_mlme::DeauthenticateRequest,
874    ) {
875        if let Err(e) = sta.send_deauth_frame(mac::ReasonCode(req.reason_code.into_primitive())) {
876            error!("Error sending deauthentication frame to BSS: {}", e);
877        }
878
879        self.pre_leaving_associated_state(sta).await;
880        let _ =
881            sta.clear_association().await.map_err(|e| error!("Failed to clear association: {}", e));
882
883        if let Err(e) = sta.ctx.device.send_mlme_event(fidl_mlme::MlmeEvent::DeauthenticateConf {
884            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: sta.sta.bssid().to_array() },
885        }) {
886            error!("Error sending MLME-DEAUTHENTICATE.confirm: {}", e)
887        }
888    }
889
890    async fn pre_leaving_associated_state<D: DeviceOps>(&mut self, sta: &mut BoundClient<'_, D>) {
891        self.0.status_check_timeout.next_event.take();
892        self.0.controlled_port_open = false;
893        if let Err(e) = sta.ctx.device.set_ethernet_down().await {
894            error!("Error disabling ethernet device offline: {}", e);
895        }
896    }
897
898    #[must_use]
899    /// Reports average signal strength to SME and check if auto deauthentication is due.
900    /// Returns true if there auto deauthentication is triggered by lack of beacon frames.
901    async fn on_timeout<D: DeviceOps>(&mut self, sta: &mut BoundClient<'_, D>) -> bool {
902        if let Err(e) = sta.ctx.device.send_mlme_event(fidl_mlme::MlmeEvent::SignalReport {
903            ind: fidl_internal::SignalReportIndication {
904                rssi_dbm: self.0.signal_strength_average.avg_dbm().0,
905                snr_db: 0,
906            },
907        }) {
908            error!("Error sending MLME-SignalReport: {}", e)
909        }
910
911        let auto_deauth = self.0.lost_bss_counter.should_deauthenticate();
912        if auto_deauth {
913            sta.send_deauthenticate_ind(
914                fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
915                LocallyInitiated(true),
916            );
917            if let Err(e) =
918                sta.send_deauth_frame(fidl_ieee80211::ReasonCode::LeavingNetworkDeauth.into())
919            {
920                warn!("Failed sending deauth frame {:?}", e);
921            }
922            self.pre_leaving_associated_state(sta).await;
923        } else {
924            // Always check should_deauthenticate() first since even if Client receives a beacon,
925            // it would still add a full association status check interval to the lost BSS counter.
926            self.0.lost_bss_counter.add_beacon_interval(ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT);
927            self.0.status_check_timeout =
928                schedule_association_status_timeout(sta.sta.beacon_period(), &mut sta.ctx.timer);
929        }
930        auto_deauth
931    }
932}
933
934statemachine!(
935    /// Client state machine.
936    #[derive(Debug)]
937    pub enum States,
938    // Regular successful flow:
939    () => Joined,
940    Joined => Authenticating,
941    Authenticating => Associating,
942    Associating => Associated,
943
944    // Timeout:
945    Authenticating => Joined,
946    Associating => Joined,
947
948    // Deauthentication:
949    Authenticating => Joined,
950    Associating => Joined,
951    Associated => Joined,
952
953    // Disassociation:
954    Associating => Authenticated, // Or failure to (re)associate
955    Associated => Authenticated,
956
957    // Reassociation:
958    Authenticated => Associating,
959);
960
961impl States {
962    /// Returns the STA's initial state.
963    pub fn new_initial() -> States {
964        States::from(State::new(Joined))
965    }
966
967    /// Begin the 802.11 connect operation, starting with authentication.
968    /// This method only has an effect if the initial state is Joined.
969    /// Otherwise it is no-op.
970    pub async fn start_connecting<D: DeviceOps>(self, sta: &mut BoundClient<'_, D>) -> States {
971        match self {
972            States::Joined(state) => {
973                // Setting timeout in term of beacon period allows us to adjust the realtime
974                // timeout in hw-sim, where we set a longer duration in case of a slowbot.
975                let duration =
976                    sta.sta.beacon_period() * sta.sta.connect_req.connect_failure_timeout;
977                let timeout = sta.ctx.timer.schedule_after(duration, TimedEvent::Connecting);
978                sta.sta.connect_timeout.replace(timeout);
979                match state.start_authenticating(sta).await {
980                    Ok(algorithm) => state.transition_to(Authenticating::new(algorithm)).into(),
981                    Err(()) => state.transition_to(Joined).into(),
982                }
983            }
984            other => {
985                warn!("Attempting to connect from a post-Joined state. Connect request ignored");
986                other
987            }
988        }
989    }
990
991    /// Callback to process arbitrary IEEE 802.11 frames.
992    /// Frames are dropped if:
993    /// - frames are corrupted (too short)
994    /// - frames' frame class is not yet permitted
995    /// - frames are from a foreign BSS
996    /// - frames are unicast but destined for a MAC address that is different from this STA.
997    // TODO(https://fxbug.dev/42119762): Implement a packet counter and add tests to verify frames are dropped correctly.
998    pub async fn on_mac_frame<B: SplitByteSlice, D: DeviceOps>(
999        mut self,
1000        sta: &mut BoundClient<'_, D>,
1001        bytes: B,
1002        rx_info: fidl_softmac::WlanRxInfo,
1003        async_id: TraceId,
1004    ) -> States {
1005        wtrace::duration!("States::on_mac_frame");
1006
1007        let body_aligned = (rx_info.rx_flags & fidl_softmac::WlanRxInfoFlags::FRAME_BODY_PADDING_4)
1008            != fidl_softmac::WlanRxInfoFlags::empty();
1009
1010        // Parse mac frame. Drop corrupted ones.
1011        trace!("Parsing MAC frame:\n  {:02x?}", bytes.deref());
1012        let mac_frame = match mac::MacFrame::parse(bytes, body_aligned) {
1013            Some(mac_frame) => mac_frame,
1014            None => {
1015                debug!("Dropping corrupt MAC frame.");
1016                wtrace::async_end_wlansoftmac_rx(async_id, "corrupt frame");
1017                return self;
1018            }
1019        };
1020
1021        if !sta.sta.should_handle_frame(&mac_frame) {
1022            warn!("Mac frame is either from a foreign BSS or not destined for us. Dropped.");
1023            wtrace::async_end_wlansoftmac_rx(async_id, "foreign BSS frame");
1024            return self;
1025        }
1026
1027        // Drop frames which are not permitted in the STA's current state.
1028        let frame_class = mac::FrameClass::from(&mac_frame);
1029        if !self.is_frame_class_permitted(frame_class) {
1030            debug!("Dropping MAC frame with prohibited frame class.");
1031            wtrace::async_end_wlansoftmac_rx(async_id, "frame with prohibited frame class");
1032            return self;
1033        }
1034
1035        match mac_frame {
1036            mac::MacFrame::Mgmt(mgmt_frame) => {
1037                let states = self.on_mgmt_frame(sta, mgmt_frame, rx_info).await;
1038                wtrace::async_end_wlansoftmac_rx(
1039                    async_id,
1040                    "management frame successfully received",
1041                );
1042                states
1043            }
1044            mac::MacFrame::Data(data_frame) => {
1045                if let States::Associated(state) = &mut self {
1046                    state.on_data_frame(sta, data_frame, async_id);
1047                    state.extract_and_record_signal_dbm(rx_info);
1048                } else {
1049                    // Drop data frames in all other states
1050                    debug!("Dropping MAC data frame while not associated.");
1051                    wtrace::async_end_wlansoftmac_rx(async_id, "data frame while not associated");
1052                }
1053                self
1054            }
1055            // Control frames are not yet supported. Drop them.
1056            _ => {
1057                debug!("Dropping unsupported MAC control frame.");
1058                wtrace::async_end_wlansoftmac_rx(async_id, "unsupported control frame");
1059                self
1060            }
1061        }
1062    }
1063
1064    /// Processes inbound management frames.
1065    /// Only frames from the joined BSS are processed. Frames from other STAs are dropped.
1066    async fn on_mgmt_frame<B: SplitByteSlice, D: DeviceOps>(
1067        self,
1068        sta: &mut BoundClient<'_, D>,
1069        mgmt_frame: mac::MgmtFrame<B>,
1070        rx_info: fidl_softmac::WlanRxInfo,
1071    ) -> States {
1072        wtrace::duration!("States::on_mgmt_frame");
1073
1074        // Parse management frame. Drop corrupted ones.
1075        let (mgmt_hdr, mgmt_body) = match mgmt_frame.try_into_mgmt_body() {
1076            (mgmt_hdr, Some(mgmt_body)) => (mgmt_hdr, mgmt_body),
1077            (_, None) => return self,
1078        };
1079
1080        match self {
1081            States::Authenticating(mut state) => match mgmt_body {
1082                mac::MgmtBody::Authentication(auth_frame) => match state
1083                    .on_auth_frame(sta, auth_frame)
1084                    .await
1085                {
1086                    AuthProgress::Complete => state.transition_to(Associating::default()).into(),
1087                    AuthProgress::InProgress => state.into(),
1088                    AuthProgress::Failed => state.transition_to(Joined).into(),
1089                },
1090                mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
1091                    state.on_deauth_frame(sta, &deauth_hdr).await;
1092                    state.transition_to(Joined).into()
1093                }
1094                _ => state.into(),
1095            },
1096            States::Associating(mut state) => match mgmt_body {
1097                mac::MgmtBody::AssociationResp(assoc_resp_frame) => {
1098                    match state.on_assoc_resp_frame(sta, assoc_resp_frame).await {
1099                        Ok(association) => state.transition_to(Associated(association)).into(),
1100                        Err(()) => state.transition_to(Joined).into(),
1101                    }
1102                }
1103                mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
1104                    state.on_deauth_frame(sta, &deauth_hdr).await;
1105                    state.transition_to(Joined).into()
1106                }
1107                // This case is highly unlikely and only added to improve interoperability with
1108                // buggy Access Points.
1109                mac::MgmtBody::Disassociation { disassoc_hdr, .. } => {
1110                    state.on_disassoc_frame(sta, &disassoc_hdr);
1111                    state.transition_to(Authenticated).into()
1112                }
1113                _ => state.into(),
1114            },
1115            States::Associated(mut state) => {
1116                state.extract_and_record_signal_dbm(rx_info);
1117                state.on_any_mgmt_frame(sta, &mgmt_hdr);
1118                match mgmt_body {
1119                    mac::MgmtBody::Beacon { bcn_hdr, elements } => {
1120                        state.on_beacon_frame(sta, &bcn_hdr, elements).await;
1121                        state.into()
1122                    }
1123                    mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
1124                        state.on_deauth_frame(sta, &deauth_hdr).await;
1125                        state.transition_to(Joined).into()
1126                    }
1127                    mac::MgmtBody::Disassociation { disassoc_hdr, .. } => {
1128                        state.on_disassoc_frame(sta, &disassoc_hdr).await;
1129                        state.transition_to(Authenticated).into()
1130                    }
1131                    mac::MgmtBody::Action(action_frame) => {
1132                        let mac::ActionBody { action_hdr, elements, .. } = action_frame.into_body();
1133                        match action_hdr.action {
1134                            mac::ActionCategory::BLOCK_ACK => {
1135                                let reader = BufferReader::new(elements);
1136                                if let Some(action) = reader.peek_unaligned::<mac::BlockAckAction>()
1137                                {
1138                                    state.on_block_ack_frame(
1139                                        sta,
1140                                        action.get(),
1141                                        reader.into_remaining(),
1142                                    );
1143                                }
1144                                state.into()
1145                            }
1146                            mac::ActionCategory::SPECTRUM_MGMT => {
1147                                let reader = BufferReader::new(elements);
1148                                if let Some(action) =
1149                                    reader.peek_unaligned::<mac::SpectrumMgmtAction>()
1150                                {
1151                                    state
1152                                        .on_spectrum_mgmt_frame(
1153                                            sta,
1154                                            action.get(),
1155                                            reader.into_remaining(),
1156                                        )
1157                                        .await;
1158                                }
1159                                state.into()
1160                            }
1161                            _ => state.into(),
1162                        }
1163                    }
1164                    _ => state.into(),
1165                }
1166            }
1167            _ => self,
1168        }
1169    }
1170
1171    pub fn on_eth_frame<B: SplitByteSlice, D: DeviceOps>(
1172        &self,
1173        sta: &mut BoundClient<'_, D>,
1174        frame: B,
1175        async_id: TraceId,
1176    ) -> Result<(), Error> {
1177        wtrace::duration!("States::on_eth_frame");
1178        match self {
1179            States::Associated(state) => state.on_eth_frame(sta, frame, async_id),
1180            _ => Err(Error::Status(
1181                format!("Not associated. Ethernet dropped"),
1182                zx::Status::BAD_STATE,
1183            )),
1184        }
1185    }
1186
1187    /// Callback when a previously scheduled event fired.
1188    pub async fn on_timed_event<D: DeviceOps>(
1189        self,
1190        sta: &mut BoundClient<'_, D>,
1191        event: TimedEvent,
1192    ) -> States {
1193        match event {
1194            TimedEvent::Connecting => {
1195                sta.sta.connect_timeout.take();
1196                sta.send_connect_conf_failure(fidl_ieee80211::StatusCode::RejectedSequenceTimeout);
1197                let _ = sta
1198                    .clear_association()
1199                    .await
1200                    .map_err(|e| error!("Failed to clear association: {}", e));
1201                match self {
1202                    States::Authenticating(state) => state.transition_to(Joined).into(),
1203                    States::Associating(state) => state.transition_to(Joined).into(),
1204                    States::Associated(state) => state.transition_to(Joined).into(),
1205                    _ => self,
1206                }
1207            }
1208            TimedEvent::Reassociating => match self {
1209                States::Associating(mut state) => {
1210                    state.reconnect_timeout.take();
1211                    sta.send_connect_conf_failure(
1212                        fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
1213                    );
1214                    state.transition_to(Authenticated).into()
1215                }
1216                _ => self,
1217            },
1218            TimedEvent::AssociationStatusCheck => match self {
1219                States::Associated(mut state) => {
1220                    let should_auto_deauth = state.on_timeout(sta).await;
1221                    match should_auto_deauth {
1222                        true => state.transition_to(Joined).into(),
1223                        false => state.into(),
1224                    }
1225                }
1226                _ => self,
1227            },
1228            TimedEvent::ChannelSwitch => {
1229                if let Err(e) = sta
1230                    .channel_state
1231                    .bind(sta.ctx, sta.scanner)
1232                    .handle_channel_switch_timeout()
1233                    .await
1234                {
1235                    error!("ChannelSwitch timeout handler failed: {}", e);
1236                }
1237                self
1238            }
1239        }
1240    }
1241
1242    pub async fn handle_mlme_req<D: DeviceOps>(
1243        self,
1244        sta: &mut BoundClient<'_, D>,
1245        req: wlan_sme::MlmeRequest,
1246    ) -> States {
1247        use wlan_sme::MlmeRequest as MlmeReq;
1248
1249        match self {
1250            States::Joined(mut state) => match req {
1251                MlmeReq::Deauthenticate(_) => {
1252                    state.on_sme_deauthenticate(sta).await;
1253                    state.into()
1254                }
1255                MlmeReq::Reconnect(req) => {
1256                    sta.send_connect_conf_failure_with_bssid(
1257                        req.peer_sta_address.into(),
1258                        fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
1259                    );
1260                    state.into()
1261                }
1262                _ => state.into(),
1263            },
1264            States::Authenticating(mut state) => match req {
1265                MlmeReq::SaeHandshakeResp(resp) => match state.on_sme_sae_resp(sta, resp).await {
1266                    AuthProgress::Complete => state.transition_to(Associating::default()).into(),
1267                    AuthProgress::InProgress => state.into(),
1268                    AuthProgress::Failed => state.transition_to(Joined).into(),
1269                },
1270                MlmeReq::SaeFrameTx(frame) => match state.on_sme_sae_tx(sta, frame).await {
1271                    AuthProgress::Complete => state.transition_to(Associating::default()).into(),
1272                    AuthProgress::InProgress => state.into(),
1273                    AuthProgress::Failed => state.transition_to(Joined).into(),
1274                },
1275                MlmeReq::Deauthenticate(_) => {
1276                    state.on_sme_deauthenticate(sta).await;
1277                    state.transition_to(Joined).into()
1278                }
1279                MlmeReq::Reconnect(req) => {
1280                    sta.send_connect_conf_failure_with_bssid(
1281                        req.peer_sta_address.into(),
1282                        fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
1283                    );
1284                    state.into()
1285                }
1286                _ => state.into(),
1287            },
1288            States::Authenticated(state) => match req {
1289                MlmeReq::Reconnect(req) => match state.on_sme_reconnect(sta, req) {
1290                    Ok(timeout) => {
1291                        state.transition_to(Associating::new_with_reconnect_timeout(timeout)).into()
1292                    }
1293                    Err(()) => state.into(),
1294                },
1295                _ => state.into(),
1296            },
1297            States::Associating(mut state) => match req {
1298                MlmeReq::Deauthenticate(_) => {
1299                    state.on_sme_deauthenticate(sta).await;
1300                    state.transition_to(Joined).into()
1301                }
1302                MlmeReq::Reconnect(req) => {
1303                    let peer_sta_address: Bssid = req.peer_sta_address.into();
1304                    if peer_sta_address != sta.sta.connect_req.selected_bss.bssid {
1305                        sta.send_connect_conf_failure_with_bssid(
1306                            peer_sta_address,
1307                            fidl_ieee80211::StatusCode::NotInSameBss,
1308                        );
1309                    }
1310                    state.into()
1311                }
1312                _ => state.into(),
1313            },
1314            States::Associated(mut state) => match req {
1315                MlmeReq::Eapol(req) => {
1316                    state.on_sme_eapol(sta, req);
1317                    state.into()
1318                }
1319                MlmeReq::SetKeys(req) => {
1320                    state.on_sme_set_keys(sta, req).await;
1321                    state.into()
1322                }
1323                MlmeReq::SetCtrlPort(req) => {
1324                    state.on_sme_set_controlled_port(sta, req).await;
1325                    state.into()
1326                }
1327                MlmeReq::Deauthenticate(req) => {
1328                    state.on_sme_deauthenticate(sta, req).await;
1329                    state.transition_to(Joined).into()
1330                }
1331                MlmeReq::Reconnect(req) => {
1332                    let peer_sta_address: Bssid = req.peer_sta_address.into();
1333                    if peer_sta_address != sta.sta.connect_req.selected_bss.bssid {
1334                        sta.send_connect_conf_failure_with_bssid(
1335                            peer_sta_address,
1336                            fidl_ieee80211::StatusCode::NotInSameBss,
1337                        );
1338                    } else {
1339                        sta.send_connect_conf_success(state.0.aid, &state.0.assoc_resp_ies[..]);
1340                    }
1341                    state.into()
1342                }
1343                _ => state.into(),
1344            },
1345        }
1346    }
1347
1348    /// Returns |true| iff a given FrameClass is permitted to be processed in the current state.
1349    fn is_frame_class_permitted(&self, class: mac::FrameClass) -> bool {
1350        wtrace::duration!("State::is_frame_class_permitted");
1351        match self {
1352            States::Joined(_) | States::Authenticating(_) => class == mac::FrameClass::Class1,
1353            States::Authenticated(_) | States::Associating(_) => class <= mac::FrameClass::Class2,
1354            States::Associated(_) => class <= mac::FrameClass::Class3,
1355        }
1356    }
1357}
1358
1359#[cfg(test)]
1360mod free_function_tests {
1361    use super::*;
1362    use wlan_common::mac::IntoBytesExt as _;
1363
1364    fn assoc_resp_frame_from_ies(elements: &[u8]) -> mac::AssocRespFrame<&[u8]> {
1365        mac::AssocRespFrame {
1366            assoc_resp_hdr: mac::AssocRespHdr {
1367                capabilities: mac::CapabilityInfo(0u16),
1368                status_code: mac::StatusCode(0u16),
1369                aid: 0u16,
1370            }
1371            .as_bytes_ref(),
1372            elements,
1373        }
1374    }
1375
1376    #[test]
1377    fn test_extract_ht_vht_op_success() {
1378        let mut buffer = Vec::new();
1379        ie::write_ht_operation(&mut buffer, &ie::fake_ht_operation()).expect("valid HT Op");
1380        ie::write_vht_operation(&mut buffer, &ie::fake_vht_operation()).expect("valid VHT Op");
1381        let (ht_operation, vht_operation) =
1382            extract_ht_vht_op(&assoc_resp_frame_from_ies(&buffer[..]));
1383        assert_eq!(ht_operation.unwrap(), ie::fake_ht_operation());
1384        assert_eq!(vht_operation.unwrap(), ie::fake_vht_operation());
1385    }
1386
1387    #[test]
1388    fn test_extract_ht_op_too_short() {
1389        let mut buffer = Vec::<u8>::new();
1390        ie::write_ht_operation(&mut buffer, &ie::fake_ht_operation()).expect("valid HT Op");
1391        buffer[1] -= 1; // Make length shorter
1392        buffer.truncate(buffer.len() - 1);
1393        ie::write_vht_operation(&mut buffer, &ie::fake_vht_operation()).expect("valid VHT Op");
1394        let (ht_operation, vht_operation) =
1395            extract_ht_vht_op(&assoc_resp_frame_from_ies(&buffer[..]));
1396        assert_eq!(ht_operation, None);
1397        assert_eq!(vht_operation.unwrap(), ie::fake_vht_operation());
1398    }
1399
1400    #[test]
1401    fn test_extract_vht_op_too_short() {
1402        let mut buffer = Vec::new();
1403        ie::write_ht_operation(&mut buffer, &ie::fake_ht_operation()).expect("valid HT Op");
1404        let ht_end = buffer.len();
1405        ie::write_vht_operation(&mut buffer, &ie::fake_vht_operation()).expect("valid VHT Op");
1406        buffer[ht_end + 1] -= 1; // Make VHT operation shorter.
1407        buffer.truncate(buffer.len() - 1);
1408        let (ht_operation, vht_operation) =
1409            extract_ht_vht_op(&assoc_resp_frame_from_ies(&buffer[..]));
1410        assert_eq!(ht_operation.unwrap(), ie::fake_ht_operation());
1411        assert_eq!(vht_operation, None);
1412    }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use super::*;
1418    use crate::block_ack::{ADDBA_REQ_FRAME_LEN, write_addba_req_body};
1419    use crate::client::channel_switch::ChannelState;
1420    use crate::client::scanner::Scanner;
1421    use crate::client::test_utils::drain_timeouts;
1422    use crate::client::{Client, Context, ParsedConnectRequest, TimedEventClass};
1423    use crate::device::{FakeDevice, FakeDeviceState};
1424    use crate::test_utils::{MockWlanRxInfo, fake_set_keys_req, fake_wlan_channel};
1425    use akm::AkmAlgorithm;
1426    use assert_matches::assert_matches;
1427    use fuchsia_sync::Mutex;
1428    use std::sync::{Arc, LazyLock};
1429    use test_case::test_case;
1430    use wlan_common::buffer_writer::BufferWriter;
1431    use wlan_common::ie::IeType;
1432    use wlan_common::mac::IntoBytesExt as _;
1433    use wlan_common::sequence::SequenceManager;
1434    use wlan_common::test_utils::fake_capabilities::fake_client_capabilities;
1435    use wlan_common::test_utils::fake_frames::*;
1436    use wlan_common::test_utils::fake_stas::IesOverrides;
1437    use wlan_common::timer::{self, create_timer};
1438    use wlan_common::{fake_bss_description, mgmt_writer};
1439    use wlan_frame_writer::append_frame_to;
1440    use {fidl_fuchsia_wlan_common as fidl_common, wlan_statemachine as statemachine};
1441
1442    static BSSID: LazyLock<Bssid> = LazyLock::new(|| [6u8; 6].into());
1443    static IFACE_MAC: LazyLock<MacAddr> = LazyLock::new(|| [3u8; 6].into());
1444
1445    struct MockObjects {
1446        fake_device: FakeDevice,
1447        fake_device_state: Arc<Mutex<FakeDeviceState>>,
1448        timer: Option<Timer<TimedEvent>>,
1449        time_stream: timer::EventStream<TimedEvent>,
1450        scanner: Scanner,
1451        channel_state: ChannelState,
1452    }
1453
1454    impl MockObjects {
1455        // TODO(https://fxbug.dev/327499461): This function is async to ensure MLME functions will
1456        // run in an async context and not call `wlan_common::timer::Timer::now` without an
1457        // executor.
1458        async fn new() -> Self {
1459            let (timer, time_stream) = create_timer();
1460            let (fake_device, fake_device_state) = FakeDevice::new().await;
1461            Self {
1462                fake_device,
1463                fake_device_state,
1464                timer: Some(timer),
1465                time_stream,
1466                scanner: Scanner::new(*IFACE_MAC),
1467                channel_state: ChannelState::new_with_main_channel(fake_wlan_channel().into()),
1468            }
1469        }
1470
1471        async fn make_ctx(&mut self) -> Context<FakeDevice> {
1472            self.fake_device
1473                .set_channel(fake_wlan_channel().into())
1474                .await
1475                .expect("fake device is obedient");
1476            self.make_base_ctx()
1477        }
1478
1479        async fn make_ctx_with_bss(&mut self) -> Context<FakeDevice> {
1480            self.fake_device
1481                .set_channel(fake_wlan_channel().into())
1482                .await
1483                .expect("fake device is obedient");
1484            self.fake_device
1485                .join_bss(&fidl_common::JoinBssRequest {
1486                    bssid: Some([1, 2, 3, 4, 5, 6]),
1487                    bss_type: Some(fidl_common::BssType::Personal),
1488                    remote: Some(true),
1489                    beacon_period: Some(100),
1490                    ..Default::default()
1491                })
1492                .await
1493                .expect("error configuring bss");
1494            self.make_base_ctx()
1495        }
1496
1497        fn make_base_ctx(&mut self) -> Context<FakeDevice> {
1498            Context {
1499                _config: Default::default(),
1500                device: self.fake_device.clone(),
1501                timer: self.timer.take().unwrap(),
1502                seq_mgr: SequenceManager::new(),
1503            }
1504        }
1505    }
1506
1507    fn make_client_station() -> Client {
1508        let connect_req = ParsedConnectRequest {
1509            selected_bss: fake_bss_description!(Open, bssid: BSSID.to_array()),
1510            connect_failure_timeout: 10,
1511            auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
1512            sae_password: vec![],
1513            wep_key: None,
1514            security_ie: vec![],
1515            owe_public_key: None,
1516        };
1517        Client::new(connect_req, *IFACE_MAC, fake_client_capabilities())
1518    }
1519
1520    fn make_protected_client_station() -> Client {
1521        let connect_req = ParsedConnectRequest {
1522            selected_bss: fake_bss_description!(Wpa2, bssid: BSSID.to_array()),
1523            connect_failure_timeout: 10,
1524            auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
1525            sae_password: vec![],
1526            wep_key: None,
1527            security_ie: vec![
1528                0x30, 0x14, //  ID and len
1529                1, 0, //  version
1530                0x00, 0x0f, 0xac, 0x04, //  group data cipher suite
1531                0x01, 0x00, //  pairwise cipher suite count
1532                0x00, 0x0f, 0xac, 0x04, //  pairwise cipher suite list
1533                0x01, 0x00, //  akm suite count
1534                0x00, 0x0f, 0xac, 0x02, //  akm suite list
1535                0xa8, 0x04, //  rsn capabilities
1536            ],
1537            owe_public_key: None,
1538        };
1539        Client::new(connect_req, *IFACE_MAC, fake_client_capabilities())
1540    }
1541
1542    fn connect_conf_failure(result_code: fidl_ieee80211::StatusCode) -> fidl_mlme::ConnectConfirm {
1543        fidl_mlme::ConnectConfirm {
1544            peer_sta_address: BSSID.to_array(),
1545            result_code,
1546            association_id: 0,
1547            association_ies: vec![],
1548        }
1549    }
1550
1551    fn empty_association(sta: &mut BoundClient<'_, FakeDevice>) -> Association {
1552        let status_check_timeout =
1553            schedule_association_status_timeout(sta.sta.beacon_period(), &mut sta.ctx.timer);
1554        Association {
1555            controlled_port_open: false,
1556            aid: 0,
1557            assoc_resp_ies: vec![],
1558            ap_ht_op: None,
1559            ap_vht_op: None,
1560            lost_bss_counter: LostBssCounter::start(
1561                sta.sta.beacon_period(),
1562                DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT,
1563            ),
1564            qos: Qos::Disabled,
1565            status_check_timeout,
1566            signal_strength_average: SignalStrengthAverage::new(),
1567            block_ack_state: StateMachine::new(BlockAckState::from(State::new(Closed))),
1568        }
1569    }
1570
1571    fn fake_assoc_cfg() -> fidl_softmac::WlanAssociationConfig {
1572        fidl_softmac::WlanAssociationConfig {
1573            bssid: Some(BSSID.to_array()),
1574            aid: Some(42),
1575            channel: Some(fidl_ieee80211::WlanChannel {
1576                primary: 149,
1577                cbw: fidl_ieee80211::ChannelBandwidth::Cbw40,
1578                secondary80: 42,
1579            }),
1580            rates: None,
1581            capability_info: None,
1582            ..Default::default()
1583        }
1584    }
1585
1586    fn fake_deauth_req() -> wlan_sme::MlmeRequest {
1587        wlan_sme::MlmeRequest::Deauthenticate(fidl_mlme::DeauthenticateRequest {
1588            peer_sta_address: BSSID.to_array(),
1589            reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
1590        })
1591    }
1592
1593    fn open_authenticating(sta: &mut BoundClient<'_, FakeDevice>) -> Authenticating {
1594        let mut auth = Authenticating::new(AkmAlgorithm::OpenSupplicant);
1595        auth.algorithm.initiate(sta).expect("Failed to initiate open auth");
1596        auth
1597    }
1598
1599    #[fuchsia::test(allow_stalls = false)]
1600    async fn connect_authenticate_tx_failure() {
1601        let mut m = MockObjects::new().await;
1602        m.fake_device_state.lock().config.send_wlan_frame_fails = true;
1603        let mut ctx = m.make_ctx().await;
1604        let mut sta = make_client_station();
1605        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1606
1607        let state = Joined;
1608        let _state =
1609            state.start_authenticating(&mut sta).await.expect_err("should fail authenticating");
1610
1611        // Verify no event was queued up in the timer.
1612        assert!(m.time_stream.try_next().is_err());
1613
1614        // Verify MLME-CONNECT.confirm message was sent.
1615        let msg = m
1616            .fake_device_state
1617            .lock()
1618            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1619            .expect("expect msg");
1620        assert_eq!(
1621            msg,
1622            fidl_mlme::ConnectConfirm {
1623                peer_sta_address: BSSID.to_array(),
1624                result_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1625                association_id: 0,
1626                association_ies: vec![],
1627            }
1628        );
1629    }
1630
1631    #[fuchsia::test(allow_stalls = false)]
1632    async fn joined_no_authentication_algorithm() {
1633        let mut m = MockObjects::new().await;
1634        let mut ctx = m.make_ctx_with_bss().await;
1635        let connect_req = ParsedConnectRequest {
1636            selected_bss: fake_bss_description!(Open, bssid: BSSID.to_array()),
1637            connect_failure_timeout: 10,
1638            // use an unsupported AuthenticationType
1639            auth_type: fidl_mlme::AuthenticationTypes::SharedKey,
1640            sae_password: vec![],
1641            wep_key: None,
1642            security_ie: vec![],
1643            owe_public_key: None,
1644        };
1645        let mut sta = Client::new(connect_req, *IFACE_MAC, fake_client_capabilities());
1646        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1647        let state = Joined;
1648
1649        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1650        let _state =
1651            state.start_authenticating(&mut sta).await.expect_err("should fail authenticating");
1652
1653        // Verify MLME-CONNECT.confirm message was sent.
1654        let msg = m
1655            .fake_device_state
1656            .lock()
1657            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1658            .expect("expect msg");
1659        assert_eq!(
1660            msg,
1661            fidl_mlme::ConnectConfirm {
1662                peer_sta_address: [6, 6, 6, 6, 6, 6],
1663                result_code: fidl_ieee80211::StatusCode::UnsupportedAuthAlgorithm,
1664                association_id: 0,
1665                association_ies: vec![],
1666            }
1667        );
1668
1669        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1670    }
1671
1672    #[fuchsia::test(allow_stalls = false)]
1673    async fn authenticating_state_auth_rejected() {
1674        let mut m = MockObjects::new().await;
1675        let mut ctx = m.make_ctx_with_bss().await;
1676        let mut sta = make_client_station();
1677        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1678        let mut state = open_authenticating(&mut sta);
1679
1680        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1681        // Verify authentication failed.
1682        assert_matches!(
1683            state
1684                .on_auth_frame(
1685                    &mut sta,
1686                    mac::AuthFrame {
1687                        auth_hdr: mac::AuthHdr {
1688                            auth_alg_num: mac::AuthAlgorithmNumber::OPEN,
1689                            auth_txn_seq_num: 2,
1690                            status_code: fidl_ieee80211::StatusCode::NotInSameBss.into(),
1691                        }
1692                        .as_bytes_ref(),
1693                        elements: &[],
1694                    },
1695                )
1696                .await,
1697            AuthProgress::Failed
1698        );
1699
1700        // Verify MLME-CONNECT.confirm message was sent.
1701        let msg = m
1702            .fake_device_state
1703            .lock()
1704            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1705            .expect("expect msg");
1706        assert_eq!(
1707            msg,
1708            fidl_mlme::ConnectConfirm {
1709                peer_sta_address: BSSID.to_array(),
1710                result_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1711                association_id: 0,
1712                association_ies: vec![],
1713            }
1714        );
1715        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1716    }
1717
1718    #[fuchsia::test(allow_stalls = false)]
1719    async fn authenticating_state_deauth_frame() {
1720        let mut m = MockObjects::new().await;
1721        let mut ctx = m.make_ctx_with_bss().await;
1722        let mut sta = make_client_station();
1723        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1724        let mut state = open_authenticating(&mut sta);
1725
1726        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1727        state
1728            .on_deauth_frame(
1729                &mut sta,
1730                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::NoMoreStas.into() },
1731            )
1732            .await;
1733
1734        // Verify MLME-CONNECT.confirm message was sent.
1735        let msg = m
1736            .fake_device_state
1737            .lock()
1738            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1739            .expect("expect msg");
1740        assert_eq!(
1741            msg,
1742            fidl_mlme::ConnectConfirm {
1743                peer_sta_address: BSSID.to_array(),
1744                result_code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
1745                association_id: 0,
1746                association_ies: vec![],
1747            }
1748        );
1749        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1750    }
1751
1752    #[fuchsia::test(allow_stalls = false)]
1753    async fn associating_success_unprotected() {
1754        let mut m = MockObjects::new().await;
1755        let mut ctx = m.make_ctx_with_bss().await;
1756        let mut sta = make_client_station();
1757        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1758
1759        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1760        let mut state = Associating::default();
1761        let assoc_resp_ies = fake_bss_description!(Wpa2, ies_overrides: IesOverrides::new()
1762            .set(IeType::HT_CAPABILITIES, ie::fake_ht_cap_bytes().to_vec())
1763            .set(IeType::VHT_CAPABILITIES, ie::fake_vht_cap_bytes().to_vec())
1764        )
1765        .ies()
1766        .to_vec();
1767        let Association { aid, controlled_port_open, .. } = state
1768            .on_assoc_resp_frame(
1769                &mut sta,
1770                mac::AssocRespFrame {
1771                    assoc_resp_hdr: mac::AssocRespHdr {
1772                        aid: 42,
1773                        capabilities: mac::CapabilityInfo(52),
1774                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1775                    }
1776                    .as_bytes_ref(),
1777                    elements: &assoc_resp_ies[..],
1778                },
1779            )
1780            .await
1781            .expect("failed processing association response frame");
1782        assert_eq!(aid, 42);
1783        assert_eq!(true, controlled_port_open);
1784
1785        // Verify MLME-CONNECT.confirm message was sent.
1786        let msg = m
1787            .fake_device_state
1788            .lock()
1789            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1790            .expect("no message");
1791        assert_eq!(
1792            msg,
1793            fidl_mlme::ConnectConfirm {
1794                peer_sta_address: BSSID.to_array(),
1795                result_code: fidl_ieee80211::StatusCode::Success,
1796                association_id: 42,
1797                association_ies: assoc_resp_ies,
1798            }
1799        );
1800        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1801    }
1802
1803    #[fuchsia::test(allow_stalls = false)]
1804    async fn associating_success_protected() {
1805        let mut m = MockObjects::new().await;
1806        let mut ctx = m.make_ctx_with_bss().await;
1807        let mut sta = make_protected_client_station();
1808        sta.client_capabilities.0.capability_info =
1809            mac::CapabilityInfo(0).with_ess(true).with_ibss(true);
1810        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1811        let mut state = Associating::default();
1812
1813        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1814        let assoc_resp_ies =
1815            fake_bss_description!(Wpa2, bssid: BSSID.to_array(), ies_overrides: IesOverrides::new()
1816                .set(IeType::HT_CAPABILITIES, ie::fake_ht_cap_bytes().to_vec())
1817                .set(IeType::VHT_CAPABILITIES, ie::fake_vht_cap_bytes().to_vec())
1818            )
1819            .ies()
1820            .to_vec();
1821        let Association { aid, controlled_port_open, .. } = state
1822            .on_assoc_resp_frame(
1823                &mut sta,
1824                mac::AssocRespFrame {
1825                    assoc_resp_hdr: mac::AssocRespHdr {
1826                        aid: 42,
1827                        capabilities: mac::CapabilityInfo(0).with_ibss(true).with_cf_pollable(true),
1828                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1829                    }
1830                    .as_bytes_ref(),
1831                    elements: &assoc_resp_ies[..],
1832                },
1833            )
1834            .await
1835            .expect("failed processing association response frame");
1836        assert_eq!(aid, 42);
1837        assert_eq!(false, controlled_port_open);
1838
1839        // Verify association context is set
1840        assert_eq!(m.fake_device_state.lock().assocs.len(), 1);
1841
1842        let assoc_cfg = m
1843            .fake_device_state
1844            .lock()
1845            .assocs
1846            .get(&(*BSSID).into())
1847            .expect("expect assoc ctx to be set")
1848            .clone();
1849        assert_eq!(assoc_cfg.aid, Some(42));
1850        assert_eq!(assoc_cfg.qos, Some(true));
1851        assert_eq!(
1852            assoc_cfg.rates,
1853            Some(vec![0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c])
1854        );
1855        assert_eq!(assoc_cfg.capability_info, Some(2));
1856        assert!(assoc_cfg.ht_cap.is_some());
1857        assert!(assoc_cfg.vht_cap.is_some());
1858        assert!(assoc_cfg.ht_op.is_some());
1859        assert!(assoc_cfg.vht_op.is_some());
1860
1861        // Verify MLME-CONNECT.confirm message was sent.
1862        let msg = m
1863            .fake_device_state
1864            .lock()
1865            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1866            .expect("no message");
1867        assert_eq!(
1868            msg,
1869            fidl_mlme::ConnectConfirm {
1870                peer_sta_address: BSSID.to_array(),
1871                result_code: fidl_ieee80211::StatusCode::Success,
1872                association_id: 42,
1873                association_ies: assoc_resp_ies,
1874            }
1875        );
1876        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1877    }
1878
1879    #[fuchsia::test(allow_stalls = false)]
1880    async fn associating_failure_due_to_failed_status_code() {
1881        let mut m = MockObjects::new().await;
1882        let mut ctx = m.make_ctx_with_bss().await;
1883        let mut sta = make_client_station();
1884        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1885
1886        let mut state = Associating::default();
1887
1888        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1889        // Verify authentication was considered successful.
1890        state
1891            .on_assoc_resp_frame(
1892                &mut sta,
1893                mac::AssocRespFrame {
1894                    assoc_resp_hdr: mac::AssocRespHdr {
1895                        aid: 42,
1896                        capabilities: mac::CapabilityInfo(52),
1897                        status_code: fidl_ieee80211::StatusCode::NotInSameBss.into(),
1898                    }
1899                    .as_bytes_ref(),
1900                    elements: &[][..],
1901                },
1902            )
1903            .await
1904            .expect_err("expected failure processing association response frame");
1905
1906        // Verify MLME-CONNECT.confirm message was sent.
1907        let msg = m
1908            .fake_device_state
1909            .lock()
1910            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1911            .expect("no message");
1912        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::NotInSameBss));
1913        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1914    }
1915
1916    #[fuchsia::test(allow_stalls = false)]
1917    async fn associating_failure_due_to_incompatibility() {
1918        let mut m = MockObjects::new().await;
1919        let mut ctx = m.make_ctx_with_bss().await;
1920        let mut sta = make_client_station();
1921        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1922
1923        let mut state = Associating::default();
1924
1925        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1926        state
1927            .on_assoc_resp_frame(
1928                &mut sta,
1929                mac::AssocRespFrame {
1930                    assoc_resp_hdr: mac::AssocRespHdr {
1931                        aid: 42,
1932                        capabilities: mac::CapabilityInfo(52),
1933                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1934                    }
1935                    .as_bytes_ref(),
1936                    elements: fake_bss_description!(Wpa2, rates: vec![0x81]).ies(),
1937                },
1938            )
1939            .await
1940            .expect_err("expected failure processing association response frame");
1941
1942        // Verify MLME-CONNECT.confirm message was sent.
1943        let msg = m
1944            .fake_device_state
1945            .lock()
1946            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1947            .expect("no message");
1948        assert_eq!(
1949            msg,
1950            connect_conf_failure(fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch)
1951        );
1952        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1953    }
1954
1955    #[fuchsia::test(allow_stalls = false)]
1956    async fn associating_deauth_frame() {
1957        let mut m = MockObjects::new().await;
1958        let mut ctx = m.make_ctx_with_bss().await;
1959        let mut sta = make_client_station();
1960        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1961
1962        let mut state = Associating::default();
1963
1964        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1965        state
1966            .on_deauth_frame(
1967                &mut sta,
1968                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
1969            )
1970            .await;
1971
1972        // Verify MLME-CONNECT.confirm message was sent.
1973        let msg = m
1974            .fake_device_state
1975            .lock()
1976            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1977            .expect("no message");
1978        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc));
1979        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1980    }
1981
1982    #[fuchsia::test(allow_stalls = false)]
1983    async fn associating_disassociation() {
1984        let mut m = MockObjects::new().await;
1985        let mut ctx = m.make_ctx_with_bss().await;
1986        let mut sta = make_client_station();
1987        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1988
1989        let mut state = Associating::default();
1990
1991        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1992        state.on_disassoc_frame(
1993            &mut sta,
1994            &mac::DisassocHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
1995        );
1996
1997        // Verify MLME-CONNECT.confirm message was sent.
1998        let msg = m
1999            .fake_device_state
2000            .lock()
2001            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2002            .expect("no message");
2003        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc));
2004        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2005    }
2006
2007    fn mock_rx_info<'a>(client: &BoundClient<'a, FakeDevice>) -> fidl_softmac::WlanRxInfo {
2008        let channel = client.channel_state.get_main_channel().unwrap();
2009        MockWlanRxInfo::with_channel(channel).into()
2010    }
2011
2012    #[fuchsia::test(allow_stalls = false)]
2013    async fn associated_block_ack_frame() {
2014        let mut mock = MockObjects::new().await;
2015        let mut ctx = mock.make_ctx().await;
2016        let mut station = make_client_station();
2017        let mut client = station.bind(&mut ctx, &mut mock.scanner, &mut mock.channel_state);
2018
2019        let frame = {
2020            let mut buffer = [0u8; ADDBA_REQ_FRAME_LEN];
2021            let writer = BufferWriter::new(&mut buffer[..]);
2022            let mut writer = append_frame_to!(
2023                writer,
2024                {
2025                    headers: {
2026                        mac::MgmtHdr: &mgmt_writer::mgmt_hdr_from_ap(
2027                            mac::FrameControl(0)
2028                                .with_frame_type(mac::FrameType::MGMT)
2029                                .with_mgmt_subtype(mac::MgmtSubtype::ACTION),
2030                            client.sta.iface_mac,
2031                            client.sta.bssid(),
2032                            mac::SequenceControl(0)
2033                                .with_seq_num(client.ctx.seq_mgr.next_sns1(&client.sta.bssid().into()) as u16),
2034                        ),
2035                    },
2036                }
2037            )
2038            .unwrap();
2039            write_addba_req_body(&mut writer, 1).unwrap();
2040            buffer
2041        };
2042
2043        let state = States::from(statemachine::testing::new_state(Associated(empty_association(
2044            &mut client,
2045        ))));
2046        let rx_info = mock_rx_info(&client);
2047        match state.on_mac_frame(&mut client, &frame[..], rx_info, 0.into()).await {
2048            States::Associated(state) => {
2049                let (_, associated) = state.release_data();
2050                // TODO(https://fxbug.dev/42104687): Handle BlockAck frames. The following code has been
2051                //                        altered as part of a fix for https://fxbug.dev/42180615. This check
2052                //                        should ensure that the state has transitioned to
2053                //                        `Established`, but since the state machine has been
2054                //                        disabled it instead checks that the state has remained
2055                //                        `Closed`.
2056                match *associated.0.block_ack_state.as_ref() {
2057                    BlockAckState::Closed(_) => {}
2058                    _ => panic!("client has transitioned BlockAck"),
2059                }
2060            }
2061            _ => panic!("client no longer associated"),
2062        }
2063    }
2064
2065    #[fuchsia::test(allow_stalls = false)]
2066    async fn associated_deauth_frame() {
2067        let mut m = MockObjects::new().await;
2068        let mut ctx = m.make_ctx_with_bss().await;
2069        let mut sta = make_client_station();
2070        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2071        let mut state = Associated(empty_association(&mut sta));
2072
2073        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2074
2075        // Association configuration will be cleared when MLME receives deauth frame.
2076        sta.ctx
2077            .device
2078            .notify_association_complete(fake_assoc_cfg())
2079            .await
2080            .expect("valid assoc_cfg should succeed");
2081        assert_eq!(1, m.fake_device_state.lock().assocs.len());
2082
2083        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
2084        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
2085
2086        let _joined = state
2087            .on_deauth_frame(
2088                &mut sta,
2089                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
2090            )
2091            .await;
2092
2093        // Verify MLME-ASSOCIATE.confirm message was sent.
2094        let msg = m
2095            .fake_device_state
2096            .lock()
2097            .next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
2098            .expect("no message");
2099        assert_eq!(
2100            msg,
2101            fidl_mlme::DeauthenticateIndication {
2102                peer_sta_address: BSSID.to_array(),
2103                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2104                locally_initiated: false,
2105            }
2106        );
2107        // Verify ethernet port is shut down.
2108        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
2109        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2110    }
2111
2112    #[fuchsia::test(allow_stalls = false)]
2113    async fn associated_disassociation() {
2114        let mut m = MockObjects::new().await;
2115        let mut ctx = m.make_ctx_with_bss().await;
2116        let mut sta = make_client_station();
2117        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2118        let mut state = Associated(empty_association(&mut sta));
2119
2120        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2121        state.0.controlled_port_open = true;
2122
2123        sta.ctx
2124            .device
2125            .notify_association_complete(fake_assoc_cfg())
2126            .await
2127            .expect("valid assoc_cfg should succeed");
2128        assert_eq!(1, m.fake_device_state.lock().assocs.len());
2129
2130        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
2131        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
2132
2133        let _authenticated = state
2134            .on_disassoc_frame(
2135                &mut sta,
2136                &mac::DisassocHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
2137            )
2138            .await;
2139
2140        // Verify MLME-ASSOCIATE.confirm message was sent.
2141        let msg = m
2142            .fake_device_state
2143            .lock()
2144            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
2145            .expect("no message");
2146        assert_eq!(
2147            msg,
2148            fidl_mlme::DisassociateIndication {
2149                peer_sta_address: BSSID.to_array(),
2150                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2151                locally_initiated: false,
2152            }
2153        );
2154
2155        // Verify ethernet port is shut down.
2156        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
2157        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2158    }
2159
2160    #[fuchsia::test(allow_stalls = false)]
2161    async fn associated_move_data_closed_controlled_port() {
2162        let mut m = MockObjects::new().await;
2163        let mut ctx = m.make_ctx().await;
2164        let mut sta = make_client_station();
2165        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2166        let state = Associated(empty_association(&mut sta));
2167
2168        let bytes = make_data_frame_single_llc(None, None);
2169        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2170        state.on_data_frame(&mut sta, data_frame, 0.into());
2171
2172        // Verify data frame was dropped.
2173        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2174    }
2175
2176    #[fuchsia::test(allow_stalls = false)]
2177    async fn associated_move_data_opened_controlled_port() {
2178        let mut m = MockObjects::new().await;
2179        let mut ctx = m.make_ctx().await;
2180        let mut sta = make_client_station();
2181        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2182        let state =
2183            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2184
2185        let bytes = make_data_frame_single_llc(None, None);
2186        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2187        state.on_data_frame(&mut sta, data_frame, 0.into());
2188
2189        // Verify data frame was processed.
2190        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
2191        #[rustfmt::skip]
2192        assert_eq!(m.fake_device_state.lock().eth_queue[0], [
2193            3, 3, 3, 3, 3, 3, // dst_addr
2194            4, 4, 4, 4, 4, 4, // src_addr
2195            9, 10, // ether_type
2196            11, 11, 11, // payload
2197        ]);
2198    }
2199
2200    #[fuchsia::test(allow_stalls = false)]
2201    async fn associated_skip_empty_data() {
2202        let mut m = MockObjects::new().await;
2203        let mut ctx = m.make_ctx().await;
2204        let mut sta = make_client_station();
2205        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2206        let state =
2207            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2208
2209        let bytes = make_data_frame_single_llc_payload(None, None, &[]);
2210        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2211        state.on_data_frame(&mut sta, data_frame, 0.into());
2212
2213        // Verify data frame was discarded.
2214        assert!(m.fake_device_state.lock().eth_queue.is_empty());
2215    }
2216
2217    #[test_case(true, true; "port open and protected")]
2218    #[test_case(false, true; "port closed and protected")]
2219    #[test_case(true, false; "port open and unprotected")]
2220    #[test_case(false, false; "port closed and unprotected (not a typical state)")]
2221    #[fuchsia::test(allow_stalls = false)]
2222    async fn associated_send_keep_alive_after_null_data_frame(
2223        controlled_port_open: bool,
2224        protected: bool,
2225    ) {
2226        let mut m = MockObjects::new().await;
2227        let mut ctx = m.make_ctx().await;
2228        let mut sta =
2229            if protected { make_protected_client_station() } else { make_client_station() };
2230        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2231        let state = Associated(Association { controlled_port_open, ..empty_association(&mut sta) });
2232
2233        let bytes = make_null_data_frame();
2234        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2235        state.on_data_frame(&mut sta, data_frame, 0.into());
2236
2237        // Verify data frame was not forwarded up.
2238        assert!(m.fake_device_state.lock().eth_queue.is_empty());
2239        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2240        let bytes = &m.fake_device_state.lock().wlan_queue[0].0;
2241        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2242        let frame_ctrl = data_frame.frame_ctrl();
2243        assert_eq!(frame_ctrl.to_ds(), true);
2244        assert_eq!(frame_ctrl.from_ds(), false);
2245        assert!(data_frame.body.is_empty());
2246    }
2247
2248    #[fuchsia::test(allow_stalls = false)]
2249    async fn associated_handle_eapol_closed_controlled_port() {
2250        let mut m = MockObjects::new().await;
2251        let mut ctx = m.make_ctx().await;
2252        let mut sta = make_protected_client_station();
2253        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2254        let state = Associated(empty_association(&mut sta));
2255
2256        let (src_addr, dst_addr, bytes) = make_eapol_frame(*IFACE_MAC);
2257        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2258        state.on_data_frame(&mut sta, data_frame, 0.into());
2259
2260        // Verify EAPOL frame was not sent to netstack.
2261        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2262
2263        // Verify EAPoL frame was sent to SME.
2264        let eapol_ind = m
2265            .fake_device_state
2266            .lock()
2267            .next_mlme_msg::<fidl_mlme::EapolIndication>()
2268            .expect("error reading EAPOL.indication");
2269        assert_eq!(
2270            eapol_ind,
2271            fidl_mlme::EapolIndication {
2272                src_addr: src_addr.to_array(),
2273                dst_addr: dst_addr.to_array(),
2274                data: EAPOL_PDU.to_vec()
2275            }
2276        );
2277    }
2278
2279    #[fuchsia::test(allow_stalls = false)]
2280    async fn associated_handle_eapol_open_controlled_port() {
2281        let mut m = MockObjects::new().await;
2282        let mut ctx = m.make_ctx().await;
2283        let mut sta = make_protected_client_station();
2284        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2285        let state = Associated(empty_association(&mut sta));
2286
2287        let (src_addr, dst_addr, bytes) = make_eapol_frame(*IFACE_MAC);
2288        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2289        state.on_data_frame(&mut sta, data_frame, 0.into());
2290
2291        // Verify EAPOL frame was not sent to netstack.
2292        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2293
2294        // Verify EAPoL frame was sent to SME.
2295        let eapol_ind = m
2296            .fake_device_state
2297            .lock()
2298            .next_mlme_msg::<fidl_mlme::EapolIndication>()
2299            .expect("error reading EAPOL.indication");
2300        assert_eq!(
2301            eapol_ind,
2302            fidl_mlme::EapolIndication {
2303                src_addr: src_addr.to_array(),
2304                dst_addr: dst_addr.to_array(),
2305                data: EAPOL_PDU.to_vec()
2306            }
2307        );
2308    }
2309
2310    #[fuchsia::test(allow_stalls = false)]
2311    async fn associated_handle_amsdus_open_controlled_port() {
2312        let mut m = MockObjects::new().await;
2313        let mut ctx = m.make_ctx().await;
2314        let mut sta = make_protected_client_station();
2315        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2316        let state =
2317            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2318
2319        let bytes = make_data_frame_amsdu();
2320        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2321        state.on_data_frame(&mut sta, data_frame, 0.into());
2322
2323        let queue = &m.fake_device_state.lock().eth_queue;
2324        assert_eq!(queue.len(), 2);
2325        #[rustfmt::skip]
2326        let mut expected_first_eth_frame = vec![
2327            0x78, 0x8a, 0x20, 0x0d, 0x67, 0x03, // dst_addr
2328            0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xab, // src_addr
2329            0x08, 0x00, // ether_type
2330        ];
2331        expected_first_eth_frame.extend_from_slice(MSDU_1_PAYLOAD);
2332        assert_eq!(queue[0], &expected_first_eth_frame[..]);
2333        #[rustfmt::skip]
2334        let mut expected_second_eth_frame = vec![
2335            0x78, 0x8a, 0x20, 0x0d, 0x67, 0x04, // dst_addr
2336            0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xac, // src_addr
2337            0x08, 0x01, // ether_type
2338        ];
2339        expected_second_eth_frame.extend_from_slice(MSDU_2_PAYLOAD);
2340        assert_eq!(queue[1], &expected_second_eth_frame[..]);
2341    }
2342
2343    #[fuchsia::test(allow_stalls = false)]
2344    async fn associated_request_bu_data_frame() {
2345        let mut m = MockObjects::new().await;
2346        let mut ctx = m.make_ctx().await;
2347        let mut sta = make_client_station();
2348        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2349        let state = Associated(Association {
2350            aid: 42,
2351            controlled_port_open: true,
2352            ..empty_association(&mut sta)
2353        });
2354
2355        let mut bytes = make_data_frame_single_llc(None, None);
2356        let mut data_frame = mac::DataFrame::parse(bytes.as_mut_slice(), false).unwrap();
2357        data_frame.fixed_fields.frame_ctrl =
2358            data_frame.fixed_fields.frame_ctrl.with_more_data(true);
2359        state.on_data_frame(&mut sta, data_frame, 0.into());
2360
2361        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2362        #[rustfmt::skip]
2363        assert_eq!(&m
2364            .fake_device_state.lock().wlan_queue[0].0[..], &[
2365            // Frame Control:
2366            0b10100100, 0b00000000, // FC
2367            42, 0b11_000000, // Id
2368            6, 6, 6, 6, 6, 6, // addr1
2369            3, 3, 3, 3, 3, 3, // addr2
2370        ][..]);
2371    }
2372
2373    #[fuchsia::test(allow_stalls = false)]
2374    async fn associated_request_bu_mgmt_frame() {
2375        let mut m = MockObjects::new().await;
2376        let mut ctx = m.make_ctx().await;
2377        let mut sta = make_client_station();
2378        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2379        let state = Associated(Association {
2380            aid: 42,
2381            controlled_port_open: true,
2382            ..empty_association(&mut sta)
2383        });
2384
2385        state.on_any_mgmt_frame(
2386            &mut sta,
2387            &mac::MgmtHdr {
2388                frame_ctrl: mac::FrameControl(0)
2389                    .with_frame_type(mac::FrameType::MGMT)
2390                    .with_mgmt_subtype(mac::MgmtSubtype::BEACON)
2391                    .with_more_data(true),
2392                duration: 0,
2393                addr1: [3; 6].into(),
2394                addr2: (*BSSID).into(),
2395                addr3: (*BSSID).into(),
2396                seq_ctrl: mac::SequenceControl(0),
2397            },
2398        );
2399
2400        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2401        #[rustfmt::skip]
2402        assert_eq!(&m
2403            .fake_device_state.lock().wlan_queue[0].0[..], &[
2404            // Frame Control:
2405            0b10100100, 0b00000000, // FC
2406            42, 0b11_000000, // Id
2407            6, 6, 6, 6, 6, 6, // addr1
2408            3, 3, 3, 3, 3, 3, // addr2
2409        ][..]);
2410    }
2411
2412    #[fuchsia::test(allow_stalls = false)]
2413    async fn associated_no_bu_request() {
2414        let mut m = MockObjects::new().await;
2415        let mut ctx = m.make_ctx().await;
2416        let mut sta = make_client_station();
2417        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2418
2419        // Closed Controlled port
2420        let state = Associated(empty_association(&mut sta));
2421        let mut bytes = make_data_frame_single_llc(None, None);
2422        let mut data_frame = mac::DataFrame::parse(bytes.as_mut_slice(), false).unwrap();
2423        data_frame.fixed_fields.frame_ctrl =
2424            data_frame.fixed_fields.frame_ctrl.with_more_data(true);
2425        state.on_data_frame(&mut sta, data_frame, 0.into());
2426        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
2427
2428        // Foreign management frame
2429        let state = States::from(statemachine::testing::new_state(Associated(Association {
2430            controlled_port_open: true,
2431            ..empty_association(&mut sta)
2432        })));
2433        #[rustfmt::skip]
2434        let beacon = vec![
2435            // Mgmt Header:
2436            0b1000_00_00, 0b00100000, // Frame Control
2437            0, 0, // Duration
2438            3, 3, 3, 3, 3, 3, // Addr1
2439            7, 7, 7, 7, 7, 7, // Addr2
2440            5, 5, 5, 5, 5, 5, // Addr3
2441            0x10, 0, // Sequence Control
2442            // Omit IEs
2443        ];
2444        let rx_info = mock_rx_info(&sta);
2445        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
2446        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
2447    }
2448
2449    #[fuchsia::test(allow_stalls = false)]
2450    async fn associated_drop_foreign_data_frames() {
2451        let mut m = MockObjects::new().await;
2452        let mut ctx = m.make_ctx().await;
2453        let mut sta = make_client_station();
2454        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2455
2456        // Foreign data frame
2457        let state = States::from(statemachine::testing::new_state(Associated(Association {
2458            aid: 42,
2459            controlled_port_open: true,
2460            ..empty_association(&mut sta)
2461        })));
2462        let fc = mac::FrameControl(0)
2463            .with_frame_type(mac::FrameType::DATA)
2464            .with_data_subtype(mac::DataSubtype(0))
2465            .with_from_ds(true);
2466        let fc = fc.0.to_le_bytes();
2467        // Send data frame from an address other than the BSSID([6u8; 6]).
2468        let bytes = vec![
2469            // Data Header
2470            fc[0], fc[1], // fc
2471            2, 2, // duration
2472            3, 3, 3, 3, 3, 3, // addr1
2473            4, 4, 4, 4, 4, 4, // addr2
2474            5, 5, 5, 5, 5, 5, // addr3
2475            6, 6, // sequence control
2476            // LLC Header
2477            7, 7, 7, // DSAP, SSAP & control
2478            8, 8, 8, // OUI
2479            9, 10, // eth type
2480            // Trailing bytes
2481            11, 11, 11,
2482        ];
2483        let rx_info = mock_rx_info(&sta);
2484        state.on_mac_frame(&mut sta, &bytes[..], rx_info, 0.into()).await;
2485        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2486    }
2487
2488    #[fuchsia::test(allow_stalls = false)]
2489    async fn state_transitions_joined_state_reconnect_denied() {
2490        let mut m = MockObjects::new().await;
2491        let mut ctx = m.make_ctx().await;
2492        let mut sta = make_client_station();
2493        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2494        let mut state = States::from(statemachine::testing::new_state(Joined));
2495
2496        // (sme->mlme) Send a reconnect request
2497        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2498            peer_sta_address: [1, 2, 3, 4, 5, 6],
2499        });
2500        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2501
2502        assert_matches!(state, States::Joined(_), "not in joined state");
2503
2504        // Verify MLME-CONNECT.confirm message was sent.
2505        let msg = m
2506            .fake_device_state
2507            .lock()
2508            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2509            .expect("expect msg");
2510        assert_eq!(
2511            msg,
2512            fidl_mlme::ConnectConfirm {
2513                peer_sta_address: [1, 2, 3, 4, 5, 6],
2514                result_code: fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
2515                association_id: 0,
2516                association_ies: vec![],
2517            }
2518        );
2519    }
2520
2521    #[fuchsia::test(allow_stalls = false)]
2522    async fn state_transitions_authing_success() {
2523        let mut m = MockObjects::new().await;
2524        let mut ctx = m.make_ctx().await;
2525        let mut sta = make_client_station();
2526        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2527        let mut state =
2528            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2529
2530        // Successful: Joined > Authenticating > Associating
2531        #[rustfmt::skip]
2532        let auth_resp_success = vec![
2533            // Mgmt Header:
2534            0b1011_00_00, 0b00000000, // Frame Control
2535            0, 0, // Duration
2536            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2537            3, 3, 3, 3, 3, 3, // Addr2
2538            6, 6, 6, 6, 6, 6, // Addr3
2539            0x10, 0, // Sequence Control
2540            // Auth Header:
2541            0, 0, // Algorithm Number (Open)
2542            2, 0, // Txn Sequence Number
2543            0, 0, // Status Code
2544        ];
2545        let rx_info = mock_rx_info(&sta);
2546        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2547        assert_matches!(state, States::Associating(_), "not in associating state");
2548    }
2549
2550    #[fuchsia::test(allow_stalls = false)]
2551    async fn state_transitions_authing_failure() {
2552        let mut m = MockObjects::new().await;
2553        let mut ctx = m.make_ctx_with_bss().await;
2554        let mut sta = make_client_station();
2555        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2556        let mut state =
2557            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2558
2559        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2560        // Failure: Joined > Authenticating > Joined
2561        #[rustfmt::skip]
2562        let auth_resp_failure = vec![
2563            // Mgmt Header:
2564            0b1011_00_00, 0b00000000, // Frame Control
2565            0, 0, // Duration
2566            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2567            3, 3, 3, 3, 3, 3, // Addr2
2568            6, 6, 6, 6, 6, 6, // Addr3
2569            0x10, 0, // Sequence Control
2570            // Auth Header:
2571            0, 0, // Algorithm Number (Open)
2572            2, 0, // Txn Sequence Number
2573            42, 0, // Status Code
2574        ];
2575        let rx_info = mock_rx_info(&sta);
2576        state = state.on_mac_frame(&mut sta, &auth_resp_failure[..], rx_info, 0.into()).await;
2577        assert_matches!(state, States::Joined(_), "not in joined state");
2578        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2579    }
2580
2581    #[fuchsia::test(allow_stalls = false)]
2582    async fn state_transitions_authing_deauth() {
2583        let mut m = MockObjects::new().await;
2584        let mut ctx = m.make_ctx_with_bss().await;
2585        let mut sta = make_client_station();
2586        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2587        let mut state =
2588            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2589
2590        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2591        // Deauthenticate: Authenticating > Joined
2592        #[rustfmt::skip]
2593        let deauth = vec![
2594            // Mgmt Header:
2595            0b1100_00_00, 0b00000000, // Frame Control
2596            0, 0, // Duration
2597            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2598            3, 3, 3, 3, 3, 3, // Addr2
2599            6, 6, 6, 6, 6, 6, // Addr3
2600            0x10, 0, // Sequence Control
2601            // Deauth Header:
2602            5, 0, // Algorithm Number (Open)
2603        ];
2604        let rx_info = mock_rx_info(&sta);
2605        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
2606        assert_matches!(state, States::Joined(_), "not in joined state");
2607        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2608    }
2609
2610    #[fuchsia::test(allow_stalls = false)]
2611    async fn state_transitions_foreign_auth_resp() {
2612        let mut m = MockObjects::new().await;
2613        let mut ctx = m.make_ctx().await;
2614        let mut sta = make_client_station();
2615        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2616        let mut state =
2617            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2618
2619        // Send foreign auth response. State should not change.
2620        #[rustfmt::skip]
2621        let auth_resp_success = vec![
2622            // Mgmt Header:
2623            0b1011_00_00, 0b00000000, // Frame Control
2624            0, 0, // Duration
2625            5, 5, 5, 5, 5, 5, // Addr1
2626            3, 3, 3, 3, 3, 3, // Addr2
2627            5, 5, 5, 5, 5, 5, // Addr3
2628            0x10, 0, // Sequence Control
2629            // Auth Header:
2630            0, 0, // Algorithm Number (Open)
2631            2, 0, // Txn Sequence Number
2632            0, 0, // Status Code
2633        ];
2634        let rx_info = mock_rx_info(&sta);
2635        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2636        assert_matches!(state, States::Authenticating(_), "not in authenticating state");
2637
2638        // Verify that an authentication response from the joined BSS still moves the Client
2639        // forward.
2640        #[rustfmt::skip]
2641        let auth_resp_success = vec![
2642            // Mgmt Header:
2643            0b1011_00_00, 0b00000000, // Frame Control
2644            0, 0, // Duration
2645            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2646            3, 3, 3, 3, 3, 3, // Addr2
2647            6, 6, 6, 6, 6, 6, // Addr3
2648            0x10, 0, // Sequence Control
2649            // Auth Header:
2650            0, 0, // Algorithm Number (Open)
2651            2, 0, // Txn Sequence Number
2652            0, 0, // Status Code
2653        ];
2654        let rx_info = mock_rx_info(&sta);
2655        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2656        assert_matches!(state, States::Associating(_), "not in associating state");
2657    }
2658
2659    #[fuchsia::test(allow_stalls = false)]
2660    async fn state_transitions_authing_state_reconnect_denied() {
2661        let mut m = MockObjects::new().await;
2662        let mut ctx = m.make_ctx().await;
2663        let mut sta = make_client_station();
2664        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2665        let mut state =
2666            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2667
2668        // (sme->mlme) Send a reconnect request
2669        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2670            peer_sta_address: [1, 2, 3, 4, 5, 6],
2671        });
2672        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2673
2674        assert_matches!(state, States::Authenticating(_), "not in authenticating state");
2675
2676        // Verify MLME-CONNECT.confirm message was sent.
2677        let msg = m
2678            .fake_device_state
2679            .lock()
2680            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2681            .expect("expect msg");
2682        assert_eq!(
2683            msg,
2684            fidl_mlme::ConnectConfirm {
2685                peer_sta_address: [1, 2, 3, 4, 5, 6],
2686                result_code: fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
2687                association_id: 0,
2688                association_ies: vec![],
2689            }
2690        );
2691    }
2692
2693    #[fuchsia::test(allow_stalls = false)]
2694    async fn state_transitions_authing_state_wrong_algorithm() {
2695        let mut m = MockObjects::new().await;
2696        let mut ctx = m.make_ctx().await;
2697        let mut sta = make_client_station();
2698        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2699        let mut state =
2700            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2701
2702        #[rustfmt::skip]
2703        let auth_resp_wrong = vec![
2704            // Mgmt Header:
2705            0b1011_00_00, 0b00000000, // Frame Control
2706            0, 0, // Duration
2707            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2708            3, 3, 3, 3, 3, 3, // Addr2
2709            6, 6, 6, 6, 6, 6, // Addr3
2710            0x10, 0, // Sequence Control
2711            // Auth Header:
2712            8, 0, // Algorithm Number (wrong algorithm: SAE)
2713            2, 0, // Txn Sequence Number
2714            0, 0, // Status Code
2715        ];
2716        let rx_info = mock_rx_info(&sta);
2717        state = state.on_mac_frame(&mut sta, &auth_resp_wrong[..], rx_info, 0.into()).await;
2718        assert_matches!(state, States::Joined(_), "not in joined state");
2719        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2720    }
2721
2722    #[fuchsia::test(allow_stalls = false)]
2723    async fn state_transitions_associng_success() {
2724        let mut m = MockObjects::new().await;
2725        let mut ctx = m.make_ctx().await;
2726        let mut sta = make_client_station();
2727        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2728        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2729
2730        // Successful: Associating > Associated
2731        #[rustfmt::skip]
2732        let assoc_resp_success = vec![
2733            // Mgmt Header:
2734            0b0001_00_00, 0b00000000, // Frame Control
2735            0, 0, // Duration
2736            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2737            3, 3, 3, 3, 3, 3, // Addr2
2738            6, 6, 6, 6, 6, 6, // Addr3
2739            0x10, 0, // Sequence Control
2740            // Assoc Resp Header:
2741            0, 0, // Capabilities
2742            0, 0, // Status Code
2743            0, 0, // AID
2744            // IEs
2745            // Basic Rates
2746            0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
2747            // HT Capabilities
2748            0x2d, 0x1a, 0xef, 0x09, // HT capabilities info
2749            0x17, // A-MPDU parameters
2750            0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2751            // VHT Capabilities
2752            0xbf, 0x0c, 0x91, 0x59, 0x82, 0x0f, // VHT capabilities info
2753            0xea, 0xff, 0x00, 0x00, 0xea, 0xff, 0x00, 0x00, // VHT supported MCS set
2754        ];
2755        let rx_info = mock_rx_info(&sta);
2756        state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], rx_info, 0.into()).await;
2757        assert_matches!(state, States::Associated(_), "not in associated state");
2758    }
2759
2760    #[fuchsia::test(allow_stalls = false)]
2761    async fn state_transitions_associng_failure() {
2762        let mut m = MockObjects::new().await;
2763        let mut ctx = m.make_ctx_with_bss().await;
2764        let mut sta = make_client_station();
2765        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2766        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2767
2768        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2769        // Failure: Associating > Joined
2770        #[rustfmt::skip]
2771        let assoc_resp_failure = vec![
2772            // Mgmt Header:
2773            0b0001_00_00, 0b00000000, // Frame Control
2774            0, 0, // Duration
2775            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2776            3, 3, 3, 3, 3, 3, // Addr2
2777            6, 6, 6, 6, 6, 6, // Addr3
2778            0x10, 0, // Sequence Control
2779            // Assoc Resp Header:
2780            0, 0, // Capabilities
2781            2, 0, // Status Code (Failed)
2782            0, 0, // AID
2783        ];
2784        let rx_info = mock_rx_info(&sta);
2785        state = state.on_mac_frame(&mut sta, &assoc_resp_failure[..], rx_info, 0.into()).await;
2786        assert_matches!(state, States::Joined(_), "not in joined state");
2787        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2788    }
2789
2790    #[fuchsia::test(allow_stalls = false)]
2791    async fn state_transitions_associng_deauthing() {
2792        let mut m = MockObjects::new().await;
2793        let mut ctx = m.make_ctx_with_bss().await;
2794        let mut sta = make_client_station();
2795        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2796        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2797
2798        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2799        // Deauthentication: Associating > Joined
2800        #[rustfmt::skip]
2801        let deauth = vec![
2802            // Mgmt Header:
2803            0b1100_00_00, 0b00000000, // Frame Control
2804            0, 0, // Duration
2805            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2806            3, 3, 3, 3, 3, 3, // Addr2
2807            6, 6, 6, 6, 6, 6, // Addr3
2808            0x10, 0, // Sequence Control
2809            // Deauth Header:
2810            4, 0, // Reason Code
2811        ];
2812        let rx_info = mock_rx_info(&sta);
2813        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
2814        assert_matches!(state, States::Joined(_), "not in joined state");
2815        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2816    }
2817
2818    #[fuchsia::test(allow_stalls = false)]
2819    async fn state_transitions_associng_reconnect_no_op() {
2820        let mut m = MockObjects::new().await;
2821        let mut ctx = m.make_ctx_with_bss().await;
2822        let mut sta = make_client_station();
2823        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2824        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2825
2826        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2827        // (sme->mlme) Send a reconnect request
2828        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2829            peer_sta_address: BSSID.to_array(),
2830        });
2831        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2832        assert_matches!(state, States::Associating(_), "not in associating state");
2833        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2834
2835        // Verify no connect conf is sent
2836        m.fake_device_state
2837            .lock()
2838            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2839            .expect_err("unexpected Connect.confirm");
2840    }
2841
2842    #[fuchsia::test(allow_stalls = false)]
2843    async fn state_transitions_associng_reconnect_denied() {
2844        let mut m = MockObjects::new().await;
2845        let mut ctx = m.make_ctx_with_bss().await;
2846        let mut sta = make_client_station();
2847        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2848        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2849
2850        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2851        // (sme->mlme) Send a reconnect request
2852        let sus_bssid = [b's', b'u', b's', b'r', b'e', b'q'];
2853        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2854            peer_sta_address: sus_bssid,
2855        });
2856        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2857        assert_matches!(state, States::Associating(_), "not in associating state");
2858        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2859
2860        // Verify a connect conf was sent
2861        let connect_conf = m
2862            .fake_device_state
2863            .lock()
2864            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2865            .expect("error reading Connect.confirm");
2866        assert_eq!(
2867            connect_conf,
2868            fidl_mlme::ConnectConfirm {
2869                peer_sta_address: sus_bssid,
2870                result_code: fidl_ieee80211::StatusCode::NotInSameBss,
2871                association_id: 0,
2872                association_ies: vec![],
2873            }
2874        );
2875    }
2876
2877    #[fuchsia::test(allow_stalls = false)]
2878    async fn state_transitions_assoced_disassoc_connect_success() {
2879        let mut m = MockObjects::new().await;
2880        let mut ctx = m.make_ctx_with_bss().await;
2881        let mut sta = make_client_station();
2882        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2883        let mut state =
2884            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
2885
2886        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2887        // Disassociation: Associated > Associating
2888        #[rustfmt::skip]
2889        let disassoc = vec![
2890            // Mgmt Header:
2891            0b1010_00_00, 0b00000000, // Frame Control
2892            0, 0, // Duration
2893            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2894            3, 3, 3, 3, 3, 3, // Addr2
2895            6, 6, 6, 6, 6, 6, // Addr3
2896            0x10, 0, // Sequence Control
2897            // Deauth Header:
2898            4, 0, // Reason Code
2899        ];
2900        let rx_info = mock_rx_info(&sta);
2901        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
2902        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
2903        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2904
2905        // Verify a disassoc ind was sent
2906        let disassoc_ind = m
2907            .fake_device_state
2908            .lock()
2909            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
2910            .expect("error reading Disassociate.ind");
2911        assert_eq!(
2912            disassoc_ind,
2913            fidl_mlme::DisassociateIndication {
2914                peer_sta_address: BSSID.to_array(),
2915                reason_code: fidl_ieee80211::ReasonCode::ReasonInactivity,
2916                locally_initiated: false,
2917            }
2918        );
2919
2920        // (sme->mlme) Send a reconnect request
2921        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2922            peer_sta_address: BSSID.to_array(),
2923        });
2924        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2925        assert_matches!(state, States::Associating(_), "not in associating state");
2926
2927        // Verify associate request frame was sent
2928        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2929        assert_eq!(
2930            &m.fake_device_state.lock().wlan_queue[0].0[..22],
2931            &[
2932                // Mgmt header:
2933                0, 0, // FC
2934                0, 0, // Duration
2935                6, 6, 6, 6, 6, 6, // addr1
2936                3, 3, 3, 3, 3, 3, // addr2
2937                6, 6, 6, 6, 6, 6, // addr3
2938            ][..]
2939        );
2940
2941        // Successful: Associating > Associated
2942        #[rustfmt::skip]
2943        let assoc_resp_success = vec![
2944            // Mgmt Header:
2945            0b0001_00_00, 0b00000000, // Frame Control
2946            0, 0, // Duration
2947            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2948            3, 3, 3, 3, 3, 3, // Addr2
2949            6, 6, 6, 6, 6, 6, // Addr3
2950            0x10, 0, // Sequence Control
2951            // Assoc Resp Header:
2952            0, 0, // Capabilities
2953            0, 0, // Status Code
2954            11, 0, // AID
2955            // IEs
2956            // Basic Rates
2957            0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
2958            // HT Capabilities
2959            0x2d, 0x1a, 0xef, 0x09, // HT capabilities info
2960            0x17, // A-MPDU parameters
2961            0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2962            // VHT Capabilities
2963            0xbf, 0x0c, 0x91, 0x59, 0x82, 0x0f, // VHT capabilities info
2964            0xea, 0xff, 0x00, 0x00, 0xea, 0xff, 0x00, 0x00, // VHT supported MCS set
2965        ];
2966        let rx_info = mock_rx_info(&sta);
2967        state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], rx_info, 0.into()).await;
2968        assert_matches!(state, States::Associated(_), "not in associated state");
2969
2970        // Verify a successful connect conf is sent
2971        let connect_conf = m
2972            .fake_device_state
2973            .lock()
2974            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2975            .expect("error reading Connect.confirm");
2976        assert_eq!(&connect_conf.peer_sta_address, BSSID.as_array());
2977        assert_eq!(connect_conf.result_code, fidl_ieee80211::StatusCode::Success);
2978        assert_eq!(connect_conf.association_id, 11);
2979    }
2980
2981    #[fuchsia::test(allow_stalls = false)]
2982    async fn state_transitions_assoced_disassoc_reconnect_timeout() {
2983        let mut m = MockObjects::new().await;
2984        let mut ctx = m.make_ctx_with_bss().await;
2985        let mut sta = make_client_station();
2986        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2987        let mut state =
2988            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
2989
2990        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2991        // Disassociation: Associated > Associating
2992        #[rustfmt::skip]
2993        let disassoc = vec![
2994            // Mgmt Header:
2995            0b1010_00_00, 0b00000000, // Frame Control
2996            0, 0, // Duration
2997            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2998            3, 3, 3, 3, 3, 3, // Addr2
2999            6, 6, 6, 6, 6, 6, // Addr3
3000            0x10, 0, // Sequence Control
3001            // Deauth Header:
3002            4, 0, // Reason Code
3003        ];
3004        let rx_info = mock_rx_info(&sta);
3005        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
3006        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3007        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3008
3009        // Verify a disassoc ind was sent
3010        let _disassoc_ind = m
3011            .fake_device_state
3012            .lock()
3013            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
3014            .expect("error reading Disassociate.ind");
3015
3016        // (sme->mlme) Send a reconnect request
3017        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3018            peer_sta_address: BSSID.to_array(),
3019        });
3020        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3021        assert_matches!(state, States::Associating(_), "not in associating state");
3022
3023        // Verify an event was queued up in the timer.
3024        let (event, _id) = assert_matches!(drain_timeouts(&mut m.time_stream).get(&TimedEventClass::Reassociating), Some(ids) => {
3025            assert_eq!(ids.len(), 1);
3026            ids[0].clone()
3027        });
3028
3029        // Notify reconnecting timeout
3030        let state = state.on_timed_event(&mut sta, event).await;
3031        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3032        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3033
3034        // Verify a connect conf was sent
3035        let connect_conf = m
3036            .fake_device_state
3037            .lock()
3038            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3039            .expect("error reading Connect.confirm");
3040        assert_eq!(
3041            connect_conf,
3042            fidl_mlme::ConnectConfirm {
3043                peer_sta_address: BSSID.to_array(),
3044                result_code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
3045                association_id: 0,
3046                association_ies: vec![],
3047            }
3048        );
3049    }
3050
3051    #[fuchsia::test(allow_stalls = false)]
3052    async fn state_transitions_assoced_disassoc_reconnect_denied() {
3053        let mut m = MockObjects::new().await;
3054        let mut ctx = m.make_ctx_with_bss().await;
3055        let mut sta = make_client_station();
3056        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3057        let mut state =
3058            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3059
3060        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3061        // Disassociation: Associated > Associating
3062        #[rustfmt::skip]
3063        let disassoc = vec![
3064            // Mgmt Header:
3065            0b1010_00_00, 0b00000000, // Frame Control
3066            0, 0, // Duration
3067            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3068            3, 3, 3, 3, 3, 3, // Addr2
3069            6, 6, 6, 6, 6, 6, // Addr3
3070            0x10, 0, // Sequence Control
3071            // Deauth Header:
3072            4, 0, // Reason Code
3073        ];
3074        let rx_info = mock_rx_info(&sta);
3075        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
3076        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3077        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3078
3079        // Verify a disassoc ind was sent
3080        let _disassoc_ind = m
3081            .fake_device_state
3082            .lock()
3083            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
3084            .expect("error reading Disassociate.ind");
3085
3086        // (sme->mlme) Send a reconnect request with a different BSSID
3087        let sus_bssid = [b's', b'u', b's', b'r', b'e', b'q'];
3088        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3089            peer_sta_address: sus_bssid,
3090        });
3091        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3092        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3093
3094        // Verify a connect conf was sent
3095        let connect_conf = m
3096            .fake_device_state
3097            .lock()
3098            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3099            .expect("error reading Connect.confirm");
3100        assert_eq!(
3101            connect_conf,
3102            fidl_mlme::ConnectConfirm {
3103                peer_sta_address: sus_bssid,
3104                result_code: fidl_ieee80211::StatusCode::NotInSameBss,
3105                association_id: 0,
3106                association_ies: vec![],
3107            }
3108        );
3109    }
3110
3111    #[fuchsia::test(allow_stalls = false)]
3112    async fn state_transitions_assoced_reconnect_no_op() {
3113        let mut m = MockObjects::new().await;
3114        let mut ctx = m.make_ctx_with_bss().await;
3115        let mut sta = make_client_station();
3116        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3117        let association = Association {
3118            aid: 42,
3119            assoc_resp_ies: vec![
3120                // Basic Rates
3121                0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
3122            ],
3123            ..empty_association(&mut sta)
3124        };
3125        let mut state = States::from(statemachine::testing::new_state(Associated(association)));
3126
3127        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3128
3129        // (sme->mlme) Send a reconnect request
3130        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3131            peer_sta_address: BSSID.to_array(),
3132        });
3133        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3134        assert_matches!(state, States::Associated(_), "not in associated state");
3135        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3136
3137        // Verify a successful connect conf is sent
3138        let connect_conf = m
3139            .fake_device_state
3140            .lock()
3141            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3142            .expect("error reading Connect.confirm");
3143        assert_eq!(&connect_conf.peer_sta_address, BSSID.as_array());
3144        assert_eq!(connect_conf.result_code, fidl_ieee80211::StatusCode::Success);
3145        assert_eq!(connect_conf.association_id, 42);
3146    }
3147
3148    #[fuchsia::test(allow_stalls = false)]
3149    async fn state_transitions_assoced_deauthing() {
3150        let mut m = MockObjects::new().await;
3151        let mut ctx = m.make_ctx_with_bss().await;
3152        let mut sta = make_client_station();
3153        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3154        let mut state =
3155            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3156
3157        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3158        // Deauthentication: Associated > Joined
3159        #[rustfmt::skip]
3160        let deauth = vec![
3161            // Mgmt Header:
3162            0b1100_00_00, 0b00000000, // Frame Control
3163            0, 0, // Duration
3164            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3165            3, 3, 3, 3, 3, 3, // Addr2
3166            6, 6, 6, 6, 6, 6, // Addr3
3167            0x10, 0, // Sequence Control
3168            // Deauth Header:
3169            4, 0, // Reason Code
3170        ];
3171        let rx_info = mock_rx_info(&sta);
3172        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
3173        assert_matches!(state, States::Joined(_), "not in joined state");
3174        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3175    }
3176
3177    #[test_case(false, false; "unprotected bss, not scanning")]
3178    #[test_case(true, false; "protected bss, not scanning")]
3179    #[test_case(false, true; "unprotected bss, scanning")]
3180    #[test_case(true, true; "protected bss, scanning")]
3181    #[fuchsia::test(allow_stalls = false)]
3182    async fn assoc_send_eth_frame_becomes_data_frame(protected: bool, scanning: bool) {
3183        let mut m = MockObjects::new().await;
3184        let mut ctx = m.make_ctx().await;
3185        let mut sta =
3186            if protected { make_protected_client_station() } else { make_client_station() };
3187        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3188        let state = States::from(statemachine::testing::new_state(Associated(Association {
3189            controlled_port_open: true,
3190            ..empty_association(&mut sta)
3191        })));
3192
3193        if scanning {
3194            let mut bound_scanner = sta.scanner.bind(sta.ctx);
3195            bound_scanner
3196                .on_sme_scan(fidl_mlme::ScanRequest {
3197                    txn_id: 1337,
3198                    scan_type: fidl_mlme::ScanTypes::Passive,
3199                    channel_list: vec![1],
3200                    ssid_list: vec![],
3201                    probe_delay: 0,
3202                    min_channel_time: 100,
3203                    max_channel_time: 300,
3204                })
3205                .await
3206                .expect("Failed to start scan");
3207            assert!(sta.scanner.is_scanning());
3208        }
3209
3210        let eth_frame = [
3211            1, 2, 3, 4, 5, 6, // dst_addr
3212            3, 3, 3, 3, 3, 3, // src_addr == IFACE_MAC
3213            0x0d, 0x05, // ether_type
3214            21, 22, 23, 24, 25, 26, 27, 28, // payload
3215            29, // more payload
3216        ];
3217
3218        state.on_eth_frame(&mut sta, &eth_frame[..], 0.into()).expect("all good");
3219
3220        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3221        let (data_frame, _tx_flags) = m.fake_device_state.lock().wlan_queue.remove(0);
3222        let mut fc_byte_2 = 0b00000001;
3223        if protected {
3224            fc_byte_2 |= 0b01000000;
3225        }
3226        assert_eq!(
3227            &data_frame[..],
3228            &[
3229                // Data header
3230                0b00001000, fc_byte_2, // Frame Control
3231                0, 0, // Duration
3232                6, 6, 6, 6, 6, 6, // addr1
3233                3, 3, 3, 3, 3, 3, // addr2 (from src_addr above)
3234                1, 2, 3, 4, 5, 6, // addr3 (from dst_addr above)
3235                0x10, 0, // Sequence Control
3236                // LLC header
3237                0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
3238                0, 0, 0, // OUI
3239                0x0d, 0x05, // Protocol ID (from ether_type above)
3240                21, 22, 23, 24, 25, 26, 27, 28, // Payload
3241                29, // More payload
3242            ][..]
3243        )
3244    }
3245
3246    #[fuchsia::test(allow_stalls = false)]
3247    async fn eth_frame_dropped_when_off_channel() {
3248        let mut m = MockObjects::new().await;
3249        let mut ctx = m.make_ctx().await;
3250        let mut sta = make_client_station();
3251        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3252        let state =
3253            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3254
3255        sta.ctx
3256            .device
3257            .set_channel(fidl_ieee80211::WlanChannel {
3258                primary: 42,
3259                cbw: fidl_ieee80211::ChannelBandwidth::Cbw20,
3260                secondary80: 0,
3261            })
3262            .await
3263            .expect("fake device is obedient");
3264        let eth_frame = &[100; 14]; // An ethernet frame must be at least 14 bytes long.
3265
3266        let error = state
3267            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3268            .expect_err("Ethernet frame is dropped when client is off channel");
3269        assert_matches!(error, Error::Status(_str, status) =>
3270            assert_eq!(status, zx::Status::BAD_STATE),
3271            "error should contain a status"
3272        );
3273    }
3274
3275    #[fuchsia::test(allow_stalls = false)]
3276    async fn assoc_eth_frame_too_short_dropped() {
3277        let mut m = MockObjects::new().await;
3278        let mut ctx = m.make_ctx().await;
3279        let mut sta = make_client_station();
3280        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3281        let state =
3282            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3283
3284        let eth_frame = &[100; 13]; // Needs at least 14 bytes for header.
3285
3286        let error = state
3287            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3288            .expect_err("Ethernet frame is too short");
3289        assert_matches!(error, Error::Status(_str, status) =>
3290            assert_eq!(status, zx::Status::IO_DATA_INTEGRITY),
3291            "error should contain a status"
3292        );
3293    }
3294
3295    #[fuchsia::test(allow_stalls = false)]
3296    async fn assoc_controlled_port_closed_eth_frame_dropped() {
3297        let mut m = MockObjects::new().await;
3298        let mut ctx = m.make_ctx().await;
3299        let mut sta = make_client_station();
3300        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3301        let state =
3302            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3303
3304        let eth_frame = &[100; 14]; // long enough for ethernet header.
3305
3306        let error = state
3307            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3308            .expect_err("Ethernet frame canot be sent when controlled port is closed");
3309        assert_matches!(error, Error::Status(_str, status) =>
3310            assert_eq!(status, zx::Status::BAD_STATE),
3311            "Error should contain status"
3312        );
3313    }
3314
3315    #[fuchsia::test(allow_stalls = false)]
3316    async fn not_assoc_eth_frame_dropped() {
3317        let mut m = MockObjects::new().await;
3318        let mut ctx = m.make_ctx().await;
3319        let mut sta = make_client_station();
3320        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3321        let state = States::from(statemachine::testing::new_state(Joined));
3322
3323        let eth_frame = &[100; 14]; // long enough for ethernet header.
3324
3325        let error = state
3326            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3327            .expect_err("Ethernet frame cannot be sent in Joined state");
3328        assert_matches!(error, Error::Status(_str, status) =>
3329            assert_eq!(status, zx::Status::BAD_STATE),
3330            "Error should contain status"
3331        );
3332    }
3333
3334    #[fuchsia::test(allow_stalls = false)]
3335    async fn joined_sme_deauth() {
3336        let mut m = MockObjects::new().await;
3337        let mut ctx = m.make_ctx_with_bss().await;
3338        let mut sta = make_client_station();
3339        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3340        let state = States::from(statemachine::testing::new_state(Joined));
3341
3342        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3343        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3344        assert_matches!(state, States::Joined(_), "Joined should stay in Joined");
3345        // No MLME message was sent because MLME already deauthenticated.
3346        m.fake_device_state
3347            .lock()
3348            .next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
3349            .expect_err("should be no outgoing message");
3350        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3351    }
3352
3353    #[fuchsia::test(allow_stalls = false)]
3354    async fn authenticating_sme_deauth() {
3355        let mut m = MockObjects::new().await;
3356        let mut ctx = m.make_ctx_with_bss().await;
3357        let mut sta = make_client_station();
3358        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3359        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3360
3361        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3362        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3363
3364        assert_matches!(state, States::Joined(_), "should transition to Joined");
3365
3366        // No need to notify SME since it already deauthenticated
3367        m.fake_device_state
3368            .lock()
3369            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3370            .expect_err("should not see more MLME messages");
3371        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3372    }
3373
3374    #[fuchsia::test(allow_stalls = false)]
3375    async fn associating_sme_deauth() {
3376        let mut m = MockObjects::new().await;
3377        let mut ctx = m.make_ctx_with_bss().await;
3378        let mut sta = make_client_station();
3379        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3380        let state = States::from(statemachine::testing::new_state(Associating::default()));
3381
3382        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3383        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3384
3385        assert_matches!(state, States::Joined(_), "should transition to Joined");
3386
3387        // No need to notify SME since it already deauthenticated
3388        m.fake_device_state
3389            .lock()
3390            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3391            .expect_err("should not see more MLME messages");
3392        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3393    }
3394
3395    #[fuchsia::test(allow_stalls = false)]
3396    async fn associated_sme_deauth() {
3397        let mut m = MockObjects::new().await;
3398        let mut ctx = m.make_ctx_with_bss().await;
3399        let mut sta = make_client_station();
3400        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3401        let state = States::from(statemachine::testing::new_state(Associated(Association {
3402            controlled_port_open: true,
3403            ..empty_association(&mut sta)
3404        })));
3405
3406        sta.ctx
3407            .device
3408            .notify_association_complete(fake_assoc_cfg())
3409            .await
3410            .expect("valid assoc ctx should not fail");
3411        assert_eq!(1, m.fake_device_state.lock().assocs.len());
3412
3413        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3414        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
3415        assert_eq!(crate::device::LinkStatus::UP, m.fake_device_state.lock().link_status);
3416
3417        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3418        assert_matches!(state, States::Joined(_), "should transition to Joined");
3419
3420        // Should accept the deauthentication request and send back confirm.
3421        let deauth_conf = m
3422            .fake_device_state
3423            .lock()
3424            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3425            .expect("should see deauth conf");
3426        assert_eq!(
3427            deauth_conf,
3428            fidl_mlme::DeauthenticateConfirm { peer_sta_address: BSSID.to_array() }
3429        );
3430        m.fake_device_state
3431            .lock()
3432            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3433            .expect_err("should not see more MLME messages");
3434        // Verify association context cleared.
3435        assert_eq!(0, m.fake_device_state.lock().assocs.len());
3436        // Verify ethernet link status is down.
3437        assert_eq!(crate::device::LinkStatus::DOWN, m.fake_device_state.lock().link_status);
3438        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3439    }
3440
3441    fn fake_eapol_req() -> wlan_sme::MlmeRequest {
3442        wlan_sme::MlmeRequest::Eapol(fidl_mlme::EapolRequest {
3443            dst_addr: BSSID.to_array(),
3444            src_addr: IFACE_MAC.to_array(),
3445            data: vec![1, 2, 3, 4],
3446        })
3447    }
3448
3449    #[allow(deprecated)]
3450    #[fuchsia::test(allow_stalls = false)]
3451    async fn mlme_eapol_not_associated() {
3452        let mut m = MockObjects::new().await;
3453        let mut ctx = m.make_ctx().await;
3454        let mut sta = make_protected_client_station();
3455        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3456
3457        let state = States::from(statemachine::testing::new_state(Joined));
3458        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3459        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3460
3461        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3462        m.fake_device_state.lock().wlan_queue.clear();
3463        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3464        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3465
3466        let state = States::from(statemachine::testing::new_state(Associating::default()));
3467        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3468        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3469    }
3470
3471    #[allow(deprecated)]
3472    #[fuchsia::test(allow_stalls = false)]
3473    async fn mlme_eapol_associated_not_protected() {
3474        let mut m = MockObjects::new().await;
3475        let mut ctx = m.make_ctx().await;
3476        let mut sta = make_client_station();
3477        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3478
3479        let state =
3480            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3481        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3482        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3483    }
3484
3485    #[allow(deprecated)]
3486    #[fuchsia::test(allow_stalls = false)]
3487    async fn mlme_eapol_associated() {
3488        let mut m = MockObjects::new().await;
3489        let mut ctx = m.make_ctx().await;
3490        let mut sta = make_protected_client_station();
3491        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3492
3493        let state =
3494            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3495        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3496        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3497        assert_eq!(
3498            &m.fake_device_state.lock().wlan_queue[0].0[..],
3499            &[
3500                // Data header (EAPoL frames are data frames)
3501                0b00001000, 0b00000001, // Frame Control
3502                0, 0, // Duration
3503                6, 6, 6, 6, 6, 6, // addr1 - BSSID
3504                3, 3, 3, 3, 3, 3, // addr2 - IFACE_MAC
3505                6, 6, 6, 6, 6, 6, // addr3 - BSSID
3506                0x10, 0, // Sequence Control
3507                // LLC header
3508                0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
3509                0, 0, 0, // OUI
3510                0x88, 0x8E, // Protocol ID (EAPoL is 0x888E)
3511                1, 2, 3, 4, // Payload
3512            ][..]
3513        );
3514    }
3515
3516    #[allow(deprecated)]
3517    #[fuchsia::test(allow_stalls = false)]
3518    async fn mlme_set_keys_not_associated() {
3519        let mut m = MockObjects::new().await;
3520        let mut ctx = m.make_ctx().await;
3521        let mut sta = make_protected_client_station();
3522        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3523
3524        let state = States::from(statemachine::testing::new_state(Joined));
3525        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3526        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3527
3528        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3529        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3530        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3531
3532        let state = States::from(statemachine::testing::new_state(Associating::default()));
3533        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3534        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3535    }
3536
3537    #[allow(deprecated)]
3538    #[fuchsia::test(allow_stalls = false)]
3539    async fn mlme_set_keys_associated_not_protected() {
3540        let mut m = MockObjects::new().await;
3541        let mut ctx = m.make_ctx().await;
3542        let mut sta = make_client_station();
3543        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3544
3545        let state =
3546            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3547        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3548        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3549    }
3550
3551    #[allow(deprecated)]
3552    #[fuchsia::test(allow_stalls = false)]
3553    async fn mlme_set_keys_associated() {
3554        let mut m = MockObjects::new().await;
3555        let mut ctx = m.make_ctx().await;
3556        let mut sta = make_protected_client_station();
3557        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3558
3559        let state =
3560            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3561        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3562        assert_eq!(m.fake_device_state.lock().keys.len(), 1);
3563        let conf = assert_matches!(m.fake_device_state.lock().next_mlme_msg::<fidl_mlme::SetKeysConfirm>(), Ok(conf) => conf);
3564        assert_eq!(conf.results.len(), 1);
3565        assert_eq!(
3566            conf.results[0],
3567            fidl_mlme::SetKeyResult { key_id: 6, status: zx::Status::OK.into_raw() }
3568        );
3569
3570        assert_eq!(
3571            m.fake_device_state.lock().keys,
3572            vec![fidl_softmac::WlanKeyConfiguration {
3573                protection: Some(fidl_softmac::WlanProtection::RxTx),
3574                cipher_oui: Some([1, 2, 3]),
3575                cipher_type: Some(4),
3576                key_type: Some(fidl_ieee80211::KeyType::Pairwise),
3577                peer_addr: Some((*BSSID).to_array()),
3578                key_idx: Some(6),
3579                key: Some(vec![1, 2, 3, 4, 5, 6, 7]),
3580                rsc: Some(8),
3581                ..Default::default()
3582            }]
3583        );
3584    }
3585
3586    #[allow(deprecated)]
3587    #[fuchsia::test(allow_stalls = false)]
3588    async fn mlme_set_keys_failure() {
3589        let mut m = MockObjects::new().await;
3590        let mut ctx = m.make_ctx().await;
3591        let mut sta = make_protected_client_station();
3592        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3593
3594        let state =
3595            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3596        m.fake_device_state.lock().install_key_results.push_back(Err(zx::Status::BAD_STATE));
3597        m.fake_device_state.lock().install_key_results.push_back(Ok(()));
3598        // Create a SetKeysReq with one success and one failure.
3599        let mut set_keys_req = fake_set_keys_req((*BSSID).into());
3600        match &mut set_keys_req {
3601            wlan_sme::MlmeRequest::SetKeys(req) => {
3602                req.keylist
3603                    .push(fidl_mlme::SetKeyDescriptor { key_id: 4, ..req.keylist[0].clone() });
3604            }
3605            _ => panic!(),
3606        }
3607        let _state = state.handle_mlme_req(&mut sta, set_keys_req).await;
3608        let conf = assert_matches!(m.fake_device_state.lock().next_mlme_msg::<fidl_mlme::SetKeysConfirm>(), Ok(conf) => conf);
3609        assert_eq!(conf.results.len(), 2);
3610        assert_eq!(
3611            conf.results[0],
3612            fidl_mlme::SetKeyResult { key_id: 6, status: zx::Status::BAD_STATE.into_raw() }
3613        );
3614        assert_eq!(
3615            conf.results[1],
3616            fidl_mlme::SetKeyResult { key_id: 4, status: zx::Status::OK.into_raw() }
3617        );
3618    }
3619
3620    fn fake_set_ctrl_port_open(open: bool) -> wlan_sme::MlmeRequest {
3621        wlan_sme::MlmeRequest::SetCtrlPort(fidl_mlme::SetControlledPortRequest {
3622            peer_sta_address: BSSID.to_array(),
3623            state: match open {
3624                true => fidl_mlme::ControlledPortState::Open,
3625                false => fidl_mlme::ControlledPortState::Closed,
3626            },
3627        })
3628    }
3629
3630    #[fuchsia::test(allow_stalls = false)]
3631    async fn mlme_set_controlled_port_not_associated() {
3632        let mut m = MockObjects::new().await;
3633        let mut ctx = m.make_ctx().await;
3634        let mut sta = make_protected_client_station();
3635        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3636
3637        let state = States::from(statemachine::testing::new_state(Joined));
3638        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3639        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3640
3641        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3642        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3643        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3644
3645        let state = States::from(statemachine::testing::new_state(Associating::default()));
3646        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3647        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3648    }
3649
3650    #[fuchsia::test(allow_stalls = false)]
3651    async fn mlme_set_controlled_port_associated_not_protected() {
3652        let mut m = MockObjects::new().await;
3653        let mut ctx = m.make_ctx().await;
3654        let mut sta = make_client_station();
3655        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3656
3657        let state =
3658            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3659        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3660        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3661    }
3662
3663    #[fuchsia::test(allow_stalls = false)]
3664    async fn mlme_set_controlled_port_associated() {
3665        let mut m = MockObjects::new().await;
3666        let mut ctx = m.make_ctx().await;
3667        let mut sta = make_protected_client_station();
3668        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3669
3670        let state =
3671            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3672        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3673        let state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3674        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
3675        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(false)).await;
3676        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3677    }
3678
3679    #[test_case(true; "while scanning")]
3680    #[test_case(false; "while not scanning")]
3681    #[fuchsia::test(allow_stalls = false)]
3682    async fn associated_rx_succeeds(scanning: bool) {
3683        let mut m = MockObjects::new().await;
3684        let mut ctx = m.make_ctx().await;
3685        let mut sta = make_client_station();
3686        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3687        let state = States::from(statemachine::testing::new_state(Associated(Association {
3688            aid: 1,
3689            controlled_port_open: true,
3690            ..empty_association(&mut sta)
3691        })));
3692
3693        if scanning {
3694            let mut bound_scanner = sta.scanner.bind(sta.ctx);
3695            bound_scanner
3696                .on_sme_scan(fidl_mlme::ScanRequest {
3697                    txn_id: 1337,
3698                    scan_type: fidl_mlme::ScanTypes::Passive,
3699                    channel_list: vec![1],
3700                    ssid_list: vec![],
3701                    probe_delay: 0,
3702                    min_channel_time: 100,
3703                    max_channel_time: 300,
3704                })
3705                .await
3706                .expect("Failed to start scan");
3707            assert!(sta.scanner.is_scanning());
3708        }
3709
3710        let fc = mac::FrameControl(0)
3711            .with_frame_type(mac::FrameType::DATA)
3712            .with_data_subtype(mac::DataSubtype(0))
3713            .with_from_ds(true);
3714        let fc = fc.0.to_le_bytes();
3715
3716        let data_frame = vec![
3717            // Data header:
3718            fc[0], fc[1], // FC
3719            0, 0, // Duration
3720            7, 7, 7, 7, 7, 7, // addr1
3721            6, 6, 6, 6, 6, 6, // addr2
3722            42, 42, 42, 42, 42, 42, // addr3
3723            0x10, 0, // Sequence Control
3724            // LLC Header
3725            7, 7, 7, // DSAP, SSAP & control
3726            8, 8, 8, // OUI
3727            9, 10, // eth type
3728            1, 2, 3, 4, 5, 6, 7, 8, // payload
3729        ];
3730
3731        let rx_info = mock_rx_info(&sta);
3732        state.on_mac_frame(&mut sta, &data_frame[..], rx_info, 0.into()).await;
3733        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
3734    }
3735
3736    #[fuchsia::test(allow_stalls = false)]
3737    async fn associated_rx_with_wrong_cbw_succeeds() {
3738        let mut m = MockObjects::new().await;
3739        let mut ctx = m.make_ctx().await;
3740        let mut sta = make_client_station();
3741        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3742        let state = States::from(statemachine::testing::new_state(Associated(Association {
3743            aid: 1,
3744            controlled_port_open: true,
3745            ..empty_association(&mut sta)
3746        })));
3747
3748        let fc = mac::FrameControl(0)
3749            .with_frame_type(mac::FrameType::DATA)
3750            .with_data_subtype(mac::DataSubtype(0))
3751            .with_from_ds(true);
3752        let fc = fc.0.to_le_bytes();
3753
3754        let data_frame = vec![
3755            // Data header:
3756            fc[0], fc[1], // FC
3757            0, 0, // Duration
3758            7, 7, 7, 7, 7, 7, // addr1
3759            6, 6, 6, 6, 6, 6, // addr2
3760            42, 42, 42, 42, 42, 42, // addr3
3761            0x10, 0, // Sequence Control
3762            // LLC Header
3763            7, 7, 7, // DSAP, SSAP & control
3764            8, 8, 8, // OUI
3765            9, 10, // eth type
3766            1, 2, 3, 4, 5, 6, 7, 8, // payload
3767        ];
3768
3769        let mut rx_info = mock_rx_info(&sta);
3770        // We deliberately ignore the cbw, since it isn't important and not all
3771        // drivers report it consistently.
3772        rx_info.channel.cbw = fidl_ieee80211::ChannelBandwidth::Cbw80;
3773        state.on_mac_frame(&mut sta, &data_frame[..], rx_info, 0.into()).await;
3774        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
3775    }
3776
3777    #[fuchsia::test(allow_stalls = false)]
3778    async fn associated_request_bu_if_tim_indicates_buffered_frame() {
3779        let mut m = MockObjects::new().await;
3780        let mut ctx = m.make_ctx().await;
3781        let mut sta = make_client_station();
3782        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3783        let state = States::from(statemachine::testing::new_state(Associated(Association {
3784            aid: 1,
3785            ..empty_association(&mut sta)
3786        })));
3787
3788        let beacon = [
3789            // Mgmt header
3790            0b10000000, 0, // Frame Control
3791            0, 0, // Duration
3792            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3793            7, 7, 7, 7, 7, 7, // Addr2
3794            6, 6, 6, 6, 6, 6, // Addr3
3795            0, 0, // Sequence Control
3796            // Beacon header:
3797            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3798            10, 0, // Beacon interval
3799            33, 0, // Capabilities
3800            5, 4, 0, 0, 0, 0b00000010, // Tim IE: bit 1 in the last octet indicates AID 1
3801        ];
3802
3803        let rx_info = mock_rx_info(&sta);
3804        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3805
3806        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3807        assert_eq!(
3808            &m.fake_device_state.lock().wlan_queue[0].0[..],
3809            &[
3810                0b10100100, 0, // Frame control
3811                1, 0b11000000, // ID (2 MSBs are set to 1 from the AID)
3812                6, 6, 6, 6, 6, 6, // BSSID
3813                3, 3, 3, 3, 3, 3, // TA
3814            ][..]
3815        );
3816    }
3817
3818    #[fuchsia::test(allow_stalls = false)]
3819    async fn associated_does_not_request_bu_if_tim_indicates_no_buffered_frame() {
3820        let mut m = MockObjects::new().await;
3821        let mut ctx = m.make_ctx().await;
3822        let mut sta = make_client_station();
3823        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3824        let state = States::from(statemachine::testing::new_state(Associated(Association {
3825            aid: 1,
3826            ..empty_association(&mut sta)
3827        })));
3828
3829        let beacon = [
3830            // Mgmt header
3831            0b10000000, 0, // Frame Control
3832            0, 0, // Duration
3833            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3834            7, 7, 7, 7, 7, 7, // Addr2
3835            6, 6, 6, 6, 6, 6, // Addr3
3836            0, 0, // Sequence Control
3837            // Beacon header:
3838            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3839            10, 0, // Beacon interval
3840            33, 0, // Capabilities
3841            5, 4, 0, 0, 0, 0, // Tim IE: No buffered frame for any client.
3842        ];
3843        let rx_info = mock_rx_info(&sta);
3844        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3845
3846        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3847    }
3848
3849    fn rx_info_with_dbm<'a>(
3850        client: &BoundClient<'a, FakeDevice>,
3851        rssi_dbm: i8,
3852    ) -> fidl_softmac::WlanRxInfo {
3853        let mut rx_info = fidl_softmac::WlanRxInfo { rssi_dbm, ..mock_rx_info(client) };
3854        rx_info.valid_fields |= fidl_softmac::WlanRxInfoValid::RSSI;
3855        rx_info
3856    }
3857
3858    #[fuchsia::test(allow_stalls = false)]
3859    async fn signal_report() {
3860        let mut m = MockObjects::new().await;
3861        let mut ctx = m.make_ctx().await;
3862        let mut sta = make_protected_client_station();
3863        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3864
3865        let state = States::from(State::from(statemachine::testing::new_state(Associated(
3866            empty_association(&mut sta),
3867        ))));
3868
3869        let (_, timed_event, _) =
3870            m.time_stream.try_next().unwrap().expect("Should have scheduled signal report timeout");
3871        let state = state.on_timed_event(&mut sta, timed_event.event).await;
3872
3873        let signal_ind = m
3874            .fake_device_state
3875            .lock()
3876            .next_mlme_msg::<fidl_internal::SignalReportIndication>()
3877            .expect("should see a signal report");
3878
3879        // -128 is the default value, equivalent to 0 watt.
3880        assert_eq!(signal_ind.rssi_dbm, -128);
3881
3882        let beacon = [
3883            // Mgmt header
3884            0b10000000, 0, // Frame Control
3885            0, 0, // Duration
3886            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3887            7, 7, 7, 7, 7, 7, // Addr2
3888            6, 6, 6, 6, 6, 6, // Addr3
3889            0, 0, // Sequence Control
3890            // Beacon header:
3891            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3892            10, 0, // Beacon interval
3893            33, 0, // Capabilities
3894        ];
3895
3896        const EXPECTED_DBM: i8 = -32;
3897        let rx_info = rx_info_with_dbm(&sta, EXPECTED_DBM);
3898        let state = state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3899
3900        let (_, timed_event, _) =
3901            m.time_stream.try_next().unwrap().expect("Should have scheduled signal report timeout");
3902        let _state = state.on_timed_event(&mut sta, timed_event.event).await;
3903
3904        let signal_ind = m
3905            .fake_device_state
3906            .lock()
3907            .next_mlme_msg::<fidl_internal::SignalReportIndication>()
3908            .expect("should see a signal report");
3909
3910        assert_eq!(signal_ind.rssi_dbm, EXPECTED_DBM);
3911    }
3912}