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!(c"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!(c"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!(c"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!(c"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!(c"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!(c"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!(c"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!(c"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 std::ffi::CStr = c"States::on_data_frame => MSDU";
662
663        wtrace::duration!(c"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!(c"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!(c"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!(c"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!(c"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!(c"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        };
1516        Client::new(connect_req, *IFACE_MAC, fake_client_capabilities())
1517    }
1518
1519    fn make_protected_client_station() -> Client {
1520        let connect_req = ParsedConnectRequest {
1521            selected_bss: fake_bss_description!(Wpa2, bssid: BSSID.to_array()),
1522            connect_failure_timeout: 10,
1523            auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
1524            sae_password: vec![],
1525            wep_key: None,
1526            security_ie: vec![
1527                0x30, 0x14, //  ID and len
1528                1, 0, //  version
1529                0x00, 0x0f, 0xac, 0x04, //  group data cipher suite
1530                0x01, 0x00, //  pairwise cipher suite count
1531                0x00, 0x0f, 0xac, 0x04, //  pairwise cipher suite list
1532                0x01, 0x00, //  akm suite count
1533                0x00, 0x0f, 0xac, 0x02, //  akm suite list
1534                0xa8, 0x04, //  rsn capabilities
1535            ],
1536        };
1537        Client::new(connect_req, *IFACE_MAC, fake_client_capabilities())
1538    }
1539
1540    fn connect_conf_failure(result_code: fidl_ieee80211::StatusCode) -> fidl_mlme::ConnectConfirm {
1541        fidl_mlme::ConnectConfirm {
1542            peer_sta_address: BSSID.to_array(),
1543            result_code,
1544            association_id: 0,
1545            association_ies: vec![],
1546        }
1547    }
1548
1549    fn empty_association(sta: &mut BoundClient<'_, FakeDevice>) -> Association {
1550        let status_check_timeout =
1551            schedule_association_status_timeout(sta.sta.beacon_period(), &mut sta.ctx.timer);
1552        Association {
1553            controlled_port_open: false,
1554            aid: 0,
1555            assoc_resp_ies: vec![],
1556            ap_ht_op: None,
1557            ap_vht_op: None,
1558            lost_bss_counter: LostBssCounter::start(
1559                sta.sta.beacon_period(),
1560                DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT,
1561            ),
1562            qos: Qos::Disabled,
1563            status_check_timeout,
1564            signal_strength_average: SignalStrengthAverage::new(),
1565            block_ack_state: StateMachine::new(BlockAckState::from(State::new(Closed))),
1566        }
1567    }
1568
1569    fn fake_assoc_cfg() -> fidl_softmac::WlanAssociationConfig {
1570        fidl_softmac::WlanAssociationConfig {
1571            bssid: Some(BSSID.to_array()),
1572            aid: Some(42),
1573            channel: Some(fidl_ieee80211::WlanChannel {
1574                primary: 149,
1575                cbw: fidl_ieee80211::ChannelBandwidth::Cbw40,
1576                secondary80: 42,
1577            }),
1578            rates: None,
1579            capability_info: None,
1580            ..Default::default()
1581        }
1582    }
1583
1584    fn fake_deauth_req() -> wlan_sme::MlmeRequest {
1585        wlan_sme::MlmeRequest::Deauthenticate(fidl_mlme::DeauthenticateRequest {
1586            peer_sta_address: BSSID.to_array(),
1587            reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
1588        })
1589    }
1590
1591    fn open_authenticating(sta: &mut BoundClient<'_, FakeDevice>) -> Authenticating {
1592        let mut auth = Authenticating::new(AkmAlgorithm::OpenSupplicant);
1593        auth.algorithm.initiate(sta).expect("Failed to initiate open auth");
1594        auth
1595    }
1596
1597    #[fuchsia::test(allow_stalls = false)]
1598    async fn connect_authenticate_tx_failure() {
1599        let mut m = MockObjects::new().await;
1600        m.fake_device_state.lock().config.send_wlan_frame_fails = true;
1601        let mut ctx = m.make_ctx().await;
1602        let mut sta = make_client_station();
1603        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1604
1605        let state = Joined;
1606        let _state =
1607            state.start_authenticating(&mut sta).await.expect_err("should fail authenticating");
1608
1609        // Verify no event was queued up in the timer.
1610        assert!(m.time_stream.try_next().is_err());
1611
1612        // Verify MLME-CONNECT.confirm message was sent.
1613        let msg = m
1614            .fake_device_state
1615            .lock()
1616            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1617            .expect("expect msg");
1618        assert_eq!(
1619            msg,
1620            fidl_mlme::ConnectConfirm {
1621                peer_sta_address: BSSID.to_array(),
1622                result_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1623                association_id: 0,
1624                association_ies: vec![],
1625            }
1626        );
1627    }
1628
1629    #[fuchsia::test(allow_stalls = false)]
1630    async fn joined_no_authentication_algorithm() {
1631        let mut m = MockObjects::new().await;
1632        let mut ctx = m.make_ctx_with_bss().await;
1633        let connect_req = ParsedConnectRequest {
1634            selected_bss: fake_bss_description!(Open, bssid: BSSID.to_array()),
1635            connect_failure_timeout: 10,
1636            // use an unsupported AuthenticationType
1637            auth_type: fidl_mlme::AuthenticationTypes::SharedKey,
1638            sae_password: vec![],
1639            wep_key: None,
1640            security_ie: vec![],
1641        };
1642        let mut sta = Client::new(connect_req, *IFACE_MAC, fake_client_capabilities());
1643        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1644        let state = Joined;
1645
1646        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1647        let _state =
1648            state.start_authenticating(&mut sta).await.expect_err("should fail authenticating");
1649
1650        // Verify MLME-CONNECT.confirm message was sent.
1651        let msg = m
1652            .fake_device_state
1653            .lock()
1654            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1655            .expect("expect msg");
1656        assert_eq!(
1657            msg,
1658            fidl_mlme::ConnectConfirm {
1659                peer_sta_address: [6, 6, 6, 6, 6, 6],
1660                result_code: fidl_ieee80211::StatusCode::UnsupportedAuthAlgorithm,
1661                association_id: 0,
1662                association_ies: vec![],
1663            }
1664        );
1665
1666        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1667    }
1668
1669    #[fuchsia::test(allow_stalls = false)]
1670    async fn authenticating_state_auth_rejected() {
1671        let mut m = MockObjects::new().await;
1672        let mut ctx = m.make_ctx_with_bss().await;
1673        let mut sta = make_client_station();
1674        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1675        let mut state = open_authenticating(&mut sta);
1676
1677        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1678        // Verify authentication failed.
1679        assert_matches!(
1680            state
1681                .on_auth_frame(
1682                    &mut sta,
1683                    mac::AuthFrame {
1684                        auth_hdr: mac::AuthHdr {
1685                            auth_alg_num: mac::AuthAlgorithmNumber::OPEN,
1686                            auth_txn_seq_num: 2,
1687                            status_code: fidl_ieee80211::StatusCode::NotInSameBss.into(),
1688                        }
1689                        .as_bytes_ref(),
1690                        elements: &[],
1691                    },
1692                )
1693                .await,
1694            AuthProgress::Failed
1695        );
1696
1697        // Verify MLME-CONNECT.confirm message was sent.
1698        let msg = m
1699            .fake_device_state
1700            .lock()
1701            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1702            .expect("expect msg");
1703        assert_eq!(
1704            msg,
1705            fidl_mlme::ConnectConfirm {
1706                peer_sta_address: BSSID.to_array(),
1707                result_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1708                association_id: 0,
1709                association_ies: vec![],
1710            }
1711        );
1712        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1713    }
1714
1715    #[fuchsia::test(allow_stalls = false)]
1716    async fn authenticating_state_deauth_frame() {
1717        let mut m = MockObjects::new().await;
1718        let mut ctx = m.make_ctx_with_bss().await;
1719        let mut sta = make_client_station();
1720        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1721        let mut state = open_authenticating(&mut sta);
1722
1723        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1724        state
1725            .on_deauth_frame(
1726                &mut sta,
1727                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::NoMoreStas.into() },
1728            )
1729            .await;
1730
1731        // Verify MLME-CONNECT.confirm message was sent.
1732        let msg = m
1733            .fake_device_state
1734            .lock()
1735            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1736            .expect("expect msg");
1737        assert_eq!(
1738            msg,
1739            fidl_mlme::ConnectConfirm {
1740                peer_sta_address: BSSID.to_array(),
1741                result_code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
1742                association_id: 0,
1743                association_ies: vec![],
1744            }
1745        );
1746        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1747    }
1748
1749    #[fuchsia::test(allow_stalls = false)]
1750    async fn associating_success_unprotected() {
1751        let mut m = MockObjects::new().await;
1752        let mut ctx = m.make_ctx_with_bss().await;
1753        let mut sta = make_client_station();
1754        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1755
1756        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1757        let mut state = Associating::default();
1758        let assoc_resp_ies = fake_bss_description!(Wpa2, ies_overrides: IesOverrides::new()
1759            .set(IeType::HT_CAPABILITIES, ie::fake_ht_cap_bytes().to_vec())
1760            .set(IeType::VHT_CAPABILITIES, ie::fake_vht_cap_bytes().to_vec())
1761        )
1762        .ies()
1763        .to_vec();
1764        let Association { aid, controlled_port_open, .. } = state
1765            .on_assoc_resp_frame(
1766                &mut sta,
1767                mac::AssocRespFrame {
1768                    assoc_resp_hdr: mac::AssocRespHdr {
1769                        aid: 42,
1770                        capabilities: mac::CapabilityInfo(52),
1771                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1772                    }
1773                    .as_bytes_ref(),
1774                    elements: &assoc_resp_ies[..],
1775                },
1776            )
1777            .await
1778            .expect("failed processing association response frame");
1779        assert_eq!(aid, 42);
1780        assert_eq!(true, controlled_port_open);
1781
1782        // Verify MLME-CONNECT.confirm message was sent.
1783        let msg = m
1784            .fake_device_state
1785            .lock()
1786            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1787            .expect("no message");
1788        assert_eq!(
1789            msg,
1790            fidl_mlme::ConnectConfirm {
1791                peer_sta_address: BSSID.to_array(),
1792                result_code: fidl_ieee80211::StatusCode::Success,
1793                association_id: 42,
1794                association_ies: assoc_resp_ies,
1795            }
1796        );
1797        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1798    }
1799
1800    #[fuchsia::test(allow_stalls = false)]
1801    async fn associating_success_protected() {
1802        let mut m = MockObjects::new().await;
1803        let mut ctx = m.make_ctx_with_bss().await;
1804        let mut sta = make_protected_client_station();
1805        sta.client_capabilities.0.capability_info =
1806            mac::CapabilityInfo(0).with_ess(true).with_ibss(true);
1807        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1808        let mut state = Associating::default();
1809
1810        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1811        let assoc_resp_ies =
1812            fake_bss_description!(Wpa2, bssid: BSSID.to_array(), ies_overrides: IesOverrides::new()
1813                .set(IeType::HT_CAPABILITIES, ie::fake_ht_cap_bytes().to_vec())
1814                .set(IeType::VHT_CAPABILITIES, ie::fake_vht_cap_bytes().to_vec())
1815            )
1816            .ies()
1817            .to_vec();
1818        let Association { aid, controlled_port_open, .. } = state
1819            .on_assoc_resp_frame(
1820                &mut sta,
1821                mac::AssocRespFrame {
1822                    assoc_resp_hdr: mac::AssocRespHdr {
1823                        aid: 42,
1824                        capabilities: mac::CapabilityInfo(0).with_ibss(true).with_cf_pollable(true),
1825                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1826                    }
1827                    .as_bytes_ref(),
1828                    elements: &assoc_resp_ies[..],
1829                },
1830            )
1831            .await
1832            .expect("failed processing association response frame");
1833        assert_eq!(aid, 42);
1834        assert_eq!(false, controlled_port_open);
1835
1836        // Verify association context is set
1837        assert_eq!(m.fake_device_state.lock().assocs.len(), 1);
1838
1839        let assoc_cfg = m
1840            .fake_device_state
1841            .lock()
1842            .assocs
1843            .get(&(*BSSID).into())
1844            .expect("expect assoc ctx to be set")
1845            .clone();
1846        assert_eq!(assoc_cfg.aid, Some(42));
1847        assert_eq!(assoc_cfg.qos, Some(true));
1848        assert_eq!(
1849            assoc_cfg.rates,
1850            Some(vec![0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c])
1851        );
1852        assert_eq!(assoc_cfg.capability_info, Some(2));
1853        assert!(assoc_cfg.ht_cap.is_some());
1854        assert!(assoc_cfg.vht_cap.is_some());
1855        assert!(assoc_cfg.ht_op.is_some());
1856        assert!(assoc_cfg.vht_op.is_some());
1857
1858        // Verify MLME-CONNECT.confirm message was sent.
1859        let msg = m
1860            .fake_device_state
1861            .lock()
1862            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1863            .expect("no message");
1864        assert_eq!(
1865            msg,
1866            fidl_mlme::ConnectConfirm {
1867                peer_sta_address: BSSID.to_array(),
1868                result_code: fidl_ieee80211::StatusCode::Success,
1869                association_id: 42,
1870                association_ies: assoc_resp_ies,
1871            }
1872        );
1873        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1874    }
1875
1876    #[fuchsia::test(allow_stalls = false)]
1877    async fn associating_failure_due_to_failed_status_code() {
1878        let mut m = MockObjects::new().await;
1879        let mut ctx = m.make_ctx_with_bss().await;
1880        let mut sta = make_client_station();
1881        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1882
1883        let mut state = Associating::default();
1884
1885        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1886        // Verify authentication was considered successful.
1887        state
1888            .on_assoc_resp_frame(
1889                &mut sta,
1890                mac::AssocRespFrame {
1891                    assoc_resp_hdr: mac::AssocRespHdr {
1892                        aid: 42,
1893                        capabilities: mac::CapabilityInfo(52),
1894                        status_code: fidl_ieee80211::StatusCode::NotInSameBss.into(),
1895                    }
1896                    .as_bytes_ref(),
1897                    elements: &[][..],
1898                },
1899            )
1900            .await
1901            .expect_err("expected failure processing association response frame");
1902
1903        // Verify MLME-CONNECT.confirm message was sent.
1904        let msg = m
1905            .fake_device_state
1906            .lock()
1907            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1908            .expect("no message");
1909        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::NotInSameBss));
1910        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1911    }
1912
1913    #[fuchsia::test(allow_stalls = false)]
1914    async fn associating_failure_due_to_incompatibility() {
1915        let mut m = MockObjects::new().await;
1916        let mut ctx = m.make_ctx_with_bss().await;
1917        let mut sta = make_client_station();
1918        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1919
1920        let mut state = Associating::default();
1921
1922        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1923        state
1924            .on_assoc_resp_frame(
1925                &mut sta,
1926                mac::AssocRespFrame {
1927                    assoc_resp_hdr: mac::AssocRespHdr {
1928                        aid: 42,
1929                        capabilities: mac::CapabilityInfo(52),
1930                        status_code: fidl_ieee80211::StatusCode::Success.into(),
1931                    }
1932                    .as_bytes_ref(),
1933                    elements: fake_bss_description!(Wpa2, rates: vec![0x81]).ies(),
1934                },
1935            )
1936            .await
1937            .expect_err("expected failure processing association response frame");
1938
1939        // Verify MLME-CONNECT.confirm message was sent.
1940        let msg = m
1941            .fake_device_state
1942            .lock()
1943            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1944            .expect("no message");
1945        assert_eq!(
1946            msg,
1947            connect_conf_failure(fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch)
1948        );
1949        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1950    }
1951
1952    #[fuchsia::test(allow_stalls = false)]
1953    async fn associating_deauth_frame() {
1954        let mut m = MockObjects::new().await;
1955        let mut ctx = m.make_ctx_with_bss().await;
1956        let mut sta = make_client_station();
1957        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1958
1959        let mut state = Associating::default();
1960
1961        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1962        state
1963            .on_deauth_frame(
1964                &mut sta,
1965                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
1966            )
1967            .await;
1968
1969        // Verify MLME-CONNECT.confirm message was sent.
1970        let msg = m
1971            .fake_device_state
1972            .lock()
1973            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1974            .expect("no message");
1975        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc));
1976        assert!(m.fake_device_state.lock().join_bss_request.is_none());
1977    }
1978
1979    #[fuchsia::test(allow_stalls = false)]
1980    async fn associating_disassociation() {
1981        let mut m = MockObjects::new().await;
1982        let mut ctx = m.make_ctx_with_bss().await;
1983        let mut sta = make_client_station();
1984        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
1985
1986        let mut state = Associating::default();
1987
1988        assert!(m.fake_device_state.lock().join_bss_request.is_some());
1989        state.on_disassoc_frame(
1990            &mut sta,
1991            &mac::DisassocHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
1992        );
1993
1994        // Verify MLME-CONNECT.confirm message was sent.
1995        let msg = m
1996            .fake_device_state
1997            .lock()
1998            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
1999            .expect("no message");
2000        assert_eq!(msg, connect_conf_failure(fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc));
2001        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2002    }
2003
2004    fn mock_rx_info<'a>(client: &BoundClient<'a, FakeDevice>) -> fidl_softmac::WlanRxInfo {
2005        let channel = client.channel_state.get_main_channel().unwrap();
2006        MockWlanRxInfo::with_channel(channel).into()
2007    }
2008
2009    #[fuchsia::test(allow_stalls = false)]
2010    async fn associated_block_ack_frame() {
2011        let mut mock = MockObjects::new().await;
2012        let mut ctx = mock.make_ctx().await;
2013        let mut station = make_client_station();
2014        let mut client = station.bind(&mut ctx, &mut mock.scanner, &mut mock.channel_state);
2015
2016        let frame = {
2017            let mut buffer = [0u8; ADDBA_REQ_FRAME_LEN];
2018            let writer = BufferWriter::new(&mut buffer[..]);
2019            let mut writer = append_frame_to!(
2020                writer,
2021                {
2022                    headers: {
2023                        mac::MgmtHdr: &mgmt_writer::mgmt_hdr_from_ap(
2024                            mac::FrameControl(0)
2025                                .with_frame_type(mac::FrameType::MGMT)
2026                                .with_mgmt_subtype(mac::MgmtSubtype::ACTION),
2027                            client.sta.iface_mac,
2028                            client.sta.bssid(),
2029                            mac::SequenceControl(0)
2030                                .with_seq_num(client.ctx.seq_mgr.next_sns1(&client.sta.bssid().into()) as u16),
2031                        ),
2032                    },
2033                }
2034            )
2035            .unwrap();
2036            write_addba_req_body(&mut writer, 1).unwrap();
2037            buffer
2038        };
2039
2040        let state = States::from(statemachine::testing::new_state(Associated(empty_association(
2041            &mut client,
2042        ))));
2043        let rx_info = mock_rx_info(&client);
2044        match state.on_mac_frame(&mut client, &frame[..], rx_info, 0.into()).await {
2045            States::Associated(state) => {
2046                let (_, associated) = state.release_data();
2047                // TODO(https://fxbug.dev/42104687): Handle BlockAck frames. The following code has been
2048                //                        altered as part of a fix for https://fxbug.dev/42180615. This check
2049                //                        should ensure that the state has transitioned to
2050                //                        `Established`, but since the state machine has been
2051                //                        disabled it instead checks that the state has remained
2052                //                        `Closed`.
2053                match *associated.0.block_ack_state.as_ref() {
2054                    BlockAckState::Closed(_) => {}
2055                    _ => panic!("client has transitioned BlockAck"),
2056                }
2057            }
2058            _ => panic!("client no longer associated"),
2059        }
2060    }
2061
2062    #[fuchsia::test(allow_stalls = false)]
2063    async fn associated_deauth_frame() {
2064        let mut m = MockObjects::new().await;
2065        let mut ctx = m.make_ctx_with_bss().await;
2066        let mut sta = make_client_station();
2067        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2068        let mut state = Associated(empty_association(&mut sta));
2069
2070        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2071
2072        // Association configuration will be cleared when MLME receives deauth frame.
2073        sta.ctx
2074            .device
2075            .notify_association_complete(fake_assoc_cfg())
2076            .await
2077            .expect("valid assoc_cfg should succeed");
2078        assert_eq!(1, m.fake_device_state.lock().assocs.len());
2079
2080        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
2081        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
2082
2083        let _joined = state
2084            .on_deauth_frame(
2085                &mut sta,
2086                &mac::DeauthHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
2087            )
2088            .await;
2089
2090        // Verify MLME-ASSOCIATE.confirm message was sent.
2091        let msg = m
2092            .fake_device_state
2093            .lock()
2094            .next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
2095            .expect("no message");
2096        assert_eq!(
2097            msg,
2098            fidl_mlme::DeauthenticateIndication {
2099                peer_sta_address: BSSID.to_array(),
2100                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2101                locally_initiated: false,
2102            }
2103        );
2104        // Verify ethernet port is shut down.
2105        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
2106        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2107    }
2108
2109    #[fuchsia::test(allow_stalls = false)]
2110    async fn associated_disassociation() {
2111        let mut m = MockObjects::new().await;
2112        let mut ctx = m.make_ctx_with_bss().await;
2113        let mut sta = make_client_station();
2114        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2115        let mut state = Associated(empty_association(&mut sta));
2116
2117        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2118        state.0.controlled_port_open = true;
2119
2120        sta.ctx
2121            .device
2122            .notify_association_complete(fake_assoc_cfg())
2123            .await
2124            .expect("valid assoc_cfg should succeed");
2125        assert_eq!(1, m.fake_device_state.lock().assocs.len());
2126
2127        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
2128        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
2129
2130        let _authenticated = state
2131            .on_disassoc_frame(
2132                &mut sta,
2133                &mac::DisassocHdr { reason_code: fidl_ieee80211::ReasonCode::ApInitiated.into() },
2134            )
2135            .await;
2136
2137        // Verify MLME-ASSOCIATE.confirm message was sent.
2138        let msg = m
2139            .fake_device_state
2140            .lock()
2141            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
2142            .expect("no message");
2143        assert_eq!(
2144            msg,
2145            fidl_mlme::DisassociateIndication {
2146                peer_sta_address: BSSID.to_array(),
2147                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
2148                locally_initiated: false,
2149            }
2150        );
2151
2152        // Verify ethernet port is shut down.
2153        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
2154        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2155    }
2156
2157    #[fuchsia::test(allow_stalls = false)]
2158    async fn associated_move_data_closed_controlled_port() {
2159        let mut m = MockObjects::new().await;
2160        let mut ctx = m.make_ctx().await;
2161        let mut sta = make_client_station();
2162        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2163        let state = Associated(empty_association(&mut sta));
2164
2165        let bytes = make_data_frame_single_llc(None, None);
2166        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2167        state.on_data_frame(&mut sta, data_frame, 0.into());
2168
2169        // Verify data frame was dropped.
2170        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2171    }
2172
2173    #[fuchsia::test(allow_stalls = false)]
2174    async fn associated_move_data_opened_controlled_port() {
2175        let mut m = MockObjects::new().await;
2176        let mut ctx = m.make_ctx().await;
2177        let mut sta = make_client_station();
2178        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2179        let state =
2180            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2181
2182        let bytes = make_data_frame_single_llc(None, None);
2183        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2184        state.on_data_frame(&mut sta, data_frame, 0.into());
2185
2186        // Verify data frame was processed.
2187        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
2188        #[rustfmt::skip]
2189        assert_eq!(m.fake_device_state.lock().eth_queue[0], [
2190            3, 3, 3, 3, 3, 3, // dst_addr
2191            4, 4, 4, 4, 4, 4, // src_addr
2192            9, 10, // ether_type
2193            11, 11, 11, // payload
2194        ]);
2195    }
2196
2197    #[fuchsia::test(allow_stalls = false)]
2198    async fn associated_skip_empty_data() {
2199        let mut m = MockObjects::new().await;
2200        let mut ctx = m.make_ctx().await;
2201        let mut sta = make_client_station();
2202        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2203        let state =
2204            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2205
2206        let bytes = make_data_frame_single_llc_payload(None, None, &[]);
2207        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2208        state.on_data_frame(&mut sta, data_frame, 0.into());
2209
2210        // Verify data frame was discarded.
2211        assert!(m.fake_device_state.lock().eth_queue.is_empty());
2212    }
2213
2214    #[test_case(true, true; "port open and protected")]
2215    #[test_case(false, true; "port closed and protected")]
2216    #[test_case(true, false; "port open and unprotected")]
2217    #[test_case(false, false; "port closed and unprotected (not a typical state)")]
2218    #[fuchsia::test(allow_stalls = false)]
2219    async fn associated_send_keep_alive_after_null_data_frame(
2220        controlled_port_open: bool,
2221        protected: bool,
2222    ) {
2223        let mut m = MockObjects::new().await;
2224        let mut ctx = m.make_ctx().await;
2225        let mut sta =
2226            if protected { make_protected_client_station() } else { make_client_station() };
2227        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2228        let state = Associated(Association { controlled_port_open, ..empty_association(&mut sta) });
2229
2230        let bytes = make_null_data_frame();
2231        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2232        state.on_data_frame(&mut sta, data_frame, 0.into());
2233
2234        // Verify data frame was not forwarded up.
2235        assert!(m.fake_device_state.lock().eth_queue.is_empty());
2236        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2237        let bytes = &m.fake_device_state.lock().wlan_queue[0].0;
2238        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2239        let frame_ctrl = data_frame.frame_ctrl();
2240        assert_eq!(frame_ctrl.to_ds(), true);
2241        assert_eq!(frame_ctrl.from_ds(), false);
2242        assert!(data_frame.body.is_empty());
2243    }
2244
2245    #[fuchsia::test(allow_stalls = false)]
2246    async fn associated_handle_eapol_closed_controlled_port() {
2247        let mut m = MockObjects::new().await;
2248        let mut ctx = m.make_ctx().await;
2249        let mut sta = make_protected_client_station();
2250        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2251        let state = Associated(empty_association(&mut sta));
2252
2253        let (src_addr, dst_addr, bytes) = make_eapol_frame(*IFACE_MAC);
2254        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2255        state.on_data_frame(&mut sta, data_frame, 0.into());
2256
2257        // Verify EAPOL frame was not sent to netstack.
2258        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2259
2260        // Verify EAPoL frame was sent to SME.
2261        let eapol_ind = m
2262            .fake_device_state
2263            .lock()
2264            .next_mlme_msg::<fidl_mlme::EapolIndication>()
2265            .expect("error reading EAPOL.indication");
2266        assert_eq!(
2267            eapol_ind,
2268            fidl_mlme::EapolIndication {
2269                src_addr: src_addr.to_array(),
2270                dst_addr: dst_addr.to_array(),
2271                data: EAPOL_PDU.to_vec()
2272            }
2273        );
2274    }
2275
2276    #[fuchsia::test(allow_stalls = false)]
2277    async fn associated_handle_eapol_open_controlled_port() {
2278        let mut m = MockObjects::new().await;
2279        let mut ctx = m.make_ctx().await;
2280        let mut sta = make_protected_client_station();
2281        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2282        let state = Associated(empty_association(&mut sta));
2283
2284        let (src_addr, dst_addr, bytes) = make_eapol_frame(*IFACE_MAC);
2285        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2286        state.on_data_frame(&mut sta, data_frame, 0.into());
2287
2288        // Verify EAPOL frame was not sent to netstack.
2289        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2290
2291        // Verify EAPoL frame was sent to SME.
2292        let eapol_ind = m
2293            .fake_device_state
2294            .lock()
2295            .next_mlme_msg::<fidl_mlme::EapolIndication>()
2296            .expect("error reading EAPOL.indication");
2297        assert_eq!(
2298            eapol_ind,
2299            fidl_mlme::EapolIndication {
2300                src_addr: src_addr.to_array(),
2301                dst_addr: dst_addr.to_array(),
2302                data: EAPOL_PDU.to_vec()
2303            }
2304        );
2305    }
2306
2307    #[fuchsia::test(allow_stalls = false)]
2308    async fn associated_handle_amsdus_open_controlled_port() {
2309        let mut m = MockObjects::new().await;
2310        let mut ctx = m.make_ctx().await;
2311        let mut sta = make_protected_client_station();
2312        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2313        let state =
2314            Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
2315
2316        let bytes = make_data_frame_amsdu();
2317        let data_frame = mac::DataFrame::parse(bytes.as_slice(), false).unwrap();
2318        state.on_data_frame(&mut sta, data_frame, 0.into());
2319
2320        let queue = &m.fake_device_state.lock().eth_queue;
2321        assert_eq!(queue.len(), 2);
2322        #[rustfmt::skip]
2323        let mut expected_first_eth_frame = vec![
2324            0x78, 0x8a, 0x20, 0x0d, 0x67, 0x03, // dst_addr
2325            0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xab, // src_addr
2326            0x08, 0x00, // ether_type
2327        ];
2328        expected_first_eth_frame.extend_from_slice(MSDU_1_PAYLOAD);
2329        assert_eq!(queue[0], &expected_first_eth_frame[..]);
2330        #[rustfmt::skip]
2331        let mut expected_second_eth_frame = vec![
2332            0x78, 0x8a, 0x20, 0x0d, 0x67, 0x04, // dst_addr
2333            0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xac, // src_addr
2334            0x08, 0x01, // ether_type
2335        ];
2336        expected_second_eth_frame.extend_from_slice(MSDU_2_PAYLOAD);
2337        assert_eq!(queue[1], &expected_second_eth_frame[..]);
2338    }
2339
2340    #[fuchsia::test(allow_stalls = false)]
2341    async fn associated_request_bu_data_frame() {
2342        let mut m = MockObjects::new().await;
2343        let mut ctx = m.make_ctx().await;
2344        let mut sta = make_client_station();
2345        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2346        let state = Associated(Association {
2347            aid: 42,
2348            controlled_port_open: true,
2349            ..empty_association(&mut sta)
2350        });
2351
2352        let mut bytes = make_data_frame_single_llc(None, None);
2353        let mut data_frame = mac::DataFrame::parse(bytes.as_mut_slice(), false).unwrap();
2354        data_frame.fixed_fields.frame_ctrl =
2355            data_frame.fixed_fields.frame_ctrl.with_more_data(true);
2356        state.on_data_frame(&mut sta, data_frame, 0.into());
2357
2358        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2359        #[rustfmt::skip]
2360        assert_eq!(&m
2361            .fake_device_state.lock().wlan_queue[0].0[..], &[
2362            // Frame Control:
2363            0b10100100, 0b00000000, // FC
2364            42, 0b11_000000, // Id
2365            6, 6, 6, 6, 6, 6, // addr1
2366            3, 3, 3, 3, 3, 3, // addr2
2367        ][..]);
2368    }
2369
2370    #[fuchsia::test(allow_stalls = false)]
2371    async fn associated_request_bu_mgmt_frame() {
2372        let mut m = MockObjects::new().await;
2373        let mut ctx = m.make_ctx().await;
2374        let mut sta = make_client_station();
2375        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2376        let state = Associated(Association {
2377            aid: 42,
2378            controlled_port_open: true,
2379            ..empty_association(&mut sta)
2380        });
2381
2382        state.on_any_mgmt_frame(
2383            &mut sta,
2384            &mac::MgmtHdr {
2385                frame_ctrl: mac::FrameControl(0)
2386                    .with_frame_type(mac::FrameType::MGMT)
2387                    .with_mgmt_subtype(mac::MgmtSubtype::BEACON)
2388                    .with_more_data(true),
2389                duration: 0,
2390                addr1: [3; 6].into(),
2391                addr2: (*BSSID).into(),
2392                addr3: (*BSSID).into(),
2393                seq_ctrl: mac::SequenceControl(0),
2394            },
2395        );
2396
2397        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2398        #[rustfmt::skip]
2399        assert_eq!(&m
2400            .fake_device_state.lock().wlan_queue[0].0[..], &[
2401            // Frame Control:
2402            0b10100100, 0b00000000, // FC
2403            42, 0b11_000000, // Id
2404            6, 6, 6, 6, 6, 6, // addr1
2405            3, 3, 3, 3, 3, 3, // addr2
2406        ][..]);
2407    }
2408
2409    #[fuchsia::test(allow_stalls = false)]
2410    async fn associated_no_bu_request() {
2411        let mut m = MockObjects::new().await;
2412        let mut ctx = m.make_ctx().await;
2413        let mut sta = make_client_station();
2414        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2415
2416        // Closed Controlled port
2417        let state = Associated(empty_association(&mut sta));
2418        let mut bytes = make_data_frame_single_llc(None, None);
2419        let mut data_frame = mac::DataFrame::parse(bytes.as_mut_slice(), false).unwrap();
2420        data_frame.fixed_fields.frame_ctrl =
2421            data_frame.fixed_fields.frame_ctrl.with_more_data(true);
2422        state.on_data_frame(&mut sta, data_frame, 0.into());
2423        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
2424
2425        // Foreign management frame
2426        let state = States::from(statemachine::testing::new_state(Associated(Association {
2427            controlled_port_open: true,
2428            ..empty_association(&mut sta)
2429        })));
2430        #[rustfmt::skip]
2431        let beacon = vec![
2432            // Mgmt Header:
2433            0b1000_00_00, 0b00100000, // Frame Control
2434            0, 0, // Duration
2435            3, 3, 3, 3, 3, 3, // Addr1
2436            7, 7, 7, 7, 7, 7, // Addr2
2437            5, 5, 5, 5, 5, 5, // Addr3
2438            0x10, 0, // Sequence Control
2439            // Omit IEs
2440        ];
2441        let rx_info = mock_rx_info(&sta);
2442        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
2443        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
2444    }
2445
2446    #[fuchsia::test(allow_stalls = false)]
2447    async fn associated_drop_foreign_data_frames() {
2448        let mut m = MockObjects::new().await;
2449        let mut ctx = m.make_ctx().await;
2450        let mut sta = make_client_station();
2451        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2452
2453        // Foreign data frame
2454        let state = States::from(statemachine::testing::new_state(Associated(Association {
2455            aid: 42,
2456            controlled_port_open: true,
2457            ..empty_association(&mut sta)
2458        })));
2459        let fc = mac::FrameControl(0)
2460            .with_frame_type(mac::FrameType::DATA)
2461            .with_data_subtype(mac::DataSubtype(0))
2462            .with_from_ds(true);
2463        let fc = fc.0.to_le_bytes();
2464        // Send data frame from an address other than the BSSID([6u8; 6]).
2465        let bytes = vec![
2466            // Data Header
2467            fc[0], fc[1], // fc
2468            2, 2, // duration
2469            3, 3, 3, 3, 3, 3, // addr1
2470            4, 4, 4, 4, 4, 4, // addr2
2471            5, 5, 5, 5, 5, 5, // addr3
2472            6, 6, // sequence control
2473            // LLC Header
2474            7, 7, 7, // DSAP, SSAP & control
2475            8, 8, 8, // OUI
2476            9, 10, // eth type
2477            // Trailing bytes
2478            11, 11, 11,
2479        ];
2480        let rx_info = mock_rx_info(&sta);
2481        state.on_mac_frame(&mut sta, &bytes[..], rx_info, 0.into()).await;
2482        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 0);
2483    }
2484
2485    #[fuchsia::test(allow_stalls = false)]
2486    async fn state_transitions_joined_state_reconnect_denied() {
2487        let mut m = MockObjects::new().await;
2488        let mut ctx = m.make_ctx().await;
2489        let mut sta = make_client_station();
2490        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2491        let mut state = States::from(statemachine::testing::new_state(Joined));
2492
2493        // (sme->mlme) Send a reconnect request
2494        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2495            peer_sta_address: [1, 2, 3, 4, 5, 6],
2496        });
2497        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2498
2499        assert_matches!(state, States::Joined(_), "not in joined state");
2500
2501        // Verify MLME-CONNECT.confirm message was sent.
2502        let msg = m
2503            .fake_device_state
2504            .lock()
2505            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2506            .expect("expect msg");
2507        assert_eq!(
2508            msg,
2509            fidl_mlme::ConnectConfirm {
2510                peer_sta_address: [1, 2, 3, 4, 5, 6],
2511                result_code: fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
2512                association_id: 0,
2513                association_ies: vec![],
2514            }
2515        );
2516    }
2517
2518    #[fuchsia::test(allow_stalls = false)]
2519    async fn state_transitions_authing_success() {
2520        let mut m = MockObjects::new().await;
2521        let mut ctx = m.make_ctx().await;
2522        let mut sta = make_client_station();
2523        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2524        let mut state =
2525            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2526
2527        // Successful: Joined > Authenticating > Associating
2528        #[rustfmt::skip]
2529        let auth_resp_success = vec![
2530            // Mgmt Header:
2531            0b1011_00_00, 0b00000000, // Frame Control
2532            0, 0, // Duration
2533            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2534            3, 3, 3, 3, 3, 3, // Addr2
2535            6, 6, 6, 6, 6, 6, // Addr3
2536            0x10, 0, // Sequence Control
2537            // Auth Header:
2538            0, 0, // Algorithm Number (Open)
2539            2, 0, // Txn Sequence Number
2540            0, 0, // Status Code
2541        ];
2542        let rx_info = mock_rx_info(&sta);
2543        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2544        assert_matches!(state, States::Associating(_), "not in associating state");
2545    }
2546
2547    #[fuchsia::test(allow_stalls = false)]
2548    async fn state_transitions_authing_failure() {
2549        let mut m = MockObjects::new().await;
2550        let mut ctx = m.make_ctx_with_bss().await;
2551        let mut sta = make_client_station();
2552        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2553        let mut state =
2554            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2555
2556        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2557        // Failure: Joined > Authenticating > Joined
2558        #[rustfmt::skip]
2559        let auth_resp_failure = vec![
2560            // Mgmt Header:
2561            0b1011_00_00, 0b00000000, // Frame Control
2562            0, 0, // Duration
2563            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2564            3, 3, 3, 3, 3, 3, // Addr2
2565            6, 6, 6, 6, 6, 6, // Addr3
2566            0x10, 0, // Sequence Control
2567            // Auth Header:
2568            0, 0, // Algorithm Number (Open)
2569            2, 0, // Txn Sequence Number
2570            42, 0, // Status Code
2571        ];
2572        let rx_info = mock_rx_info(&sta);
2573        state = state.on_mac_frame(&mut sta, &auth_resp_failure[..], rx_info, 0.into()).await;
2574        assert_matches!(state, States::Joined(_), "not in joined state");
2575        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2576    }
2577
2578    #[fuchsia::test(allow_stalls = false)]
2579    async fn state_transitions_authing_deauth() {
2580        let mut m = MockObjects::new().await;
2581        let mut ctx = m.make_ctx_with_bss().await;
2582        let mut sta = make_client_station();
2583        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2584        let mut state =
2585            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2586
2587        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2588        // Deauthenticate: Authenticating > Joined
2589        #[rustfmt::skip]
2590        let deauth = vec![
2591            // Mgmt Header:
2592            0b1100_00_00, 0b00000000, // Frame Control
2593            0, 0, // Duration
2594            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2595            3, 3, 3, 3, 3, 3, // Addr2
2596            6, 6, 6, 6, 6, 6, // Addr3
2597            0x10, 0, // Sequence Control
2598            // Deauth Header:
2599            5, 0, // Algorithm Number (Open)
2600        ];
2601        let rx_info = mock_rx_info(&sta);
2602        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
2603        assert_matches!(state, States::Joined(_), "not in joined state");
2604        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2605    }
2606
2607    #[fuchsia::test(allow_stalls = false)]
2608    async fn state_transitions_foreign_auth_resp() {
2609        let mut m = MockObjects::new().await;
2610        let mut ctx = m.make_ctx().await;
2611        let mut sta = make_client_station();
2612        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2613        let mut state =
2614            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2615
2616        // Send foreign auth response. State should not change.
2617        #[rustfmt::skip]
2618        let auth_resp_success = vec![
2619            // Mgmt Header:
2620            0b1011_00_00, 0b00000000, // Frame Control
2621            0, 0, // Duration
2622            5, 5, 5, 5, 5, 5, // Addr1
2623            3, 3, 3, 3, 3, 3, // Addr2
2624            5, 5, 5, 5, 5, 5, // Addr3
2625            0x10, 0, // Sequence Control
2626            // Auth Header:
2627            0, 0, // Algorithm Number (Open)
2628            2, 0, // Txn Sequence Number
2629            0, 0, // Status Code
2630        ];
2631        let rx_info = mock_rx_info(&sta);
2632        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2633        assert_matches!(state, States::Authenticating(_), "not in authenticating state");
2634
2635        // Verify that an authentication response from the joined BSS still moves the Client
2636        // forward.
2637        #[rustfmt::skip]
2638        let auth_resp_success = vec![
2639            // Mgmt Header:
2640            0b1011_00_00, 0b00000000, // Frame Control
2641            0, 0, // Duration
2642            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2643            3, 3, 3, 3, 3, 3, // Addr2
2644            6, 6, 6, 6, 6, 6, // Addr3
2645            0x10, 0, // Sequence Control
2646            // Auth Header:
2647            0, 0, // Algorithm Number (Open)
2648            2, 0, // Txn Sequence Number
2649            0, 0, // Status Code
2650        ];
2651        let rx_info = mock_rx_info(&sta);
2652        state = state.on_mac_frame(&mut sta, &auth_resp_success[..], rx_info, 0.into()).await;
2653        assert_matches!(state, States::Associating(_), "not in associating state");
2654    }
2655
2656    #[fuchsia::test(allow_stalls = false)]
2657    async fn state_transitions_authing_state_reconnect_denied() {
2658        let mut m = MockObjects::new().await;
2659        let mut ctx = m.make_ctx().await;
2660        let mut sta = make_client_station();
2661        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2662        let mut state =
2663            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2664
2665        // (sme->mlme) Send a reconnect request
2666        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2667            peer_sta_address: [1, 2, 3, 4, 5, 6],
2668        });
2669        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2670
2671        assert_matches!(state, States::Authenticating(_), "not in authenticating state");
2672
2673        // Verify MLME-CONNECT.confirm message was sent.
2674        let msg = m
2675            .fake_device_state
2676            .lock()
2677            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2678            .expect("expect msg");
2679        assert_eq!(
2680            msg,
2681            fidl_mlme::ConnectConfirm {
2682                peer_sta_address: [1, 2, 3, 4, 5, 6],
2683                result_code: fidl_ieee80211::StatusCode::DeniedNoAssociationExists,
2684                association_id: 0,
2685                association_ies: vec![],
2686            }
2687        );
2688    }
2689
2690    #[fuchsia::test(allow_stalls = false)]
2691    async fn state_transitions_authing_state_wrong_algorithm() {
2692        let mut m = MockObjects::new().await;
2693        let mut ctx = m.make_ctx().await;
2694        let mut sta = make_client_station();
2695        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2696        let mut state =
2697            States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
2698
2699        #[rustfmt::skip]
2700        let auth_resp_wrong = vec![
2701            // Mgmt Header:
2702            0b1011_00_00, 0b00000000, // Frame Control
2703            0, 0, // Duration
2704            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2705            3, 3, 3, 3, 3, 3, // Addr2
2706            6, 6, 6, 6, 6, 6, // Addr3
2707            0x10, 0, // Sequence Control
2708            // Auth Header:
2709            8, 0, // Algorithm Number (wrong algorithm: SAE)
2710            2, 0, // Txn Sequence Number
2711            0, 0, // Status Code
2712        ];
2713        let rx_info = mock_rx_info(&sta);
2714        state = state.on_mac_frame(&mut sta, &auth_resp_wrong[..], rx_info, 0.into()).await;
2715        assert_matches!(state, States::Joined(_), "not in joined state");
2716        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2717    }
2718
2719    #[fuchsia::test(allow_stalls = false)]
2720    async fn state_transitions_associng_success() {
2721        let mut m = MockObjects::new().await;
2722        let mut ctx = m.make_ctx().await;
2723        let mut sta = make_client_station();
2724        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2725        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2726
2727        // Successful: Associating > Associated
2728        #[rustfmt::skip]
2729        let assoc_resp_success = vec![
2730            // Mgmt Header:
2731            0b0001_00_00, 0b00000000, // Frame Control
2732            0, 0, // Duration
2733            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2734            3, 3, 3, 3, 3, 3, // Addr2
2735            6, 6, 6, 6, 6, 6, // Addr3
2736            0x10, 0, // Sequence Control
2737            // Assoc Resp Header:
2738            0, 0, // Capabilities
2739            0, 0, // Status Code
2740            0, 0, // AID
2741            // IEs
2742            // Basic Rates
2743            0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
2744            // HT Capabilities
2745            0x2d, 0x1a, 0xef, 0x09, // HT capabilities info
2746            0x17, // A-MPDU parameters
2747            0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2748            // VHT Capabilities
2749            0xbf, 0x0c, 0x91, 0x59, 0x82, 0x0f, // VHT capabilities info
2750            0xea, 0xff, 0x00, 0x00, 0xea, 0xff, 0x00, 0x00, // VHT supported MCS set
2751        ];
2752        let rx_info = mock_rx_info(&sta);
2753        state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], rx_info, 0.into()).await;
2754        assert_matches!(state, States::Associated(_), "not in associated state");
2755    }
2756
2757    #[fuchsia::test(allow_stalls = false)]
2758    async fn state_transitions_associng_failure() {
2759        let mut m = MockObjects::new().await;
2760        let mut ctx = m.make_ctx_with_bss().await;
2761        let mut sta = make_client_station();
2762        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2763        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2764
2765        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2766        // Failure: Associating > Joined
2767        #[rustfmt::skip]
2768        let assoc_resp_failure = vec![
2769            // Mgmt Header:
2770            0b0001_00_00, 0b00000000, // Frame Control
2771            0, 0, // Duration
2772            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2773            3, 3, 3, 3, 3, 3, // Addr2
2774            6, 6, 6, 6, 6, 6, // Addr3
2775            0x10, 0, // Sequence Control
2776            // Assoc Resp Header:
2777            0, 0, // Capabilities
2778            2, 0, // Status Code (Failed)
2779            0, 0, // AID
2780        ];
2781        let rx_info = mock_rx_info(&sta);
2782        state = state.on_mac_frame(&mut sta, &assoc_resp_failure[..], rx_info, 0.into()).await;
2783        assert_matches!(state, States::Joined(_), "not in joined state");
2784        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2785    }
2786
2787    #[fuchsia::test(allow_stalls = false)]
2788    async fn state_transitions_associng_deauthing() {
2789        let mut m = MockObjects::new().await;
2790        let mut ctx = m.make_ctx_with_bss().await;
2791        let mut sta = make_client_station();
2792        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2793        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2794
2795        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2796        // Deauthentication: Associating > Joined
2797        #[rustfmt::skip]
2798        let deauth = vec![
2799            // Mgmt Header:
2800            0b1100_00_00, 0b00000000, // Frame Control
2801            0, 0, // Duration
2802            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2803            3, 3, 3, 3, 3, 3, // Addr2
2804            6, 6, 6, 6, 6, 6, // Addr3
2805            0x10, 0, // Sequence Control
2806            // Deauth Header:
2807            4, 0, // Reason Code
2808        ];
2809        let rx_info = mock_rx_info(&sta);
2810        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
2811        assert_matches!(state, States::Joined(_), "not in joined state");
2812        assert!(m.fake_device_state.lock().join_bss_request.is_none());
2813    }
2814
2815    #[fuchsia::test(allow_stalls = false)]
2816    async fn state_transitions_associng_reconnect_no_op() {
2817        let mut m = MockObjects::new().await;
2818        let mut ctx = m.make_ctx_with_bss().await;
2819        let mut sta = make_client_station();
2820        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2821        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2822
2823        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2824        // (sme->mlme) Send a reconnect request
2825        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2826            peer_sta_address: BSSID.to_array(),
2827        });
2828        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2829        assert_matches!(state, States::Associating(_), "not in associating state");
2830        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2831
2832        // Verify no connect conf is sent
2833        m.fake_device_state
2834            .lock()
2835            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2836            .expect_err("unexpected Connect.confirm");
2837    }
2838
2839    #[fuchsia::test(allow_stalls = false)]
2840    async fn state_transitions_associng_reconnect_denied() {
2841        let mut m = MockObjects::new().await;
2842        let mut ctx = m.make_ctx_with_bss().await;
2843        let mut sta = make_client_station();
2844        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2845        let mut state = States::from(statemachine::testing::new_state(Associating::default()));
2846
2847        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2848        // (sme->mlme) Send a reconnect request
2849        let sus_bssid = [b's', b'u', b's', b'r', b'e', b'q'];
2850        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2851            peer_sta_address: sus_bssid,
2852        });
2853        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2854        assert_matches!(state, States::Associating(_), "not in associating state");
2855        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2856
2857        // Verify a connect conf was sent
2858        let connect_conf = m
2859            .fake_device_state
2860            .lock()
2861            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2862            .expect("error reading Connect.confirm");
2863        assert_eq!(
2864            connect_conf,
2865            fidl_mlme::ConnectConfirm {
2866                peer_sta_address: sus_bssid,
2867                result_code: fidl_ieee80211::StatusCode::NotInSameBss,
2868                association_id: 0,
2869                association_ies: vec![],
2870            }
2871        );
2872    }
2873
2874    #[fuchsia::test(allow_stalls = false)]
2875    async fn state_transitions_assoced_disassoc_connect_success() {
2876        let mut m = MockObjects::new().await;
2877        let mut ctx = m.make_ctx_with_bss().await;
2878        let mut sta = make_client_station();
2879        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2880        let mut state =
2881            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
2882
2883        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2884        // Disassociation: Associated > Associating
2885        #[rustfmt::skip]
2886        let disassoc = vec![
2887            // Mgmt Header:
2888            0b1010_00_00, 0b00000000, // Frame Control
2889            0, 0, // Duration
2890            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2891            3, 3, 3, 3, 3, 3, // Addr2
2892            6, 6, 6, 6, 6, 6, // Addr3
2893            0x10, 0, // Sequence Control
2894            // Deauth Header:
2895            4, 0, // Reason Code
2896        ];
2897        let rx_info = mock_rx_info(&sta);
2898        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
2899        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
2900        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2901
2902        // Verify a disassoc ind was sent
2903        let disassoc_ind = m
2904            .fake_device_state
2905            .lock()
2906            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
2907            .expect("error reading Disassociate.ind");
2908        assert_eq!(
2909            disassoc_ind,
2910            fidl_mlme::DisassociateIndication {
2911                peer_sta_address: BSSID.to_array(),
2912                reason_code: fidl_ieee80211::ReasonCode::ReasonInactivity,
2913                locally_initiated: false,
2914            }
2915        );
2916
2917        // (sme->mlme) Send a reconnect request
2918        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
2919            peer_sta_address: BSSID.to_array(),
2920        });
2921        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
2922        assert_matches!(state, States::Associating(_), "not in associating state");
2923
2924        // Verify associate request frame was sent
2925        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
2926        assert_eq!(
2927            &m.fake_device_state.lock().wlan_queue[0].0[..22],
2928            &[
2929                // Mgmt header:
2930                0, 0, // FC
2931                0, 0, // Duration
2932                6, 6, 6, 6, 6, 6, // addr1
2933                3, 3, 3, 3, 3, 3, // addr2
2934                6, 6, 6, 6, 6, 6, // addr3
2935            ][..]
2936        );
2937
2938        // Successful: Associating > Associated
2939        #[rustfmt::skip]
2940        let assoc_resp_success = vec![
2941            // Mgmt Header:
2942            0b0001_00_00, 0b00000000, // Frame Control
2943            0, 0, // Duration
2944            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2945            3, 3, 3, 3, 3, 3, // Addr2
2946            6, 6, 6, 6, 6, 6, // Addr3
2947            0x10, 0, // Sequence Control
2948            // Assoc Resp Header:
2949            0, 0, // Capabilities
2950            0, 0, // Status Code
2951            11, 0, // AID
2952            // IEs
2953            // Basic Rates
2954            0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
2955            // HT Capabilities
2956            0x2d, 0x1a, 0xef, 0x09, // HT capabilities info
2957            0x17, // A-MPDU parameters
2958            0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2959            // VHT Capabilities
2960            0xbf, 0x0c, 0x91, 0x59, 0x82, 0x0f, // VHT capabilities info
2961            0xea, 0xff, 0x00, 0x00, 0xea, 0xff, 0x00, 0x00, // VHT supported MCS set
2962        ];
2963        let rx_info = mock_rx_info(&sta);
2964        state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], rx_info, 0.into()).await;
2965        assert_matches!(state, States::Associated(_), "not in associated state");
2966
2967        // Verify a successful connect conf is sent
2968        let connect_conf = m
2969            .fake_device_state
2970            .lock()
2971            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
2972            .expect("error reading Connect.confirm");
2973        assert_eq!(&connect_conf.peer_sta_address, BSSID.as_array());
2974        assert_eq!(connect_conf.result_code, fidl_ieee80211::StatusCode::Success);
2975        assert_eq!(connect_conf.association_id, 11);
2976    }
2977
2978    #[fuchsia::test(allow_stalls = false)]
2979    async fn state_transitions_assoced_disassoc_reconnect_timeout() {
2980        let mut m = MockObjects::new().await;
2981        let mut ctx = m.make_ctx_with_bss().await;
2982        let mut sta = make_client_station();
2983        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
2984        let mut state =
2985            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
2986
2987        assert!(m.fake_device_state.lock().join_bss_request.is_some());
2988        // Disassociation: Associated > Associating
2989        #[rustfmt::skip]
2990        let disassoc = vec![
2991            // Mgmt Header:
2992            0b1010_00_00, 0b00000000, // Frame Control
2993            0, 0, // Duration
2994            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
2995            3, 3, 3, 3, 3, 3, // Addr2
2996            6, 6, 6, 6, 6, 6, // Addr3
2997            0x10, 0, // Sequence Control
2998            // Deauth Header:
2999            4, 0, // Reason Code
3000        ];
3001        let rx_info = mock_rx_info(&sta);
3002        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
3003        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3004        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3005
3006        // Verify a disassoc ind was sent
3007        let _disassoc_ind = m
3008            .fake_device_state
3009            .lock()
3010            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
3011            .expect("error reading Disassociate.ind");
3012
3013        // (sme->mlme) Send a reconnect request
3014        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3015            peer_sta_address: BSSID.to_array(),
3016        });
3017        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3018        assert_matches!(state, States::Associating(_), "not in associating state");
3019
3020        // Verify an event was queued up in the timer.
3021        let (event, _id) = assert_matches!(drain_timeouts(&mut m.time_stream).get(&TimedEventClass::Reassociating), Some(ids) => {
3022            assert_eq!(ids.len(), 1);
3023            ids[0].clone()
3024        });
3025
3026        // Notify reconnecting timeout
3027        let state = state.on_timed_event(&mut sta, event).await;
3028        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3029        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3030
3031        // Verify a connect conf was sent
3032        let connect_conf = m
3033            .fake_device_state
3034            .lock()
3035            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3036            .expect("error reading Connect.confirm");
3037        assert_eq!(
3038            connect_conf,
3039            fidl_mlme::ConnectConfirm {
3040                peer_sta_address: BSSID.to_array(),
3041                result_code: fidl_ieee80211::StatusCode::RejectedSequenceTimeout,
3042                association_id: 0,
3043                association_ies: vec![],
3044            }
3045        );
3046    }
3047
3048    #[fuchsia::test(allow_stalls = false)]
3049    async fn state_transitions_assoced_disassoc_reconnect_denied() {
3050        let mut m = MockObjects::new().await;
3051        let mut ctx = m.make_ctx_with_bss().await;
3052        let mut sta = make_client_station();
3053        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3054        let mut state =
3055            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3056
3057        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3058        // Disassociation: Associated > Associating
3059        #[rustfmt::skip]
3060        let disassoc = vec![
3061            // Mgmt Header:
3062            0b1010_00_00, 0b00000000, // Frame Control
3063            0, 0, // Duration
3064            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3065            3, 3, 3, 3, 3, 3, // Addr2
3066            6, 6, 6, 6, 6, 6, // Addr3
3067            0x10, 0, // Sequence Control
3068            // Deauth Header:
3069            4, 0, // Reason Code
3070        ];
3071        let rx_info = mock_rx_info(&sta);
3072        state = state.on_mac_frame(&mut sta, &disassoc[..], rx_info, 0.into()).await;
3073        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3074        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3075
3076        // Verify a disassoc ind was sent
3077        let _disassoc_ind = m
3078            .fake_device_state
3079            .lock()
3080            .next_mlme_msg::<fidl_mlme::DisassociateIndication>()
3081            .expect("error reading Disassociate.ind");
3082
3083        // (sme->mlme) Send a reconnect request with a different BSSID
3084        let sus_bssid = [b's', b'u', b's', b'r', b'e', b'q'];
3085        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3086            peer_sta_address: sus_bssid,
3087        });
3088        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3089        assert_matches!(state, States::Authenticated(_), "not in auth'd state");
3090
3091        // Verify a connect conf was sent
3092        let connect_conf = m
3093            .fake_device_state
3094            .lock()
3095            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3096            .expect("error reading Connect.confirm");
3097        assert_eq!(
3098            connect_conf,
3099            fidl_mlme::ConnectConfirm {
3100                peer_sta_address: sus_bssid,
3101                result_code: fidl_ieee80211::StatusCode::NotInSameBss,
3102                association_id: 0,
3103                association_ies: vec![],
3104            }
3105        );
3106    }
3107
3108    #[fuchsia::test(allow_stalls = false)]
3109    async fn state_transitions_assoced_reconnect_no_op() {
3110        let mut m = MockObjects::new().await;
3111        let mut ctx = m.make_ctx_with_bss().await;
3112        let mut sta = make_client_station();
3113        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3114        let association = Association {
3115            aid: 42,
3116            assoc_resp_ies: vec![
3117                // Basic Rates
3118                0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24,
3119            ],
3120            ..empty_association(&mut sta)
3121        };
3122        let mut state = States::from(statemachine::testing::new_state(Associated(association)));
3123
3124        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3125
3126        // (sme->mlme) Send a reconnect request
3127        let reconnect_req = wlan_sme::MlmeRequest::Reconnect(fidl_mlme::ReconnectRequest {
3128            peer_sta_address: BSSID.to_array(),
3129        });
3130        state = state.handle_mlme_req(&mut sta, reconnect_req).await;
3131        assert_matches!(state, States::Associated(_), "not in associated state");
3132        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3133
3134        // Verify a successful connect conf is sent
3135        let connect_conf = m
3136            .fake_device_state
3137            .lock()
3138            .next_mlme_msg::<fidl_mlme::ConnectConfirm>()
3139            .expect("error reading Connect.confirm");
3140        assert_eq!(&connect_conf.peer_sta_address, BSSID.as_array());
3141        assert_eq!(connect_conf.result_code, fidl_ieee80211::StatusCode::Success);
3142        assert_eq!(connect_conf.association_id, 42);
3143    }
3144
3145    #[fuchsia::test(allow_stalls = false)]
3146    async fn state_transitions_assoced_deauthing() {
3147        let mut m = MockObjects::new().await;
3148        let mut ctx = m.make_ctx_with_bss().await;
3149        let mut sta = make_client_station();
3150        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3151        let mut state =
3152            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3153
3154        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3155        // Deauthentication: Associated > Joined
3156        #[rustfmt::skip]
3157        let deauth = vec![
3158            // Mgmt Header:
3159            0b1100_00_00, 0b00000000, // Frame Control
3160            0, 0, // Duration
3161            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3162            3, 3, 3, 3, 3, 3, // Addr2
3163            6, 6, 6, 6, 6, 6, // Addr3
3164            0x10, 0, // Sequence Control
3165            // Deauth Header:
3166            4, 0, // Reason Code
3167        ];
3168        let rx_info = mock_rx_info(&sta);
3169        state = state.on_mac_frame(&mut sta, &deauth[..], rx_info, 0.into()).await;
3170        assert_matches!(state, States::Joined(_), "not in joined state");
3171        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3172    }
3173
3174    #[test_case(false, false; "unprotected bss, not scanning")]
3175    #[test_case(true, false; "protected bss, not scanning")]
3176    #[test_case(false, true; "unprotected bss, scanning")]
3177    #[test_case(true, true; "protected bss, scanning")]
3178    #[fuchsia::test(allow_stalls = false)]
3179    async fn assoc_send_eth_frame_becomes_data_frame(protected: bool, scanning: bool) {
3180        let mut m = MockObjects::new().await;
3181        let mut ctx = m.make_ctx().await;
3182        let mut sta =
3183            if protected { make_protected_client_station() } else { make_client_station() };
3184        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3185        let state = States::from(statemachine::testing::new_state(Associated(Association {
3186            controlled_port_open: true,
3187            ..empty_association(&mut sta)
3188        })));
3189
3190        if scanning {
3191            let mut bound_scanner = sta.scanner.bind(sta.ctx);
3192            bound_scanner
3193                .on_sme_scan(fidl_mlme::ScanRequest {
3194                    txn_id: 1337,
3195                    scan_type: fidl_mlme::ScanTypes::Passive,
3196                    channel_list: vec![1],
3197                    ssid_list: vec![],
3198                    probe_delay: 0,
3199                    min_channel_time: 100,
3200                    max_channel_time: 300,
3201                })
3202                .await
3203                .expect("Failed to start scan");
3204            assert!(sta.scanner.is_scanning());
3205        }
3206
3207        let eth_frame = [
3208            1, 2, 3, 4, 5, 6, // dst_addr
3209            3, 3, 3, 3, 3, 3, // src_addr == IFACE_MAC
3210            0x0d, 0x05, // ether_type
3211            21, 22, 23, 24, 25, 26, 27, 28, // payload
3212            29, // more payload
3213        ];
3214
3215        state.on_eth_frame(&mut sta, &eth_frame[..], 0.into()).expect("all good");
3216
3217        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3218        let (data_frame, _tx_flags) = m.fake_device_state.lock().wlan_queue.remove(0);
3219        let mut fc_byte_2 = 0b00000001;
3220        if protected {
3221            fc_byte_2 |= 0b01000000;
3222        }
3223        assert_eq!(
3224            &data_frame[..],
3225            &[
3226                // Data header
3227                0b00001000, fc_byte_2, // Frame Control
3228                0, 0, // Duration
3229                6, 6, 6, 6, 6, 6, // addr1
3230                3, 3, 3, 3, 3, 3, // addr2 (from src_addr above)
3231                1, 2, 3, 4, 5, 6, // addr3 (from dst_addr above)
3232                0x10, 0, // Sequence Control
3233                // LLC header
3234                0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
3235                0, 0, 0, // OUI
3236                0x0d, 0x05, // Protocol ID (from ether_type above)
3237                21, 22, 23, 24, 25, 26, 27, 28, // Payload
3238                29, // More payload
3239            ][..]
3240        )
3241    }
3242
3243    #[fuchsia::test(allow_stalls = false)]
3244    async fn eth_frame_dropped_when_off_channel() {
3245        let mut m = MockObjects::new().await;
3246        let mut ctx = m.make_ctx().await;
3247        let mut sta = make_client_station();
3248        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3249        let state =
3250            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3251
3252        sta.ctx
3253            .device
3254            .set_channel(fidl_ieee80211::WlanChannel {
3255                primary: 42,
3256                cbw: fidl_ieee80211::ChannelBandwidth::Cbw20,
3257                secondary80: 0,
3258            })
3259            .await
3260            .expect("fake device is obedient");
3261        let eth_frame = &[100; 14]; // An ethernet frame must be at least 14 bytes long.
3262
3263        let error = state
3264            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3265            .expect_err("Ethernet frame is dropped when client is off channel");
3266        assert_matches!(error, Error::Status(_str, status) =>
3267            assert_eq!(status, zx::Status::BAD_STATE),
3268            "error should contain a status"
3269        );
3270    }
3271
3272    #[fuchsia::test(allow_stalls = false)]
3273    async fn assoc_eth_frame_too_short_dropped() {
3274        let mut m = MockObjects::new().await;
3275        let mut ctx = m.make_ctx().await;
3276        let mut sta = make_client_station();
3277        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3278        let state =
3279            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3280
3281        let eth_frame = &[100; 13]; // Needs at least 14 bytes for header.
3282
3283        let error = state
3284            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3285            .expect_err("Ethernet frame is too short");
3286        assert_matches!(error, Error::Status(_str, status) =>
3287            assert_eq!(status, zx::Status::IO_DATA_INTEGRITY),
3288            "error should contain a status"
3289        );
3290    }
3291
3292    #[fuchsia::test(allow_stalls = false)]
3293    async fn assoc_controlled_port_closed_eth_frame_dropped() {
3294        let mut m = MockObjects::new().await;
3295        let mut ctx = m.make_ctx().await;
3296        let mut sta = make_client_station();
3297        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3298        let state =
3299            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3300
3301        let eth_frame = &[100; 14]; // long enough for ethernet header.
3302
3303        let error = state
3304            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3305            .expect_err("Ethernet frame canot be sent when controlled port is closed");
3306        assert_matches!(error, Error::Status(_str, status) =>
3307            assert_eq!(status, zx::Status::BAD_STATE),
3308            "Error should contain status"
3309        );
3310    }
3311
3312    #[fuchsia::test(allow_stalls = false)]
3313    async fn not_assoc_eth_frame_dropped() {
3314        let mut m = MockObjects::new().await;
3315        let mut ctx = m.make_ctx().await;
3316        let mut sta = make_client_station();
3317        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3318        let state = States::from(statemachine::testing::new_state(Joined));
3319
3320        let eth_frame = &[100; 14]; // long enough for ethernet header.
3321
3322        let error = state
3323            .on_eth_frame(&mut sta, &eth_frame[..], 0.into())
3324            .expect_err("Ethernet frame cannot be sent in Joined state");
3325        assert_matches!(error, Error::Status(_str, status) =>
3326            assert_eq!(status, zx::Status::BAD_STATE),
3327            "Error should contain status"
3328        );
3329    }
3330
3331    #[fuchsia::test(allow_stalls = false)]
3332    async fn joined_sme_deauth() {
3333        let mut m = MockObjects::new().await;
3334        let mut ctx = m.make_ctx_with_bss().await;
3335        let mut sta = make_client_station();
3336        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3337        let state = States::from(statemachine::testing::new_state(Joined));
3338
3339        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3340        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3341        assert_matches!(state, States::Joined(_), "Joined should stay in Joined");
3342        // No MLME message was sent because MLME already deauthenticated.
3343        m.fake_device_state
3344            .lock()
3345            .next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
3346            .expect_err("should be no outgoing message");
3347        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3348    }
3349
3350    #[fuchsia::test(allow_stalls = false)]
3351    async fn authenticating_sme_deauth() {
3352        let mut m = MockObjects::new().await;
3353        let mut ctx = m.make_ctx_with_bss().await;
3354        let mut sta = make_client_station();
3355        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3356        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3357
3358        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3359        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3360
3361        assert_matches!(state, States::Joined(_), "should transition to Joined");
3362
3363        // No need to notify SME since it already deauthenticated
3364        m.fake_device_state
3365            .lock()
3366            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3367            .expect_err("should not see more MLME messages");
3368        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3369    }
3370
3371    #[fuchsia::test(allow_stalls = false)]
3372    async fn associating_sme_deauth() {
3373        let mut m = MockObjects::new().await;
3374        let mut ctx = m.make_ctx_with_bss().await;
3375        let mut sta = make_client_station();
3376        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3377        let state = States::from(statemachine::testing::new_state(Associating::default()));
3378
3379        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3380        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3381
3382        assert_matches!(state, States::Joined(_), "should transition to Joined");
3383
3384        // No need to notify SME since it already deauthenticated
3385        m.fake_device_state
3386            .lock()
3387            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3388            .expect_err("should not see more MLME messages");
3389        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3390    }
3391
3392    #[fuchsia::test(allow_stalls = false)]
3393    async fn associated_sme_deauth() {
3394        let mut m = MockObjects::new().await;
3395        let mut ctx = m.make_ctx_with_bss().await;
3396        let mut sta = make_client_station();
3397        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3398        let state = States::from(statemachine::testing::new_state(Associated(Association {
3399            controlled_port_open: true,
3400            ..empty_association(&mut sta)
3401        })));
3402
3403        sta.ctx
3404            .device
3405            .notify_association_complete(fake_assoc_cfg())
3406            .await
3407            .expect("valid assoc ctx should not fail");
3408        assert_eq!(1, m.fake_device_state.lock().assocs.len());
3409
3410        assert!(m.fake_device_state.lock().join_bss_request.is_some());
3411        sta.ctx.device.set_ethernet_up().await.expect("should succeed");
3412        assert_eq!(crate::device::LinkStatus::UP, m.fake_device_state.lock().link_status);
3413
3414        let state = state.handle_mlme_req(&mut sta, fake_deauth_req()).await;
3415        assert_matches!(state, States::Joined(_), "should transition to Joined");
3416
3417        // Should accept the deauthentication request and send back confirm.
3418        let deauth_conf = m
3419            .fake_device_state
3420            .lock()
3421            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3422            .expect("should see deauth conf");
3423        assert_eq!(
3424            deauth_conf,
3425            fidl_mlme::DeauthenticateConfirm { peer_sta_address: BSSID.to_array() }
3426        );
3427        m.fake_device_state
3428            .lock()
3429            .next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
3430            .expect_err("should not see more MLME messages");
3431        // Verify association context cleared.
3432        assert_eq!(0, m.fake_device_state.lock().assocs.len());
3433        // Verify ethernet link status is down.
3434        assert_eq!(crate::device::LinkStatus::DOWN, m.fake_device_state.lock().link_status);
3435        assert!(m.fake_device_state.lock().join_bss_request.is_none());
3436    }
3437
3438    fn fake_eapol_req() -> wlan_sme::MlmeRequest {
3439        wlan_sme::MlmeRequest::Eapol(fidl_mlme::EapolRequest {
3440            dst_addr: BSSID.to_array(),
3441            src_addr: IFACE_MAC.to_array(),
3442            data: vec![1, 2, 3, 4],
3443        })
3444    }
3445
3446    #[allow(deprecated)]
3447    #[fuchsia::test(allow_stalls = false)]
3448    async fn mlme_eapol_not_associated() {
3449        let mut m = MockObjects::new().await;
3450        let mut ctx = m.make_ctx().await;
3451        let mut sta = make_protected_client_station();
3452        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3453
3454        let state = States::from(statemachine::testing::new_state(Joined));
3455        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3456        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3457
3458        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3459        m.fake_device_state.lock().wlan_queue.clear();
3460        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3461        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3462
3463        let state = States::from(statemachine::testing::new_state(Associating::default()));
3464        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3465        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3466    }
3467
3468    #[allow(deprecated)]
3469    #[fuchsia::test(allow_stalls = false)]
3470    async fn mlme_eapol_associated_not_protected() {
3471        let mut m = MockObjects::new().await;
3472        let mut ctx = m.make_ctx().await;
3473        let mut sta = make_client_station();
3474        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3475
3476        let state =
3477            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3478        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3479        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3480    }
3481
3482    #[allow(deprecated)]
3483    #[fuchsia::test(allow_stalls = false)]
3484    async fn mlme_eapol_associated() {
3485        let mut m = MockObjects::new().await;
3486        let mut ctx = m.make_ctx().await;
3487        let mut sta = make_protected_client_station();
3488        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3489
3490        let state =
3491            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3492        let _state = state.handle_mlme_req(&mut sta, fake_eapol_req()).await;
3493        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3494        assert_eq!(
3495            &m.fake_device_state.lock().wlan_queue[0].0[..],
3496            &[
3497                // Data header (EAPoL frames are data frames)
3498                0b00001000, 0b00000001, // Frame Control
3499                0, 0, // Duration
3500                6, 6, 6, 6, 6, 6, // addr1 - BSSID
3501                3, 3, 3, 3, 3, 3, // addr2 - IFACE_MAC
3502                6, 6, 6, 6, 6, 6, // addr3 - BSSID
3503                0x10, 0, // Sequence Control
3504                // LLC header
3505                0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
3506                0, 0, 0, // OUI
3507                0x88, 0x8E, // Protocol ID (EAPoL is 0x888E)
3508                1, 2, 3, 4, // Payload
3509            ][..]
3510        );
3511    }
3512
3513    #[allow(deprecated)]
3514    #[fuchsia::test(allow_stalls = false)]
3515    async fn mlme_set_keys_not_associated() {
3516        let mut m = MockObjects::new().await;
3517        let mut ctx = m.make_ctx().await;
3518        let mut sta = make_protected_client_station();
3519        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3520
3521        let state = States::from(statemachine::testing::new_state(Joined));
3522        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3523        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3524
3525        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3526        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3527        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3528
3529        let state = States::from(statemachine::testing::new_state(Associating::default()));
3530        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3531        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3532    }
3533
3534    #[allow(deprecated)]
3535    #[fuchsia::test(allow_stalls = false)]
3536    async fn mlme_set_keys_associated_not_protected() {
3537        let mut m = MockObjects::new().await;
3538        let mut ctx = m.make_ctx().await;
3539        let mut sta = make_client_station();
3540        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3541
3542        let state =
3543            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3544        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3545        assert_eq!(m.fake_device_state.lock().keys.len(), 0);
3546    }
3547
3548    #[allow(deprecated)]
3549    #[fuchsia::test(allow_stalls = false)]
3550    async fn mlme_set_keys_associated() {
3551        let mut m = MockObjects::new().await;
3552        let mut ctx = m.make_ctx().await;
3553        let mut sta = make_protected_client_station();
3554        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3555
3556        let state =
3557            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3558        let _state = state.handle_mlme_req(&mut sta, fake_set_keys_req((*BSSID).into())).await;
3559        assert_eq!(m.fake_device_state.lock().keys.len(), 1);
3560        let conf = assert_matches!(m.fake_device_state.lock().next_mlme_msg::<fidl_mlme::SetKeysConfirm>(), Ok(conf) => conf);
3561        assert_eq!(conf.results.len(), 1);
3562        assert_eq!(
3563            conf.results[0],
3564            fidl_mlme::SetKeyResult { key_id: 6, status: zx::Status::OK.into_raw() }
3565        );
3566
3567        assert_eq!(
3568            m.fake_device_state.lock().keys,
3569            vec![fidl_softmac::WlanKeyConfiguration {
3570                protection: Some(fidl_softmac::WlanProtection::RxTx),
3571                cipher_oui: Some([1, 2, 3]),
3572                cipher_type: Some(4),
3573                key_type: Some(fidl_ieee80211::KeyType::Pairwise),
3574                peer_addr: Some((*BSSID).to_array()),
3575                key_idx: Some(6),
3576                key: Some(vec![1, 2, 3, 4, 5, 6, 7]),
3577                rsc: Some(8),
3578                ..Default::default()
3579            }]
3580        );
3581    }
3582
3583    #[allow(deprecated)]
3584    #[fuchsia::test(allow_stalls = false)]
3585    async fn mlme_set_keys_failure() {
3586        let mut m = MockObjects::new().await;
3587        let mut ctx = m.make_ctx().await;
3588        let mut sta = make_protected_client_station();
3589        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3590
3591        let state =
3592            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3593        m.fake_device_state.lock().install_key_results.push_back(Err(zx::Status::BAD_STATE));
3594        m.fake_device_state.lock().install_key_results.push_back(Ok(()));
3595        // Create a SetKeysReq with one success and one failure.
3596        let mut set_keys_req = fake_set_keys_req((*BSSID).into());
3597        match &mut set_keys_req {
3598            wlan_sme::MlmeRequest::SetKeys(req) => {
3599                req.keylist
3600                    .push(fidl_mlme::SetKeyDescriptor { key_id: 4, ..req.keylist[0].clone() });
3601            }
3602            _ => panic!(),
3603        }
3604        let _state = state.handle_mlme_req(&mut sta, set_keys_req).await;
3605        let conf = assert_matches!(m.fake_device_state.lock().next_mlme_msg::<fidl_mlme::SetKeysConfirm>(), Ok(conf) => conf);
3606        assert_eq!(conf.results.len(), 2);
3607        assert_eq!(
3608            conf.results[0],
3609            fidl_mlme::SetKeyResult { key_id: 6, status: zx::Status::BAD_STATE.into_raw() }
3610        );
3611        assert_eq!(
3612            conf.results[1],
3613            fidl_mlme::SetKeyResult { key_id: 4, status: zx::Status::OK.into_raw() }
3614        );
3615    }
3616
3617    fn fake_set_ctrl_port_open(open: bool) -> wlan_sme::MlmeRequest {
3618        wlan_sme::MlmeRequest::SetCtrlPort(fidl_mlme::SetControlledPortRequest {
3619            peer_sta_address: BSSID.to_array(),
3620            state: match open {
3621                true => fidl_mlme::ControlledPortState::Open,
3622                false => fidl_mlme::ControlledPortState::Closed,
3623            },
3624        })
3625    }
3626
3627    #[fuchsia::test(allow_stalls = false)]
3628    async fn mlme_set_controlled_port_not_associated() {
3629        let mut m = MockObjects::new().await;
3630        let mut ctx = m.make_ctx().await;
3631        let mut sta = make_protected_client_station();
3632        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3633
3634        let state = States::from(statemachine::testing::new_state(Joined));
3635        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3636        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3637
3638        let state = States::from(statemachine::testing::new_state(open_authenticating(&mut sta)));
3639        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3640        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3641
3642        let state = States::from(statemachine::testing::new_state(Associating::default()));
3643        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3644        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3645    }
3646
3647    #[fuchsia::test(allow_stalls = false)]
3648    async fn mlme_set_controlled_port_associated_not_protected() {
3649        let mut m = MockObjects::new().await;
3650        let mut ctx = m.make_ctx().await;
3651        let mut sta = make_client_station();
3652        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3653
3654        let state =
3655            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3656        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3657        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3658    }
3659
3660    #[fuchsia::test(allow_stalls = false)]
3661    async fn mlme_set_controlled_port_associated() {
3662        let mut m = MockObjects::new().await;
3663        let mut ctx = m.make_ctx().await;
3664        let mut sta = make_protected_client_station();
3665        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3666
3667        let state =
3668            States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
3669        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3670        let state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(true)).await;
3671        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::UP);
3672        let _state = state.handle_mlme_req(&mut sta, fake_set_ctrl_port_open(false)).await;
3673        assert_eq!(m.fake_device_state.lock().link_status, crate::device::LinkStatus::DOWN);
3674    }
3675
3676    #[test_case(true; "while scanning")]
3677    #[test_case(false; "while not scanning")]
3678    #[fuchsia::test(allow_stalls = false)]
3679    async fn associated_rx_succeeds(scanning: bool) {
3680        let mut m = MockObjects::new().await;
3681        let mut ctx = m.make_ctx().await;
3682        let mut sta = make_client_station();
3683        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3684        let state = States::from(statemachine::testing::new_state(Associated(Association {
3685            aid: 1,
3686            controlled_port_open: true,
3687            ..empty_association(&mut sta)
3688        })));
3689
3690        if scanning {
3691            let mut bound_scanner = sta.scanner.bind(sta.ctx);
3692            bound_scanner
3693                .on_sme_scan(fidl_mlme::ScanRequest {
3694                    txn_id: 1337,
3695                    scan_type: fidl_mlme::ScanTypes::Passive,
3696                    channel_list: vec![1],
3697                    ssid_list: vec![],
3698                    probe_delay: 0,
3699                    min_channel_time: 100,
3700                    max_channel_time: 300,
3701                })
3702                .await
3703                .expect("Failed to start scan");
3704            assert!(sta.scanner.is_scanning());
3705        }
3706
3707        let fc = mac::FrameControl(0)
3708            .with_frame_type(mac::FrameType::DATA)
3709            .with_data_subtype(mac::DataSubtype(0))
3710            .with_from_ds(true);
3711        let fc = fc.0.to_le_bytes();
3712
3713        let data_frame = vec![
3714            // Data header:
3715            fc[0], fc[1], // FC
3716            0, 0, // Duration
3717            7, 7, 7, 7, 7, 7, // addr1
3718            6, 6, 6, 6, 6, 6, // addr2
3719            42, 42, 42, 42, 42, 42, // addr3
3720            0x10, 0, // Sequence Control
3721            // LLC Header
3722            7, 7, 7, // DSAP, SSAP & control
3723            8, 8, 8, // OUI
3724            9, 10, // eth type
3725            1, 2, 3, 4, 5, 6, 7, 8, // payload
3726        ];
3727
3728        let rx_info = mock_rx_info(&sta);
3729        state.on_mac_frame(&mut sta, &data_frame[..], rx_info, 0.into()).await;
3730        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
3731    }
3732
3733    #[fuchsia::test(allow_stalls = false)]
3734    async fn associated_rx_with_wrong_cbw_succeeds() {
3735        let mut m = MockObjects::new().await;
3736        let mut ctx = m.make_ctx().await;
3737        let mut sta = make_client_station();
3738        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3739        let state = States::from(statemachine::testing::new_state(Associated(Association {
3740            aid: 1,
3741            controlled_port_open: true,
3742            ..empty_association(&mut sta)
3743        })));
3744
3745        let fc = mac::FrameControl(0)
3746            .with_frame_type(mac::FrameType::DATA)
3747            .with_data_subtype(mac::DataSubtype(0))
3748            .with_from_ds(true);
3749        let fc = fc.0.to_le_bytes();
3750
3751        let data_frame = vec![
3752            // Data header:
3753            fc[0], fc[1], // FC
3754            0, 0, // Duration
3755            7, 7, 7, 7, 7, 7, // addr1
3756            6, 6, 6, 6, 6, 6, // addr2
3757            42, 42, 42, 42, 42, 42, // addr3
3758            0x10, 0, // Sequence Control
3759            // LLC Header
3760            7, 7, 7, // DSAP, SSAP & control
3761            8, 8, 8, // OUI
3762            9, 10, // eth type
3763            1, 2, 3, 4, 5, 6, 7, 8, // payload
3764        ];
3765
3766        let mut rx_info = mock_rx_info(&sta);
3767        // We deliberately ignore the cbw, since it isn't important and not all
3768        // drivers report it consistently.
3769        rx_info.channel.cbw = fidl_ieee80211::ChannelBandwidth::Cbw80;
3770        state.on_mac_frame(&mut sta, &data_frame[..], rx_info, 0.into()).await;
3771        assert_eq!(m.fake_device_state.lock().eth_queue.len(), 1);
3772    }
3773
3774    #[fuchsia::test(allow_stalls = false)]
3775    async fn associated_request_bu_if_tim_indicates_buffered_frame() {
3776        let mut m = MockObjects::new().await;
3777        let mut ctx = m.make_ctx().await;
3778        let mut sta = make_client_station();
3779        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3780        let state = States::from(statemachine::testing::new_state(Associated(Association {
3781            aid: 1,
3782            ..empty_association(&mut sta)
3783        })));
3784
3785        let beacon = [
3786            // Mgmt header
3787            0b10000000, 0, // Frame Control
3788            0, 0, // Duration
3789            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3790            7, 7, 7, 7, 7, 7, // Addr2
3791            6, 6, 6, 6, 6, 6, // Addr3
3792            0, 0, // Sequence Control
3793            // Beacon header:
3794            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3795            10, 0, // Beacon interval
3796            33, 0, // Capabilities
3797            5, 4, 0, 0, 0, 0b00000010, // Tim IE: bit 1 in the last octet indicates AID 1
3798        ];
3799
3800        let rx_info = mock_rx_info(&sta);
3801        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3802
3803        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 1);
3804        assert_eq!(
3805            &m.fake_device_state.lock().wlan_queue[0].0[..],
3806            &[
3807                0b10100100, 0, // Frame control
3808                1, 0b11000000, // ID (2 MSBs are set to 1 from the AID)
3809                6, 6, 6, 6, 6, 6, // BSSID
3810                3, 3, 3, 3, 3, 3, // TA
3811            ][..]
3812        );
3813    }
3814
3815    #[fuchsia::test(allow_stalls = false)]
3816    async fn associated_does_not_request_bu_if_tim_indicates_no_buffered_frame() {
3817        let mut m = MockObjects::new().await;
3818        let mut ctx = m.make_ctx().await;
3819        let mut sta = make_client_station();
3820        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3821        let state = States::from(statemachine::testing::new_state(Associated(Association {
3822            aid: 1,
3823            ..empty_association(&mut sta)
3824        })));
3825
3826        let beacon = [
3827            // Mgmt header
3828            0b10000000, 0, // Frame Control
3829            0, 0, // Duration
3830            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3831            7, 7, 7, 7, 7, 7, // Addr2
3832            6, 6, 6, 6, 6, 6, // Addr3
3833            0, 0, // Sequence Control
3834            // Beacon header:
3835            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3836            10, 0, // Beacon interval
3837            33, 0, // Capabilities
3838            5, 4, 0, 0, 0, 0, // Tim IE: No buffered frame for any client.
3839        ];
3840        let rx_info = mock_rx_info(&sta);
3841        state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3842
3843        assert_eq!(m.fake_device_state.lock().wlan_queue.len(), 0);
3844    }
3845
3846    fn rx_info_with_dbm<'a>(
3847        client: &BoundClient<'a, FakeDevice>,
3848        rssi_dbm: i8,
3849    ) -> fidl_softmac::WlanRxInfo {
3850        let mut rx_info = fidl_softmac::WlanRxInfo { rssi_dbm, ..mock_rx_info(client) };
3851        rx_info.valid_fields |= fidl_softmac::WlanRxInfoValid::RSSI;
3852        rx_info
3853    }
3854
3855    #[fuchsia::test(allow_stalls = false)]
3856    async fn signal_report() {
3857        let mut m = MockObjects::new().await;
3858        let mut ctx = m.make_ctx().await;
3859        let mut sta = make_protected_client_station();
3860        let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.channel_state);
3861
3862        let state = States::from(State::from(statemachine::testing::new_state(Associated(
3863            empty_association(&mut sta),
3864        ))));
3865
3866        let (_, timed_event, _) =
3867            m.time_stream.try_next().unwrap().expect("Should have scheduled signal report timeout");
3868        let state = state.on_timed_event(&mut sta, timed_event.event).await;
3869
3870        let signal_ind = m
3871            .fake_device_state
3872            .lock()
3873            .next_mlme_msg::<fidl_internal::SignalReportIndication>()
3874            .expect("should see a signal report");
3875
3876        // -128 is the default value, equivalent to 0 watt.
3877        assert_eq!(signal_ind.rssi_dbm, -128);
3878
3879        let beacon = [
3880            // Mgmt header
3881            0b10000000, 0, // Frame Control
3882            0, 0, // Duration
3883            3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3884            7, 7, 7, 7, 7, 7, // Addr2
3885            6, 6, 6, 6, 6, 6, // Addr3
3886            0, 0, // Sequence Control
3887            // Beacon header:
3888            0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
3889            10, 0, // Beacon interval
3890            33, 0, // Capabilities
3891        ];
3892
3893        const EXPECTED_DBM: i8 = -32;
3894        let rx_info = rx_info_with_dbm(&sta, EXPECTED_DBM);
3895        let state = state.on_mac_frame(&mut sta, &beacon[..], rx_info, 0.into()).await;
3896
3897        let (_, timed_event, _) =
3898            m.time_stream.try_next().unwrap().expect("Should have scheduled signal report timeout");
3899        let _state = state.on_timed_event(&mut sta, timed_event.event).await;
3900
3901        let signal_ind = m
3902            .fake_device_state
3903            .lock()
3904            .next_mlme_msg::<fidl_internal::SignalReportIndication>()
3905            .expect("should see a signal report");
3906
3907        assert_eq!(signal_ind.rssi_dbm, EXPECTED_DBM);
3908    }
3909}