wlan_sme/client/state/
mod.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5mod link_state;
6
7use crate::client::event::{self, Event};
8use crate::client::internal::Context;
9use crate::client::protection::{Protection, ProtectionIe, SecurityContext, build_protection_ie};
10use crate::client::{
11    AssociationFailure, ClientConfig, ClientSmeStatus, ConnectResult, ConnectTransactionEvent,
12    ConnectTransactionSink, EstablishRsnaFailure, EstablishRsnaFailureReason, RoamFailure,
13    RoamFailureType, RoamResult, ServingApInfo, report_connect_finished, report_roam_finished,
14};
15use crate::{MlmeRequest, MlmeSink, mlme_event_name};
16use anyhow::bail;
17use fidl_fuchsia_wlan_common_security::Authentication;
18use fidl_fuchsia_wlan_mlme::{self as fidl_mlme, MlmeEvent};
19use fuchsia_inspect_contrib::inspect_log;
20use fuchsia_inspect_contrib::log::InspectBytes;
21use ieee80211::{Bssid, MacAddr, MacAddrBytes, Ssid};
22use link_state::LinkState;
23use log::{error, info, warn};
24use wlan_common::bss::BssDescription;
25use wlan_common::ie::rsn::cipher;
26use wlan_common::ie::rsn::suite_selector::OUI;
27use wlan_common::ie::{self};
28use wlan_common::security::SecurityAuthenticator;
29use wlan_common::security::wep::WepKey;
30use wlan_common::timer::EventHandle;
31use wlan_rsn::auth;
32use wlan_rsn::rsna::{AuthRejectedReason, AuthStatus, SecAssocUpdate, UpdateSink};
33use wlan_statemachine::*;
34use {
35    fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211,
36    fidl_fuchsia_wlan_internal as fidl_internal, fidl_fuchsia_wlan_sme as fidl_sme,
37};
38
39/// Timeout for the MLME connect op, which consists of Join, Auth, and Assoc steps.
40/// TODO(https://fxbug.dev/42182084): Consider having a single overall connect timeout that is
41///                        managed by SME and also includes the EstablishRsna step.
42const DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT: u32 = 60; // beacon intervals
43/// Maximum number of association attempts we will make without achieving a
44/// link up state. We abort and notify of a connection result if we exceed this
45/// count.
46const MAX_REASSOCIATIONS_WITHOUT_LINK_UP: u32 = 5;
47
48const IDLE_STATE: &str = "IdleState";
49const DISCONNECTING_STATE: &str = "DisconnectingState";
50const CONNECTING_STATE: &str = "ConnectingState";
51const ROAMING_STATE: &str = "RoamingState";
52const RSNA_STATE: &str = "EstablishingRsnaState";
53const LINK_UP_STATE: &str = "LinkUpState";
54
55#[derive(Debug)]
56pub struct ConnectCommand {
57    pub bss: Box<BssDescription>,
58    pub connect_txn_sink: ConnectTransactionSink,
59    pub protection: Protection,
60    pub authentication: Authentication,
61}
62
63#[derive(Debug)]
64pub struct Idle {
65    cfg: ClientConfig,
66}
67
68#[derive(Debug)]
69pub struct Connecting {
70    cfg: ClientConfig,
71    cmd: ConnectCommand,
72    protection_ie: Option<ProtectionIe>,
73    reassociation_loop_count: u32,
74}
75
76// When a roam attempt starts, SME needs to keep track of where the attempt initiated, in order to
77// handle a failure.
78#[derive(Debug)]
79enum RoamInitiator {
80    RoamStartInd,
81    RoamRequest,
82}
83
84impl From<RoamInitiator> for fidl_sme::DisconnectMlmeEventName {
85    fn from(roam_initiator: RoamInitiator) -> fidl_sme::DisconnectMlmeEventName {
86        match roam_initiator {
87            RoamInitiator::RoamStartInd => fidl_sme::DisconnectMlmeEventName::RoamStartIndication,
88            RoamInitiator::RoamRequest => fidl_sme::DisconnectMlmeEventName::RoamRequest,
89        }
90    }
91}
92
93#[derive(Debug)]
94pub struct Associated {
95    cfg: ClientConfig,
96    connect_txn_sink: ConnectTransactionSink,
97    latest_ap_state: Box<BssDescription>,
98    auth_method: Option<auth::MethodName>,
99    last_signal_report_time: zx::MonotonicInstant,
100    link_state: LinkState,
101    protection_ie: Option<ProtectionIe>,
102    // TODO(https://fxbug.dev/42163244): Remove `wmm_param` field when wlanstack telemetry is deprecated.
103    wmm_param: Option<ie::WmmParam>,
104    last_channel_switch_time: Option<zx::MonotonicInstant>,
105    reassociation_loop_count: u32,
106    authentication: Authentication,
107    // If a roam is in progress, track the type of roam here.
108    roam_in_progress: Option<RoamInitiator>,
109}
110
111#[derive(Debug)]
112pub struct Roaming {
113    cfg: ClientConfig,
114    cmd: ConnectCommand,
115    auth_method: Option<auth::MethodName>,
116    protection_ie: Option<ProtectionIe>,
117}
118
119#[derive(Debug)]
120pub struct Disconnecting {
121    cfg: ClientConfig,
122    action: PostDisconnectAction,
123    _timeout: Option<EventHandle>,
124}
125
126statemachine!(
127    #[derive(Debug)]
128    pub enum ClientState,
129    () => Idle,
130    Idle => Connecting,
131    Connecting => [Associated, Disconnecting, Idle],
132    // Receiving a disassociation ind while Associated causes a transition back to Connecting.
133    Associated => [Connecting, Roaming, Disconnecting, Idle],
134    Roaming => [Associated, Disconnecting, Idle],
135    // We transition directly to Connecting if the disconnect was due to a
136    // pending connect.
137    Disconnecting => [Connecting, Idle],
138);
139
140#[allow(clippy::large_enum_variant)] // TODO(https://fxbug.dev/401087337)
141/// Only one PostDisconnectAction may be selected at a time. This means that
142/// in some cases a connect or disconnect might be reported as finished when
143/// we're actually still in a disconnecting state, since a different trigger
144/// for the disconnect might arrive later and abort the earlier one (e.g. two
145/// subsequent disconnect requests). We still process all PostDisconnectAction
146/// events, and generally expect these events to be initiated one at a time by
147/// wlancfg, so this should not be an issue.
148enum PostDisconnectAction {
149    ReportConnectFinished { sink: ConnectTransactionSink, result: ConnectResult },
150    RespondDisconnect { responder: fidl_sme::ClientSmeDisconnectResponder },
151    BeginConnect { cmd: ConnectCommand },
152    ReportRoamFinished { sink: ConnectTransactionSink, result: RoamResult },
153    None,
154}
155
156enum AfterDisconnectState {
157    Idle(Idle),
158    Connecting(Connecting),
159}
160
161impl std::fmt::Debug for PostDisconnectAction {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
163        f.write_str("PostDisconnectAction::")?;
164        match self {
165            PostDisconnectAction::RespondDisconnect { .. } => f.write_str("RespondDisconnect"),
166            PostDisconnectAction::BeginConnect { .. } => f.write_str("BeginConnect"),
167            PostDisconnectAction::ReportConnectFinished { .. } => {
168                f.write_str("ReportConnectFinished")
169            }
170            PostDisconnectAction::ReportRoamFinished { .. } => f.write_str("ReportRoamFinished"),
171            PostDisconnectAction::None => f.write_str("None"),
172        }?;
173        Ok(())
174    }
175}
176
177/// Context surrounding the state change, for Inspect logging
178pub enum StateChangeContext {
179    Disconnect { msg: String, disconnect_source: fidl_sme::DisconnectSource },
180    Connect { msg: String, bssid: Bssid, ssid: Ssid },
181    Roam { msg: String, bssid: Bssid },
182    Msg(String),
183}
184
185trait StateChangeContextExt {
186    fn set_msg(&mut self, msg: String);
187}
188
189impl StateChangeContextExt for Option<StateChangeContext> {
190    fn set_msg(&mut self, msg: String) {
191        match self {
192            Some(ctx) => match ctx {
193                StateChangeContext::Disconnect { msg: inner, .. } => *inner = msg,
194                StateChangeContext::Connect { msg: inner, .. } => *inner = msg,
195                StateChangeContext::Roam { msg: inner, .. } => *inner = msg,
196                StateChangeContext::Msg(inner) => *inner = msg,
197            },
198            None => {
199                *self = Some(StateChangeContext::Msg(msg));
200            }
201        }
202    }
203}
204
205impl Idle {
206    fn on_connect(
207        self,
208        cmd: ConnectCommand,
209        state_change_ctx: &mut Option<StateChangeContext>,
210        context: &mut Context,
211    ) -> Result<Connecting, Idle> {
212        // Derive RSN (for WPA2) or Vendor IEs (for WPA1) or neither(WEP/non-protected).
213        let protection_ie = match build_protection_ie(&cmd.protection) {
214            Ok(ie) => ie,
215            Err(e) => {
216                let msg = format!("Failed to build protection IEs: {e}");
217                error!("{}", msg);
218                let _ = state_change_ctx.replace(StateChangeContext::Connect {
219                    msg,
220                    bssid: cmd.bss.bssid,
221                    ssid: cmd.bss.ssid,
222                });
223                return Err(self);
224            }
225        };
226        let (auth_type, sae_password, wep_key) = match &cmd.protection {
227            Protection::Rsna(rsna) => match rsna.supplicant.get_auth_cfg() {
228                auth::Config::Sae { .. } => (fidl_mlme::AuthenticationTypes::Sae, vec![], None),
229                auth::Config::DriverSae { password } => {
230                    (fidl_mlme::AuthenticationTypes::Sae, password.clone(), None)
231                }
232                auth::Config::ComputedPsk(_) => {
233                    (fidl_mlme::AuthenticationTypes::OpenSystem, vec![], None)
234                }
235            },
236            Protection::Wep(key) => {
237                let wep_key = build_wep_set_key_descriptor(cmd.bss.bssid, key);
238                inspect_log!(context.inspect.rsn_events.lock(), {
239                    derived_key: "WEP",
240                    cipher: format!("{:?}", cipher::Cipher::new_dot11(wep_key.cipher_suite_type.into_primitive() as u8)),
241                    key_index: wep_key.key_id,
242                });
243                (fidl_mlme::AuthenticationTypes::SharedKey, vec![], Some(wep_key))
244            }
245            _ => (fidl_mlme::AuthenticationTypes::OpenSystem, vec![], None),
246        };
247        let security_ie = match protection_ie.as_ref() {
248            Some(ProtectionIe::Rsne(v)) => v.to_vec(),
249            Some(ProtectionIe::VendorIes(v)) => v.to_vec(),
250            None => vec![],
251        };
252        context.mlme_sink.send(MlmeRequest::Connect(fidl_mlme::ConnectRequest {
253            selected_bss: (*cmd.bss).clone().into(),
254            connect_failure_timeout: DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT,
255            auth_type,
256            sae_password,
257            wep_key: wep_key.map(Box::new),
258            security_ie,
259        }));
260        context.att_id += 1;
261
262        let msg = connect_cmd_inspect_summary(&cmd);
263        let _ = state_change_ctx.replace(StateChangeContext::Connect {
264            msg,
265            bssid: cmd.bss.bssid,
266            ssid: cmd.bss.ssid.clone(),
267        });
268
269        Ok(Connecting { cfg: self.cfg, cmd, protection_ie, reassociation_loop_count: 0 })
270    }
271
272    fn on_disconnect_complete(
273        self,
274        context: &mut Context,
275        action: PostDisconnectAction,
276        state_change_ctx: &mut Option<StateChangeContext>,
277    ) -> AfterDisconnectState {
278        match action {
279            PostDisconnectAction::RespondDisconnect { responder } => {
280                if let Err(e) = responder.send() {
281                    error!("Failed to send disconnect response: {}", e);
282                }
283                AfterDisconnectState::Idle(self)
284            }
285            PostDisconnectAction::BeginConnect { cmd } => {
286                match self.on_connect(cmd, state_change_ctx, context) {
287                    Ok(connecting) => AfterDisconnectState::Connecting(connecting),
288                    Err(idle) => AfterDisconnectState::Idle(idle),
289                }
290            }
291            PostDisconnectAction::ReportConnectFinished { mut sink, result } => {
292                sink.send_connect_result(result);
293                AfterDisconnectState::Idle(self)
294            }
295            PostDisconnectAction::ReportRoamFinished { mut sink, result } => {
296                sink.send_roam_result(result);
297                AfterDisconnectState::Idle(self)
298            }
299            PostDisconnectAction::None => AfterDisconnectState::Idle(self),
300        }
301    }
302}
303
304fn parse_wmm_from_ies(ies: &[u8]) -> Option<ie::WmmParam> {
305    let mut wmm_param = None;
306    for (id, body) in ie::Reader::new(ies) {
307        if id == ie::Id::VENDOR_SPECIFIC
308            && let Ok(ie::VendorIe::WmmParam(wmm_param_body)) = ie::parse_vendor_ie(body)
309        {
310            match ie::parse_wmm_param(wmm_param_body) {
311                Ok(param) => wmm_param = Some(*param),
312                Err(e) => {
313                    warn!(
314                        "Fail parsing IEs for WMM param. Bytes: {:?}. Error: {}",
315                        wmm_param_body, e
316                    );
317                }
318            }
319        }
320    }
321    wmm_param
322}
323
324impl Connecting {
325    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
326    fn on_connect_conf(
327        mut self,
328        conf: fidl_mlme::ConnectConfirm,
329        state_change_ctx: &mut Option<StateChangeContext>,
330        context: &mut Context,
331    ) -> Result<Associated, Disconnecting> {
332        let auth_method = self.cmd.protection.rsn_auth_method();
333        let wmm_param = parse_wmm_from_ies(&conf.association_ies);
334
335        let link_state = match conf.result_code {
336            fidl_ieee80211::StatusCode::Success => {
337                // TODO(https://fxbug.dev/359842400) Check that peer_sta_address matches request.
338                match LinkState::new(self.cmd.protection, context) {
339                    Ok(link_state) => link_state,
340                    Err(failure_reason) => {
341                        let msg = "Connect terminated; failed to initialize LinkState".to_string();
342                        error!("{}", msg);
343                        state_change_ctx.set_msg(msg);
344                        send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
345                        let timeout = context.timer.schedule(event::DeauthenticateTimeout);
346                        return Err(Disconnecting {
347                            cfg: self.cfg,
348                            action: PostDisconnectAction::ReportConnectFinished {
349                                sink: self.cmd.connect_txn_sink,
350                                result: EstablishRsnaFailure {
351                                    auth_method,
352                                    reason: failure_reason,
353                                }
354                                .into(),
355                            },
356                            _timeout: Some(timeout),
357                        });
358                    }
359                }
360            }
361            other => {
362                let msg = format!("Connect request failed: {other:?}");
363                warn!("{}", msg);
364                state_change_ctx.set_msg(msg);
365                send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
366                let timeout = context.timer.schedule(event::DeauthenticateTimeout);
367                return Err(Disconnecting {
368                    cfg: self.cfg,
369                    action: PostDisconnectAction::ReportConnectFinished {
370                        sink: self.cmd.connect_txn_sink,
371                        result: ConnectResult::Failed(
372                            AssociationFailure {
373                                bss_protection: self.cmd.bss.protection(),
374                                code: other,
375                            }
376                            .into(),
377                        ),
378                    },
379                    _timeout: Some(timeout),
380                });
381            }
382        };
383        state_change_ctx.set_msg("Connect succeeded".to_string());
384
385        if let LinkState::LinkUp(_) = link_state {
386            report_connect_finished(&mut self.cmd.connect_txn_sink, ConnectResult::Success);
387            self.reassociation_loop_count = 0;
388        }
389
390        Ok(Associated {
391            cfg: self.cfg,
392            connect_txn_sink: self.cmd.connect_txn_sink,
393            auth_method,
394            last_signal_report_time: now(),
395            latest_ap_state: self.cmd.bss,
396            link_state,
397            protection_ie: self.protection_ie,
398            wmm_param,
399            last_channel_switch_time: None,
400            reassociation_loop_count: self.reassociation_loop_count,
401            authentication: self.cmd.authentication,
402            roam_in_progress: None,
403        })
404    }
405
406    fn on_deauthenticate_ind(
407        mut self,
408        ind: fidl_mlme::DeauthenticateIndication,
409        state_change_ctx: &mut Option<StateChangeContext>,
410    ) -> Idle {
411        let msg = format!(
412            "Association request failed due to spurious deauthentication; reason code: {:?}, locally_initiated: {:?}",
413            ind.reason_code, ind.locally_initiated
414        );
415        warn!("{}", msg);
416        // A deauthenticate_ind means that MLME has already cleared the association,
417        // so we go directly to Idle instead of through Disconnecting.
418        report_connect_finished(
419            &mut self.cmd.connect_txn_sink,
420            ConnectResult::Failed(
421                AssociationFailure {
422                    bss_protection: self.cmd.bss.protection(),
423                    code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
424                }
425                .into(),
426            ),
427        );
428        state_change_ctx.set_msg(msg);
429        Idle { cfg: self.cfg }
430    }
431
432    fn on_disassociate_ind(
433        self,
434        ind: fidl_mlme::DisassociateIndication,
435        state_change_ctx: &mut Option<StateChangeContext>,
436        context: &mut Context,
437    ) -> Disconnecting {
438        let msg = format!(
439            "Association request failed due to spurious disassociation; reason code: {:?}, locally_initiated: {:?}",
440            ind.reason_code, ind.locally_initiated
441        );
442        warn!("{}", msg);
443        send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
444        let timeout = context.timer.schedule(event::DeauthenticateTimeout);
445        state_change_ctx.set_msg(msg);
446        Disconnecting {
447            cfg: self.cfg,
448            action: PostDisconnectAction::ReportConnectFinished {
449                sink: self.cmd.connect_txn_sink,
450                result: ConnectResult::Failed(
451                    AssociationFailure {
452                        bss_protection: self.cmd.bss.protection(),
453                        code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
454                    }
455                    .into(),
456                ),
457            },
458            _timeout: Some(timeout),
459        }
460    }
461
462    // Sae management functions
463
464    fn on_sae_handshake_ind(
465        &mut self,
466        ind: fidl_mlme::SaeHandshakeIndication,
467        context: &mut Context,
468    ) -> Result<(), anyhow::Error> {
469        process_sae_handshake_ind(&mut self.cmd.protection, ind, context)
470    }
471
472    fn on_sae_frame_rx(
473        &mut self,
474        frame: fidl_mlme::SaeFrame,
475        context: &mut Context,
476    ) -> Result<(), anyhow::Error> {
477        process_sae_frame_rx(&mut self.cmd.protection, frame, context)
478    }
479
480    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
481    fn handle_timeout(
482        mut self,
483        event: Event,
484        state_change_ctx: &mut Option<StateChangeContext>,
485        context: &mut Context,
486    ) -> Result<Self, Disconnecting> {
487        match process_sae_timeout(&mut self.cmd.protection, self.cmd.bss.bssid, event, context) {
488            Ok(()) => Ok(self),
489            Err(e) => {
490                // An error in handling a timeout means that we may have no way to abort a
491                // failed handshake. Drop to idle.
492                let msg = format!("failed to handle SAE timeout: {e:?}");
493                error!("{}", msg);
494                send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
495                let timeout = context.timer.schedule(event::DeauthenticateTimeout);
496                state_change_ctx.set_msg(msg);
497                Err(Disconnecting {
498                    cfg: self.cfg,
499                    action: PostDisconnectAction::ReportConnectFinished {
500                        sink: self.cmd.connect_txn_sink,
501                        result: ConnectResult::Failed(
502                            AssociationFailure {
503                                bss_protection: self.cmd.bss.protection(),
504                                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
505                            }
506                            .into(),
507                        ),
508                    },
509                    _timeout: Some(timeout),
510                })
511            }
512        }
513    }
514
515    fn disconnect(mut self, context: &mut Context, action: PostDisconnectAction) -> Disconnecting {
516        report_connect_finished(&mut self.cmd.connect_txn_sink, ConnectResult::Canceled);
517        send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
518        let timeout = context.timer.schedule(event::DeauthenticateTimeout);
519        Disconnecting { cfg: self.cfg, action, _timeout: Some(timeout) }
520    }
521}
522
523impl Associated {
524    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
525    fn on_disassociate_ind(
526        mut self,
527        ind: fidl_mlme::DisassociateIndication,
528        state_change_ctx: &mut Option<StateChangeContext>,
529        context: &mut Context,
530    ) -> Result<Connecting, Disconnecting> {
531        let (mut protection, connected_duration) = self.link_state.disconnect();
532
533        let disconnect_reason = fidl_sme::DisconnectCause {
534            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
535            reason_code: ind.reason_code,
536        };
537        let disconnect_source = if ind.locally_initiated {
538            fidl_sme::DisconnectSource::Mlme(disconnect_reason)
539        } else {
540            fidl_sme::DisconnectSource::Ap(disconnect_reason)
541        };
542
543        if self.reassociation_loop_count >= MAX_REASSOCIATIONS_WITHOUT_LINK_UP {
544            // We have exceeded our retry count. Disconnect.
545            let fidl_disconnect_info =
546                fidl_sme::DisconnectInfo { is_sme_reconnecting: false, disconnect_source };
547            self.connect_txn_sink
548                .send(ConnectTransactionEvent::OnDisconnect { info: fidl_disconnect_info });
549            let msg = format!(
550                "received DisassociateInd msg; reason code {:?}. Too many retries, disconnecting.",
551                ind.reason_code
552            );
553            let _ =
554                state_change_ctx.replace(StateChangeContext::Disconnect { msg, disconnect_source });
555            send_deauthenticate_request(&self.latest_ap_state.bssid, &context.mlme_sink);
556            let timeout = context.timer.schedule(event::DeauthenticateTimeout);
557            Err(Disconnecting {
558                cfg: self.cfg,
559                action: PostDisconnectAction::None,
560                _timeout: Some(timeout),
561            })
562        } else {
563            if connected_duration.is_some() {
564                // Only notify clients of SME if the link was fully established.
565                let fidl_disconnect_info =
566                    fidl_sme::DisconnectInfo { is_sme_reconnecting: true, disconnect_source };
567                self.connect_txn_sink
568                    .send(ConnectTransactionEvent::OnDisconnect { info: fidl_disconnect_info });
569            }
570
571            let msg = format!("received DisassociateInd msg; reason code {:?}", ind.reason_code);
572            let _ = state_change_ctx.replace(match connected_duration {
573                Some(_) => StateChangeContext::Disconnect { msg, disconnect_source },
574                None => StateChangeContext::Msg(msg),
575            });
576
577            // Client is disassociating. The ESS-SA must be kept alive but reset.
578            if let Protection::Rsna(rsna) = &mut protection {
579                // Reset the state of the ESS-SA and its replay counter to zero per IEEE 802.11-2016 12.7.2.
580                rsna.supplicant.reset();
581            }
582
583            context.att_id += 1;
584            let cmd = ConnectCommand {
585                bss: self.latest_ap_state,
586                connect_txn_sink: self.connect_txn_sink,
587                protection,
588                authentication: self.authentication,
589            };
590            let req = fidl_mlme::ReconnectRequest { peer_sta_address: cmd.bss.bssid.to_array() };
591            context.mlme_sink.send(MlmeRequest::Reconnect(req));
592            Ok(Connecting {
593                cfg: self.cfg,
594                cmd,
595                protection_ie: self.protection_ie,
596                reassociation_loop_count: self.reassociation_loop_count + 1,
597            })
598        }
599    }
600
601    fn on_deauthenticate_ind(
602        mut self,
603        ind: fidl_mlme::DeauthenticateIndication,
604        state_change_ctx: &mut Option<StateChangeContext>,
605    ) -> Idle {
606        let (_, connected_duration) = self.link_state.disconnect();
607
608        let disconnect_reason = fidl_sme::DisconnectCause {
609            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
610            reason_code: ind.reason_code,
611        };
612        let disconnect_source = if ind.locally_initiated {
613            fidl_sme::DisconnectSource::Mlme(disconnect_reason)
614        } else {
615            fidl_sme::DisconnectSource::Ap(disconnect_reason)
616        };
617        let disconnect_info =
618            fidl_sme::DisconnectInfo { is_sme_reconnecting: false, disconnect_source };
619
620        match connected_duration {
621            Some(_duration) => {
622                self.connect_txn_sink
623                    .send(ConnectTransactionEvent::OnDisconnect { info: disconnect_info });
624            }
625            None => match self.roam_in_progress {
626                Some(_) => {
627                    report_roam_finished(
628                        &mut self.connect_txn_sink,
629                        RoamFailure {
630                            failure_type: RoamFailureType::EstablishRsnaFailure,
631                            selected_bss: Some(*self.latest_ap_state.clone()),
632                            disconnect_info,
633                            selected_bssid: self.latest_ap_state.bssid,
634                            auth_method: self.auth_method,
635                            status_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
636                            establish_rsna_failure_reason: Some(
637                                EstablishRsnaFailureReason::InternalError,
638                            ),
639                        }
640                        .into(),
641                    );
642                }
643                _ => {
644                    report_connect_finished(
645                        &mut self.connect_txn_sink,
646                        EstablishRsnaFailure {
647                            auth_method: self.auth_method,
648                            reason: EstablishRsnaFailureReason::InternalError,
649                        }
650                        .into(),
651                    );
652                }
653            },
654        }
655
656        let _ = state_change_ctx.replace(StateChangeContext::Disconnect {
657            msg: format!("received DeauthenticateInd msg; reason code {:?}", ind.reason_code),
658            disconnect_source,
659        });
660        Idle { cfg: self.cfg }
661    }
662
663    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
664    fn process_link_state_update<U, H>(
665        mut self,
666        update: U,
667        update_handler: H,
668        context: &mut Context,
669        state_change_ctx: &mut Option<StateChangeContext>,
670    ) -> Result<Self, Disconnecting>
671    where
672        H: Fn(
673            LinkState,
674            U,
675            &BssDescription,
676            &mut Option<StateChangeContext>,
677            &mut Context,
678        ) -> Result<LinkState, EstablishRsnaFailureReason>,
679    {
680        let was_establishing_rsna = match &self.link_state {
681            LinkState::EstablishingRsna(_) => true,
682            LinkState::Init(_) | LinkState::LinkUp(_) => false,
683        };
684
685        let link_state = match update_handler(
686            self.link_state,
687            update,
688            &self.latest_ap_state,
689            state_change_ctx,
690            context,
691        ) {
692            Ok(link_state) => link_state,
693            Err(failure_reason) => {
694                send_deauthenticate_request(&self.latest_ap_state.bssid, &context.mlme_sink);
695
696                let timeout = context.timer.schedule(event::DeauthenticateTimeout);
697                match self.roam_in_progress {
698                    Some(roam_initiator) => {
699                        report_roam_finished(
700                            &mut self.connect_txn_sink,
701                            RoamFailure {
702                                failure_type: RoamFailureType::EstablishRsnaFailure,
703                                selected_bssid: self.latest_ap_state.bssid,
704                                status_code: fidl_ieee80211::StatusCode::EstablishRsnaFailure,
705                                disconnect_info: make_roam_disconnect_info(
706                                    roam_initiator.into(),
707                                    None,
708                                ),
709                                auth_method: self.auth_method,
710                                selected_bss: Some(*self.latest_ap_state),
711                                establish_rsna_failure_reason: Some(failure_reason),
712                            }
713                            .into(),
714                        );
715                    }
716                    _ => {
717                        report_connect_finished(
718                            &mut self.connect_txn_sink,
719                            EstablishRsnaFailure {
720                                auth_method: self.auth_method,
721                                reason: failure_reason,
722                            }
723                            .into(),
724                        );
725                    }
726                }
727                return Err(Disconnecting {
728                    cfg: self.cfg,
729                    action: PostDisconnectAction::None,
730                    _timeout: Some(timeout),
731                });
732            }
733        };
734
735        if let LinkState::LinkUp(_) = link_state
736            && was_establishing_rsna
737        {
738            match self.roam_in_progress {
739                Some(_roam_initiator) => {
740                    report_roam_finished(
741                        &mut self.connect_txn_sink,
742                        RoamResult::Success(Box::new(*self.latest_ap_state.clone())),
743                    );
744                    self.roam_in_progress = None;
745                }
746                _ => {
747                    report_connect_finished(&mut self.connect_txn_sink, ConnectResult::Success);
748                }
749            }
750            self.reassociation_loop_count = 0;
751        }
752
753        Ok(Self { link_state, ..self })
754    }
755
756    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
757    fn on_eapol_ind(
758        self,
759        ind: fidl_mlme::EapolIndication,
760        state_change_ctx: &mut Option<StateChangeContext>,
761        context: &mut Context,
762    ) -> Result<Self, Disconnecting> {
763        // Ignore unexpected EAPoL frames.
764        if !self.latest_ap_state.needs_eapol_exchange() {
765            return Ok(self);
766        }
767
768        // Reject EAPoL frames from other BSS.
769        if &ind.src_addr != self.latest_ap_state.bssid.as_array() {
770            let eapol_pdu = &ind.data[..];
771            inspect_log!(context.inspect.rsn_events.lock(), {
772                rx_eapol_frame: InspectBytes(&eapol_pdu),
773                foreign_bssid: MacAddr::from(ind.src_addr).to_string(),
774                current_bssid: self.latest_ap_state.bssid.to_string(),
775                status: "rejected (foreign BSS)",
776            });
777            return Ok(self);
778        }
779
780        self.process_link_state_update(ind, LinkState::on_eapol_ind, context, state_change_ctx)
781    }
782
783    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
784    fn on_eapol_conf(
785        self,
786        resp: fidl_mlme::EapolConfirm,
787        state_change_ctx: &mut Option<StateChangeContext>,
788        context: &mut Context,
789    ) -> Result<Self, Disconnecting> {
790        self.process_link_state_update(resp, LinkState::on_eapol_conf, context, state_change_ctx)
791    }
792
793    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
794    fn on_set_keys_conf(
795        self,
796        conf: fidl_mlme::SetKeysConfirm,
797        state_change_ctx: &mut Option<StateChangeContext>,
798        context: &mut Context,
799    ) -> Result<Self, Disconnecting> {
800        self.process_link_state_update(conf, LinkState::on_set_keys_conf, context, state_change_ctx)
801    }
802
803    fn on_channel_switched(&mut self, info: fidl_internal::ChannelSwitchInfo) {
804        self.connect_txn_sink.send(ConnectTransactionEvent::OnChannelSwitched { info });
805        self.latest_ap_state.channel.primary = info.new_channel;
806        self.last_channel_switch_time = Some(now());
807    }
808
809    fn on_wmm_status_resp(
810        &mut self,
811        status: zx::sys::zx_status_t,
812        resp: fidl_internal::WmmStatusResponse,
813    ) {
814        if status == zx::sys::ZX_OK {
815            let wmm_param = self.wmm_param.get_or_insert_default();
816            let mut wmm_info = wmm_param.wmm_info.ap_wmm_info();
817            wmm_info.set_uapsd(resp.apsd);
818            wmm_param.wmm_info.0 = wmm_info.0;
819            update_wmm_ac_param(&mut wmm_param.ac_be_params, &resp.ac_be_params);
820            update_wmm_ac_param(&mut wmm_param.ac_bk_params, &resp.ac_bk_params);
821            update_wmm_ac_param(&mut wmm_param.ac_vo_params, &resp.ac_vo_params);
822            update_wmm_ac_param(&mut wmm_param.ac_vi_params, &resp.ac_vi_params);
823        }
824    }
825
826    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
827    fn handle_timeout(
828        mut self,
829        event: Event,
830        state_change_ctx: &mut Option<StateChangeContext>,
831        context: &mut Context,
832    ) -> Result<Self, Disconnecting> {
833        match self.link_state.handle_timeout(event, state_change_ctx, context) {
834            Ok(link_state) => Ok(Associated { link_state, ..self }),
835            Err(failure_reason) => {
836                send_deauthenticate_request(&self.latest_ap_state.bssid, &context.mlme_sink);
837                let timeout = context.timer.schedule(event::DeauthenticateTimeout);
838                match self.roam_in_progress {
839                    Some(roam_initiator) => {
840                        report_roam_finished(
841                            &mut self.connect_txn_sink,
842                            RoamFailure {
843                                failure_type: RoamFailureType::EstablishRsnaFailure,
844                                selected_bssid: self.latest_ap_state.bssid,
845                                status_code: fidl_ieee80211::StatusCode::EstablishRsnaFailure,
846                                disconnect_info: make_roam_disconnect_info(
847                                    roam_initiator.into(),
848                                    Some(fidl_ieee80211::ReasonCode::Timeout),
849                                ),
850                                auth_method: self.auth_method,
851                                selected_bss: Some(*self.latest_ap_state),
852                                establish_rsna_failure_reason: Some(failure_reason),
853                            }
854                            .into(),
855                        );
856                    }
857                    _ => {
858                        report_connect_finished(
859                            &mut self.connect_txn_sink,
860                            EstablishRsnaFailure {
861                                auth_method: self.auth_method,
862                                reason: failure_reason,
863                            }
864                            .into(),
865                        );
866                    }
867                }
868
869                Err(Disconnecting {
870                    cfg: self.cfg,
871                    action: PostDisconnectAction::None,
872                    _timeout: Some(timeout),
873                })
874            }
875        }
876    }
877
878    fn disconnect(self, context: &mut Context, action: PostDisconnectAction) -> Disconnecting {
879        send_deauthenticate_request(&self.latest_ap_state.bssid, &context.mlme_sink);
880        let timeout = context.timer.schedule(event::DeauthenticateTimeout);
881        Disconnecting { cfg: self.cfg, action, _timeout: Some(timeout) }
882    }
883}
884
885// Used when the next state after a roam failure is conditionally determined (e.g. if target needs deauth).
886#[allow(clippy::large_enum_variant)] // TODO(https://fxbug.dev/401087337)
887enum AfterRoamFailureState {
888    Idle(Idle),
889    Disconnecting(Disconnecting),
890}
891
892impl Roaming {
893    // Disassociation while roaming requires special handling. Since roaming causes loss of
894    // association with the original BSS, we must ignore a disassoc from the original BSS. But a
895    // disassociation from the target BSS means that the roam attempt failed, and we should
896    // transition to Disconnecting.
897    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
898    fn on_disassociate_ind(
899        self,
900        ind: fidl_mlme::DisassociateIndication,
901        state_change_ctx: &mut Option<StateChangeContext>,
902        context: &mut Context,
903    ) -> Result<Roaming, Disconnecting> {
904        let peer_sta_address: Bssid = ind.peer_sta_address.into();
905        if peer_sta_address != self.cmd.bss.bssid {
906            return Ok(self);
907        }
908
909        let disconnect_info = make_roam_disconnect_info(
910            fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
911            Some(ind.reason_code),
912        );
913
914        let failure = RoamFailure {
915            failure_type: RoamFailureType::ReassociationFailure,
916            selected_bss: Some(*self.cmd.bss.clone()),
917            disconnect_info,
918            selected_bssid: self.cmd.bss.bssid,
919            auth_method: self.auth_method,
920            status_code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
921            establish_rsna_failure_reason: None,
922        };
923        let msg = format!("Roam failed due to disassociation, reason_code: {:?}", ind.reason_code);
924        Err(Self::to_disconnecting(
925            msg,
926            failure,
927            self.cfg,
928            self.cmd.connect_txn_sink,
929            state_change_ctx,
930            context,
931        ))
932    }
933
934    // Deauthentication while roaming requires special handling. If the deauth came from the
935    // original BSS, we ignore it; but if if deauth came from target BSS, we move to Idle.
936    fn on_deauthenticate_ind(
937        self,
938        ind: fidl_mlme::DeauthenticateIndication,
939        state_change_ctx: &mut Option<StateChangeContext>,
940    ) -> Result<Roaming, Idle> {
941        let peer_sta_address: Bssid = ind.peer_sta_address.into();
942        if peer_sta_address != self.cmd.bss.bssid {
943            return Ok(self);
944        }
945
946        let disconnect_info = make_roam_disconnect_info(
947            fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
948            Some(ind.reason_code),
949        );
950
951        let failure = RoamFailure {
952            failure_type: RoamFailureType::ReassociationFailure,
953            selected_bss: Some(*self.cmd.bss.clone()),
954            disconnect_info,
955            selected_bssid: self.cmd.bss.bssid,
956            auth_method: self.auth_method,
957            status_code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
958            establish_rsna_failure_reason: None,
959        };
960        let msg =
961            format!("Roam failed due to deauthentication, reason_code: {:?}", ind.reason_code);
962        Err(self.to_idle(msg, failure, state_change_ctx))
963    }
964
965    fn on_sae_handshake_ind(
966        &mut self,
967        ind: fidl_mlme::SaeHandshakeIndication,
968        context: &mut Context,
969    ) -> Result<(), anyhow::Error> {
970        process_sae_handshake_ind(&mut self.cmd.protection, ind, context)
971    }
972
973    fn on_sae_frame_rx(
974        &mut self,
975        frame: fidl_mlme::SaeFrame,
976        context: &mut Context,
977    ) -> Result<(), anyhow::Error> {
978        process_sae_frame_rx(&mut self.cmd.protection, frame, context)
979    }
980
981    fn handle_timeout(
982        mut self,
983        event: Event,
984        state_change_ctx: &mut Option<StateChangeContext>,
985        context: &mut Context,
986    ) -> Result<Self, Idle> {
987        match process_sae_timeout(&mut self.cmd.protection, self.cmd.bss.bssid, event, context) {
988            Ok(()) => Ok(self),
989            Err(e) => {
990                // An error in handling a timeout means that we may have no way to abort a failed
991                // handshake. Drop to idle.
992                let msg = format!("failed to handle SAE timeout: {e:?}");
993                let disconnect_info = make_roam_disconnect_info(
994                    fidl_sme::DisconnectMlmeEventName::SaeHandshakeResponse,
995                    None,
996                );
997                // Send ReassociationFailure here, similar to the AssociationFailure returned by SAE
998                // timeout handler in connect path.
999                let failure = RoamFailure {
1000                    failure_type: RoamFailureType::ReassociationFailure,
1001                    selected_bss: Some(*self.cmd.bss.clone()),
1002                    disconnect_info,
1003                    selected_bssid: self.cmd.bss.bssid,
1004                    auth_method: self.auth_method,
1005                    status_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1006                    establish_rsna_failure_reason: None,
1007                };
1008
1009                // In the unlikely case that we are authenticated but the last frame was dropped,
1010                // send a deauth.
1011                send_deauthenticate_request(&self.cmd.bss.bssid, &context.mlme_sink);
1012
1013                Err(self.to_idle(msg, failure, state_change_ctx))
1014            }
1015        }
1016    }
1017
1018    fn to_disconnecting(
1019        //self,
1020        msg: String,
1021        failure: RoamFailure,
1022        cfg: ClientConfig,
1023        sink: ConnectTransactionSink,
1024        state_change_ctx: &mut Option<StateChangeContext>,
1025        context: &mut Context,
1026    ) -> Disconnecting {
1027        warn!("{}", msg);
1028        _ = state_change_ctx.replace(StateChangeContext::Disconnect {
1029            msg,
1030            disconnect_source: failure.disconnect_info.disconnect_source,
1031        });
1032
1033        send_deauthenticate_request(&failure.selected_bssid, &context.mlme_sink);
1034        let timeout = context.timer.schedule(event::DeauthenticateTimeout);
1035
1036        Disconnecting {
1037            cfg,
1038            action: PostDisconnectAction::ReportRoamFinished { sink, result: failure.into() },
1039            _timeout: Some(timeout),
1040        }
1041    }
1042
1043    #[allow(clippy::wrong_self_convention, reason = "mass allow for https://fxbug.dev/381896734")]
1044    fn to_idle(
1045        mut self,
1046        msg: String,
1047        failure: RoamFailure,
1048        state_change_ctx: &mut Option<StateChangeContext>,
1049    ) -> Idle {
1050        warn!("{}", msg);
1051        _ = state_change_ctx.replace(StateChangeContext::Disconnect {
1052            msg,
1053            disconnect_source: failure.disconnect_info.disconnect_source,
1054        });
1055        report_roam_finished(&mut self.cmd.connect_txn_sink, failure.into());
1056        Idle { cfg: self.cfg }
1057    }
1058}
1059
1060impl Disconnecting {
1061    fn handle_deauthenticate_conf(
1062        self,
1063        _conf: fidl_mlme::DeauthenticateConfirm,
1064        state_change_ctx: &mut Option<StateChangeContext>,
1065        context: &mut Context,
1066    ) -> AfterDisconnectState {
1067        Idle { cfg: self.cfg }.on_disconnect_complete(context, self.action, state_change_ctx)
1068    }
1069
1070    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
1071    fn handle_timeout(
1072        self,
1073        event: Event,
1074        state_change_ctx: &mut Option<StateChangeContext>,
1075        context: &mut Context,
1076    ) -> Result<Self, AfterDisconnectState> {
1077        if let Event::DeauthenticateTimeout(_) = event {
1078            let msg = "Completing disconnect without confirm due to disconnect timeout".to_string();
1079            error!("{}", msg);
1080            state_change_ctx.set_msg(msg);
1081            return Err(Idle { cfg: self.cfg }.on_disconnect_complete(
1082                context,
1083                self.action,
1084                state_change_ctx,
1085            ));
1086        }
1087        Ok(self)
1088    }
1089
1090    fn disconnect(self, action: PostDisconnectAction) -> Disconnecting {
1091        // We were already disconnecting, so we don't need to initiate another request.
1092        // Instead, clean up the state from the previous attempt and set the new
1093        // post-disconnect action.
1094        match self.action {
1095            PostDisconnectAction::RespondDisconnect { responder } => {
1096                if let Err(e) = responder.send() {
1097                    error!("Failed to send disconnect response: {}", e);
1098                }
1099            }
1100            PostDisconnectAction::ReportConnectFinished { mut sink, result } => {
1101                report_connect_finished(&mut sink, result);
1102            }
1103            PostDisconnectAction::ReportRoamFinished { mut sink, result } => {
1104                report_roam_finished(&mut sink, result);
1105            }
1106            PostDisconnectAction::BeginConnect { mut cmd } => {
1107                report_connect_finished(&mut cmd.connect_txn_sink, ConnectResult::Canceled);
1108            }
1109            PostDisconnectAction::None => (),
1110        }
1111        Disconnecting { action, ..self }
1112    }
1113}
1114
1115impl ClientState {
1116    pub fn new(cfg: ClientConfig) -> Self {
1117        Self::from(State::new(Idle { cfg }))
1118    }
1119
1120    fn state_name(&self) -> &'static str {
1121        match self {
1122            Self::Idle(_) => IDLE_STATE,
1123            Self::Connecting(_) => CONNECTING_STATE,
1124            Self::Associated(state) => match state.link_state {
1125                LinkState::EstablishingRsna(_) => RSNA_STATE,
1126                LinkState::LinkUp(_) => LINK_UP_STATE,
1127                // LinkState always transition to EstablishingRsna or LinkUp on initialization
1128                // and never transition back
1129                #[expect(clippy::unreachable)]
1130                _ => unreachable!(),
1131            },
1132            Self::Roaming(_) => ROAMING_STATE,
1133            Self::Disconnecting(_) => DISCONNECTING_STATE,
1134        }
1135    }
1136
1137    pub fn on_mlme_event(self, event: MlmeEvent, context: &mut Context) -> Self {
1138        let start_state = self.state_name();
1139        let mut state_change_ctx: Option<StateChangeContext> = None;
1140
1141        let new_state = match self {
1142            Self::Idle(_) => {
1143                match event {
1144                    MlmeEvent::OnWmmStatusResp { .. } => (),
1145                    MlmeEvent::DeauthenticateConf { resp } => {
1146                        warn!(
1147                            "Unexpected MLME message while Idle: {:?} for BSSID {:?}",
1148                            mlme_event_name(&event),
1149                            resp.peer_sta_address
1150                        );
1151                    }
1152                    _ => warn!("Unexpected MLME message while Idle: {:?}", mlme_event_name(&event)),
1153                }
1154                self
1155            }
1156            Self::Connecting(state) => match event {
1157                MlmeEvent::ConnectConf { resp } => {
1158                    let (transition, connecting) = state.release_data();
1159                    match connecting.on_connect_conf(resp, &mut state_change_ctx, context) {
1160                        Ok(associated) => transition.to(associated).into(),
1161                        Err(disconnecting) => transition.to(disconnecting).into(),
1162                    }
1163                }
1164                MlmeEvent::DeauthenticateInd { ind } => {
1165                    let (transition, connecting) = state.release_data();
1166                    let idle = connecting.on_deauthenticate_ind(ind, &mut state_change_ctx);
1167                    transition.to(idle).into()
1168                }
1169                MlmeEvent::DisassociateInd { ind } => {
1170                    let (transition, connecting) = state.release_data();
1171                    let disconnecting =
1172                        connecting.on_disassociate_ind(ind, &mut state_change_ctx, context);
1173                    transition.to(disconnecting).into()
1174                }
1175                MlmeEvent::OnSaeHandshakeInd { ind } => {
1176                    let (transition, mut connecting) = state.release_data();
1177                    if let Err(e) = connecting.on_sae_handshake_ind(ind, context) {
1178                        error!("Failed to process SaeHandshakeInd: {:?}", e);
1179                    }
1180                    transition.to(connecting).into()
1181                }
1182                MlmeEvent::OnSaeFrameRx { frame } => {
1183                    let (transition, mut connecting) = state.release_data();
1184                    if let Err(e) = connecting.on_sae_frame_rx(frame, context) {
1185                        error!("Failed to process SaeFrameRx: {:?}", e);
1186                    }
1187                    transition.to(connecting).into()
1188                }
1189                _ => state.into(),
1190            },
1191            Self::Associated(mut state) => match event {
1192                MlmeEvent::DisassociateInd { ind } => {
1193                    let (transition, associated) = state.release_data();
1194                    match associated.on_disassociate_ind(ind, &mut state_change_ctx, context) {
1195                        Ok(connecting) => transition.to(connecting).into(),
1196                        Err(disconnecting) => transition.to(disconnecting).into(),
1197                    }
1198                }
1199                MlmeEvent::DeauthenticateInd { ind } => {
1200                    let (transition, associated) = state.release_data();
1201                    let idle = associated.on_deauthenticate_ind(ind, &mut state_change_ctx);
1202                    transition.to(idle).into()
1203                }
1204                MlmeEvent::SignalReport { ind } => {
1205                    if matches!(state.link_state, LinkState::LinkUp(_)) {
1206                        state
1207                            .connect_txn_sink
1208                            .send(ConnectTransactionEvent::OnSignalReport { ind });
1209                    }
1210                    state.latest_ap_state.rssi_dbm = ind.rssi_dbm;
1211                    state.latest_ap_state.snr_db = ind.snr_db;
1212                    state.last_signal_report_time = now();
1213                    state.into()
1214                }
1215                MlmeEvent::EapolInd { ind } => {
1216                    let (transition, associated) = state.release_data();
1217                    match associated.on_eapol_ind(ind, &mut state_change_ctx, context) {
1218                        Ok(associated) => transition.to(associated).into(),
1219                        Err(disconnecting) => transition.to(disconnecting).into(),
1220                    }
1221                }
1222                MlmeEvent::EapolConf { resp } => {
1223                    let (transition, associated) = state.release_data();
1224                    match associated.on_eapol_conf(resp, &mut state_change_ctx, context) {
1225                        Ok(associated) => transition.to(associated).into(),
1226                        Err(disconnecting) => transition.to(disconnecting).into(),
1227                    }
1228                }
1229                MlmeEvent::SetKeysConf { conf } => {
1230                    let (transition, associated) = state.release_data();
1231                    match associated.on_set_keys_conf(conf, &mut state_change_ctx, context) {
1232                        Ok(associated) => transition.to(associated).into(),
1233                        Err(disconnecting) => transition.to(disconnecting).into(),
1234                    }
1235                }
1236                MlmeEvent::OnChannelSwitched { info } => {
1237                    state.on_channel_switched(info);
1238                    state.into()
1239                }
1240                MlmeEvent::OnWmmStatusResp { status, resp } => {
1241                    state.on_wmm_status_resp(status, resp);
1242                    state.into()
1243                }
1244                MlmeEvent::RoamStartInd { ind } => {
1245                    _ = state_change_ctx.replace(StateChangeContext::Msg(
1246                        "Fullmac-initiated roam initiated".to_owned(),
1247                    ));
1248                    let (transition, associated) = state.release_data();
1249                    match roam_internal(
1250                        associated,
1251                        context,
1252                        ind.selected_bssid.into(),
1253                        ind.selected_bss,
1254                        &mut state_change_ctx,
1255                        RoamInitiator::RoamStartInd,
1256                    ) {
1257                        Ok(roaming) => transition.to(roaming).into(),
1258                        Err(disconnecting) => transition.to(disconnecting).into(),
1259                    }
1260                }
1261                _ => state.into(),
1262            },
1263            Self::Roaming(state) => match event {
1264                MlmeEvent::OnSaeHandshakeInd { ind } => {
1265                    let (transition, mut roaming) = state.release_data();
1266                    if let Err(e) = roaming.on_sae_handshake_ind(ind, context) {
1267                        error!("Failed to process SaeHandshakeInd: {:?}", e);
1268                    }
1269                    transition.to(roaming).into()
1270                }
1271                MlmeEvent::OnSaeFrameRx { frame } => {
1272                    let (transition, mut roaming) = state.release_data();
1273                    if let Err(e) = roaming.on_sae_frame_rx(frame, context) {
1274                        error!("Failed to process SaeFrameRx: {:?}", e);
1275                    }
1276                    transition.to(roaming).into()
1277                }
1278                MlmeEvent::DisassociateInd { ind } => {
1279                    let (transition, roaming) = state.release_data();
1280                    match roaming.on_disassociate_ind(ind, &mut state_change_ctx, context) {
1281                        Ok(roaming) => transition.to(roaming).into(),
1282                        Err(disconnecting) => transition.to(disconnecting).into(),
1283                    }
1284                }
1285                MlmeEvent::DeauthenticateInd { ind } => {
1286                    let (transition, roaming) = state.release_data();
1287                    match roaming.on_deauthenticate_ind(ind, &mut state_change_ctx) {
1288                        Ok(roaming) => transition.to(roaming).into(),
1289                        Err(idle) => transition.to(idle).into(),
1290                    }
1291                }
1292                MlmeEvent::RoamConf { conf } => {
1293                    let (transition, roaming) = state.release_data();
1294
1295                    match roam_handle_result(
1296                        roaming,
1297                        RoamResultFields {
1298                            selected_bssid: conf.selected_bssid.into(),
1299                            status_code: conf.status_code,
1300                            original_association_maintained: conf.original_association_maintained,
1301                            target_bss_authenticated: conf.target_bss_authenticated,
1302                            association_ies: conf.association_ies,
1303                        },
1304                        context,
1305                        &mut state_change_ctx,
1306                        RoamInitiator::RoamRequest,
1307                    ) {
1308                        Ok(associated) => transition.to(associated).into(),
1309                        Err(after_roam_failure_state) => match after_roam_failure_state {
1310                            AfterRoamFailureState::Disconnecting(disconnecting) => {
1311                                transition.to(disconnecting).into()
1312                            }
1313                            AfterRoamFailureState::Idle(idle) => transition.to(idle).into(),
1314                        },
1315                    }
1316                }
1317                MlmeEvent::RoamResultInd { ind } => {
1318                    let (transition, roaming) = state.release_data();
1319
1320                    match roam_handle_result(
1321                        roaming,
1322                        RoamResultFields {
1323                            selected_bssid: ind.selected_bssid.into(),
1324                            status_code: ind.status_code,
1325                            original_association_maintained: ind.original_association_maintained,
1326                            target_bss_authenticated: ind.target_bss_authenticated,
1327                            association_ies: ind.association_ies,
1328                        },
1329                        context,
1330                        &mut state_change_ctx,
1331                        RoamInitiator::RoamStartInd,
1332                    ) {
1333                        Ok(associated) => transition.to(associated).into(),
1334                        Err(after_roam_failure_state) => match after_roam_failure_state {
1335                            AfterRoamFailureState::Disconnecting(disconnecting) => {
1336                                transition.to(disconnecting).into()
1337                            }
1338                            AfterRoamFailureState::Idle(idle) => transition.to(idle).into(),
1339                        },
1340                    }
1341                }
1342                _ => state.into(),
1343            },
1344            Self::Disconnecting(state) => match event {
1345                MlmeEvent::DeauthenticateConf { resp } => {
1346                    let (transition, disconnecting) = state.release_data();
1347                    match disconnecting.handle_deauthenticate_conf(
1348                        resp,
1349                        &mut state_change_ctx,
1350                        context,
1351                    ) {
1352                        AfterDisconnectState::Idle(idle) => transition.to(idle).into(),
1353                        AfterDisconnectState::Connecting(connecting) => {
1354                            transition.to(connecting).into()
1355                        }
1356                    }
1357                }
1358                _ => state.into(),
1359            },
1360        };
1361
1362        log_state_change(start_state, &new_state, state_change_ctx, context);
1363        new_state
1364    }
1365
1366    pub fn handle_timeout(self, event: Event, context: &mut Context) -> Self {
1367        let start_state = self.state_name();
1368        let mut state_change_ctx: Option<StateChangeContext> = None;
1369
1370        let new_state = match self {
1371            Self::Connecting(state) => {
1372                let (transition, connecting) = state.release_data();
1373                match connecting.handle_timeout(event, &mut state_change_ctx, context) {
1374                    Ok(connecting) => transition.to(connecting).into(),
1375                    Err(disconnecting) => transition.to(disconnecting).into(),
1376                }
1377            }
1378            Self::Associated(state) => {
1379                let (transition, associated) = state.release_data();
1380                match associated.handle_timeout(event, &mut state_change_ctx, context) {
1381                    Ok(associated) => transition.to(associated).into(),
1382                    Err(disconnecting) => transition.to(disconnecting).into(),
1383                }
1384            }
1385            Self::Roaming(state) => {
1386                let (transition, roaming) = state.release_data();
1387                match roaming.handle_timeout(event, &mut state_change_ctx, context) {
1388                    Ok(roaming) => transition.to(roaming).into(),
1389                    Err(idle) => transition.to(idle).into(),
1390                }
1391            }
1392            Self::Disconnecting(state) => {
1393                let (transition, disconnecting) = state.release_data();
1394                match disconnecting.handle_timeout(event, &mut state_change_ctx, context) {
1395                    Ok(disconnecting) => transition.to(disconnecting).into(),
1396                    Err(after_disconnect) => match after_disconnect {
1397                        AfterDisconnectState::Idle(idle) => transition.to(idle).into(),
1398                        AfterDisconnectState::Connecting(connecting) => {
1399                            transition.to(connecting).into()
1400                        }
1401                    },
1402                }
1403            }
1404            _ => self,
1405        };
1406
1407        log_state_change(start_state, &new_state, state_change_ctx, context);
1408        new_state
1409    }
1410
1411    pub fn connect(self, cmd: ConnectCommand, context: &mut Context) -> Self {
1412        let start_state = self.state_name();
1413        let mut state_change_ctx: Option<StateChangeContext> = None;
1414
1415        let new_state = self.disconnect_internal(
1416            context,
1417            PostDisconnectAction::BeginConnect { cmd },
1418            &mut state_change_ctx,
1419        );
1420
1421        log_state_change(start_state, &new_state, state_change_ctx, context);
1422        new_state
1423    }
1424
1425    pub fn roam(self, context: &mut Context, selected_bss: fidl_common::BssDescription) -> Self {
1426        let start_state = self.state_name();
1427        let mut state_change_ctx =
1428            Some(StateChangeContext::Msg("Policy-initiated roam attempt in progress".to_owned()));
1429
1430        let new_state = match self {
1431            ClientState::Associated(state) => {
1432                let (transition, state) = state.release_data();
1433                match roam_internal(
1434                    state,
1435                    context,
1436                    selected_bss.bssid.into(),
1437                    selected_bss,
1438                    &mut state_change_ctx,
1439                    RoamInitiator::RoamRequest,
1440                ) {
1441                    Ok(roaming) => transition.to(roaming).into(),
1442                    Err(disconnecting) => transition.to(disconnecting).into(),
1443                }
1444            }
1445            // The roam function should only be called from Associated state; these branches are
1446            // present for logging, but they should be fairly rare. If we see these, it is likely
1447            // due to a roam request coming in during the short periods when one layer is
1448            // transitioning (e.g. SME started a disconnect just as Policy sent a roam request).
1449            ClientState::Connecting(state) => {
1450                warn!("Requested roam could not be attempted, client is connecting");
1451                state.into()
1452            }
1453            ClientState::Roaming(state) => {
1454                error!("Overlapping roam request ignored, client is already roaming");
1455                state.into()
1456            }
1457            ClientState::Disconnecting(state) => {
1458                warn!("Requested roam could not be attempted, client is disconnecting");
1459                state.into()
1460            }
1461            ClientState::Idle(state) => {
1462                warn!("Requested roam could not be attempted, client is idle");
1463                state.into()
1464            }
1465        };
1466
1467        log_state_change(start_state, &new_state, state_change_ctx, context);
1468        new_state
1469    }
1470
1471    pub fn disconnect(
1472        mut self,
1473        context: &mut Context,
1474        user_disconnect_reason: fidl_sme::UserDisconnectReason,
1475        responder: fidl_sme::ClientSmeDisconnectResponder,
1476    ) -> Self {
1477        let mut disconnected_from_link_up = false;
1478        let disconnect_source = fidl_sme::DisconnectSource::User(user_disconnect_reason);
1479        if let Self::Associated(state) = &mut self
1480            && let LinkState::LinkUp(_link_up) = &state.link_state
1481        {
1482            disconnected_from_link_up = true;
1483            let fidl_disconnect_info =
1484                fidl_sme::DisconnectInfo { is_sme_reconnecting: false, disconnect_source };
1485            state
1486                .connect_txn_sink
1487                .send(ConnectTransactionEvent::OnDisconnect { info: fidl_disconnect_info });
1488        }
1489
1490        let start_state = self.state_name();
1491
1492        let new_state = self.disconnect_internal(
1493            context,
1494            PostDisconnectAction::RespondDisconnect { responder },
1495            &mut None,
1496        );
1497
1498        let msg =
1499            format!("received disconnect command from user; reason {user_disconnect_reason:?}");
1500        let state_change_ctx = Some(if disconnected_from_link_up {
1501            StateChangeContext::Disconnect { msg, disconnect_source }
1502        } else {
1503            StateChangeContext::Msg(msg)
1504        });
1505        log_state_change(start_state, &new_state, state_change_ctx, context);
1506        new_state
1507    }
1508
1509    fn disconnect_internal(
1510        self,
1511        context: &mut Context,
1512        action: PostDisconnectAction,
1513        state_change_ctx: &mut Option<StateChangeContext>,
1514    ) -> Self {
1515        match self {
1516            Self::Idle(state) => {
1517                let (transition, state) = state.release_data();
1518                match state.on_disconnect_complete(context, action, state_change_ctx) {
1519                    AfterDisconnectState::Idle(idle) => transition.to(idle).into(),
1520                    AfterDisconnectState::Connecting(connecting) => {
1521                        transition.to(connecting).into()
1522                    }
1523                }
1524            }
1525            Self::Connecting(state) => {
1526                let (transition, state) = state.release_data();
1527                transition.to(state.disconnect(context, action)).into()
1528            }
1529            Self::Associated(state) => {
1530                let (transition, state) = state.release_data();
1531                transition.to(state.disconnect(context, action)).into()
1532            }
1533            Self::Roaming(state) => {
1534                let (transition, state) = state.release_data();
1535                transition.to(Idle { cfg: state.cfg }).into()
1536            }
1537            Self::Disconnecting(state) => {
1538                let (transition, state) = state.release_data();
1539                transition.to(state.disconnect(action)).into()
1540            }
1541        }
1542    }
1543
1544    // Cancel any connect that is in progress. No-op if client is already idle or connected.
1545    pub fn cancel_ongoing_connect(self, context: &mut Context) -> Self {
1546        // Only move to idle if client is not already connected. Technically, SME being in
1547        // transition state does not necessarily mean that a (manual) connect attempt is
1548        // in progress (since DisassociateInd moves SME to transition state). However, the
1549        // main thing we are concerned about is that we don't disconnect from an already
1550        // connected state until the new connect attempt succeeds in selecting BSS.
1551        if self.in_transition_state() {
1552            let mut state_change_ctx = None;
1553            self.disconnect_internal(context, PostDisconnectAction::None, &mut state_change_ctx)
1554        } else {
1555            self
1556        }
1557    }
1558
1559    #[allow(
1560        clippy::match_like_matches_macro,
1561        reason = "mass allow for https://fxbug.dev/381896734"
1562    )]
1563    fn in_transition_state(&self) -> bool {
1564        match self {
1565            Self::Idle(_) => false,
1566            Self::Associated(state) => match state.link_state {
1567                LinkState::LinkUp { .. } => false,
1568                _ => true,
1569            },
1570            Self::Roaming(_) => false,
1571            _ => true,
1572        }
1573    }
1574
1575    pub fn status(&self) -> ClientSmeStatus {
1576        match self {
1577            Self::Idle(_) => ClientSmeStatus::Idle,
1578            Self::Connecting(connecting) => {
1579                ClientSmeStatus::Connecting(connecting.cmd.bss.ssid.clone())
1580            }
1581            Self::Associated(associated) => match associated.link_state {
1582                LinkState::EstablishingRsna { .. } => {
1583                    ClientSmeStatus::Connecting(associated.latest_ap_state.ssid.clone())
1584                }
1585                LinkState::LinkUp { .. } => {
1586                    let latest_ap_state = &associated.latest_ap_state;
1587                    ClientSmeStatus::Connected(ServingApInfo {
1588                        bssid: latest_ap_state.bssid,
1589                        ssid: latest_ap_state.ssid.clone(),
1590                        rssi_dbm: latest_ap_state.rssi_dbm,
1591                        snr_db: latest_ap_state.snr_db,
1592                        signal_report_time: associated.last_signal_report_time,
1593                        channel: latest_ap_state.channel,
1594                        protection: latest_ap_state.protection(),
1595                        ht_cap: latest_ap_state.raw_ht_cap(),
1596                        vht_cap: latest_ap_state.raw_vht_cap(),
1597                        probe_resp_wsc: latest_ap_state.probe_resp_wsc(),
1598                        wmm_param: associated.wmm_param,
1599                    })
1600                }
1601                // LinkState always transition to EstablishingRsna or LinkUp on initialization
1602                // and never transition back
1603                #[expect(clippy::unreachable)]
1604                _ => unreachable!(),
1605            },
1606            Self::Roaming(roaming) => ClientSmeStatus::Roaming(roaming.cmd.bss.bssid),
1607            Self::Disconnecting(disconnecting) => match &disconnecting.action {
1608                PostDisconnectAction::BeginConnect { cmd } => {
1609                    ClientSmeStatus::Connecting(cmd.bss.ssid.clone())
1610                }
1611                _ => ClientSmeStatus::Idle,
1612            },
1613        }
1614    }
1615}
1616
1617#[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
1618fn roam_internal(
1619    state: Associated,
1620    context: &mut Context,
1621    selected_bssid: Bssid,
1622    selected_bss: fidl_common::BssDescription,
1623    state_change_ctx: &mut Option<StateChangeContext>,
1624    roam_initiator: RoamInitiator,
1625) -> Result<Roaming, Disconnecting> {
1626    // Reassociation is imminent, so consider client disassociated from original BSS. Need a new ESS-SA.
1627    let (mut orig_bss_protection, _connected_duration) = state.link_state.disconnect();
1628    if let Protection::Rsna(rsna) = &mut orig_bss_protection {
1629        // Reset the state of the ESS-SA and its replay counter to zero per IEEE 802.11-2016 12.7.2.
1630        rsna.supplicant.reset();
1631    }
1632
1633    // If a failure occurs in this function, SME will need to know how to log the failure, and where
1634    // to send a deauth.
1635    let (mlme_event_name, deauth_addr) = match roam_initiator {
1636        // RoamStartInd means that auth may have already started with target BSS.
1637        RoamInitiator::RoamStartInd => {
1638            (fidl_sme::DisconnectMlmeEventName::RoamStartIndication, &selected_bssid)
1639        }
1640        // RoamRequest means that client is only authenticated with the original BSS.
1641        RoamInitiator::RoamRequest => {
1642            (fidl_sme::DisconnectMlmeEventName::RoamRequest, &state.latest_ap_state.bssid)
1643        }
1644    };
1645
1646    let selected_bss = match BssDescription::try_from(selected_bss) {
1647        Ok(selected_bss) => selected_bss,
1648        Err(error) => {
1649            error!("Roam cannot proceed due to missing/malformed BSS description: {:?}", error);
1650
1651            send_deauthenticate_request(deauth_addr, &context.mlme_sink);
1652            let timeout = context.timer.schedule(event::DeauthenticateTimeout);
1653
1654            let disconnect_info = make_roam_disconnect_info(mlme_event_name, None);
1655
1656            let failure_type = match roam_initiator {
1657                RoamInitiator::RoamStartInd => RoamFailureType::RoamStartMalformedFailure,
1658                RoamInitiator::RoamRequest => RoamFailureType::RoamRequestMalformedFailure,
1659            };
1660
1661            return Err(Disconnecting {
1662                cfg: state.cfg,
1663                action: PostDisconnectAction::ReportRoamFinished {
1664                    sink: state.connect_txn_sink,
1665                    result: RoamFailure {
1666                        failure_type,
1667                        selected_bss: None,
1668                        disconnect_info,
1669                        selected_bssid,
1670                        auth_method: state.auth_method,
1671                        status_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1672                        establish_rsna_failure_reason: None,
1673                    }
1674                    .into(),
1675                },
1676                _timeout: Some(timeout),
1677            });
1678        }
1679    };
1680
1681    let authentication = state.authentication.clone();
1682    let selected_bss_protection = match SecurityAuthenticator::try_from(authentication)
1683        .map_err(From::from)
1684        .and_then(|authenticator| {
1685            Protection::try_from(SecurityContext {
1686                security: &authenticator,
1687                device: &context.device_info,
1688                security_support: &context.security_support,
1689                config: &state.cfg,
1690                bss: &selected_bss.clone(),
1691            })
1692        }) {
1693        Ok(protection) => protection,
1694        Err(error) => {
1695            error!("Failed to configure protection for selected BSS during roam: {:?}", error);
1696
1697            send_deauthenticate_request(deauth_addr, &context.mlme_sink);
1698            let timeout = context.timer.schedule(event::DeauthenticateTimeout);
1699
1700            let disconnect_info = make_roam_disconnect_info(mlme_event_name, None);
1701
1702            return Err(Disconnecting {
1703                cfg: state.cfg,
1704                action: PostDisconnectAction::ReportRoamFinished {
1705                    sink: state.connect_txn_sink,
1706                    result: RoamFailure {
1707                        status_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1708                        failure_type: RoamFailureType::SelectNetworkFailure,
1709                        selected_bssid: selected_bss.bssid,
1710                        disconnect_info,
1711                        auth_method: state.auth_method,
1712                        selected_bss: Some(selected_bss),
1713                        establish_rsna_failure_reason: None,
1714                    }
1715                    .into(),
1716                },
1717                _timeout: Some(timeout),
1718            });
1719        }
1720    };
1721
1722    // Policy-initiated roam requires that SME send a roam request to MLME.
1723    if matches!(roam_initiator, RoamInitiator::RoamRequest) {
1724        let roam_req = fidl_mlme::RoamRequest { selected_bss: selected_bss.clone().into() };
1725        context.mlme_sink.send(MlmeRequest::Roam(roam_req));
1726    }
1727
1728    _ = state_change_ctx.replace(StateChangeContext::Roam {
1729        msg: "Roam attempt in progress".to_owned(),
1730        bssid: selected_bssid,
1731    });
1732    Ok(Roaming {
1733        cfg: state.cfg,
1734        cmd: ConnectCommand {
1735            bss: Box::new(selected_bss),
1736            connect_txn_sink: state.connect_txn_sink,
1737            protection: selected_bss_protection,
1738            authentication: state.authentication,
1739        },
1740        auth_method: state.auth_method,
1741        // protection_ie from original connection is preserved.
1742        protection_ie: state.protection_ie,
1743    })
1744}
1745
1746struct RoamResultFields {
1747    selected_bssid: Bssid,
1748    status_code: fidl_ieee80211::StatusCode,
1749    original_association_maintained: bool,
1750    target_bss_authenticated: bool,
1751    association_ies: Vec<u8>,
1752}
1753
1754// If the roam attempt succeeded, move into Associated with the selected BSS.
1755// If the roam attempt failed, return the next state, wrapped in AfterRoamFailureState.
1756#[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255153)
1757fn roam_handle_result(
1758    mut state: Roaming,
1759    result_fields: RoamResultFields,
1760    context: &mut Context,
1761    state_change_ctx: &mut Option<StateChangeContext>,
1762    roam_initiator: RoamInitiator,
1763) -> Result<Associated, AfterRoamFailureState> {
1764    if result_fields.original_association_maintained {
1765        warn!(
1766            "Roam result claims that device is still associated with original BSS, but Fast BSS Transition is currently unsupported"
1767        );
1768    }
1769
1770    // If a failure occurs in this function, SME will need to know how to log the failure.
1771    let mlme_event_name = match roam_initiator {
1772        RoamInitiator::RoamStartInd => fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
1773        RoamInitiator::RoamRequest => fidl_sme::DisconnectMlmeEventName::RoamConfirmation,
1774    };
1775
1776    if result_fields.selected_bssid != state.cmd.bss.bssid {
1777        let disconnect_info = make_roam_disconnect_info(mlme_event_name, None);
1778        let failure_type = match roam_initiator {
1779            RoamInitiator::RoamStartInd => RoamFailureType::RoamResultMalformedFailure,
1780            RoamInitiator::RoamRequest => RoamFailureType::RoamConfirmationMalformedFailure,
1781        };
1782        let failure = RoamFailure {
1783            failure_type,
1784            selected_bss: None,
1785            disconnect_info,
1786            selected_bssid: state.cmd.bss.bssid,
1787            auth_method: state.auth_method,
1788            status_code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1789            establish_rsna_failure_reason: None,
1790        };
1791        return Err(AfterRoamFailureState::Disconnecting(Roaming::to_disconnecting(
1792            "Roam failed; unexpected BSSID in result".to_owned(),
1793            failure,
1794            state.cfg,
1795            state.cmd.connect_txn_sink,
1796            state_change_ctx,
1797            context,
1798        )));
1799    }
1800
1801    #[allow(clippy::clone_on_copy, reason = "mass allow for https://fxbug.dev/381896734")]
1802    match result_fields.status_code {
1803        fidl_ieee80211::StatusCode::Success => {
1804            let wmm_param = parse_wmm_from_ies(&result_fields.association_ies);
1805
1806            // Get started with link state before going to Associated.
1807            let link_state = match LinkState::new(state.cmd.protection, context) {
1808                Ok(link_state) => link_state,
1809                Err(failure_reason) => {
1810                    let disconnect_info = make_roam_disconnect_info(mlme_event_name, None);
1811                    let failure = RoamFailure {
1812                        failure_type: RoamFailureType::EstablishRsnaFailure,
1813                        selected_bss: Some(*state.cmd.bss.clone()),
1814                        disconnect_info,
1815                        selected_bssid: state.cmd.bss.bssid,
1816                        auth_method: state.auth_method,
1817                        status_code: fidl_ieee80211::StatusCode::EstablishRsnaFailure,
1818                        establish_rsna_failure_reason: Some(failure_reason),
1819                    };
1820
1821                    if result_fields.target_bss_authenticated {
1822                        return Err(AfterRoamFailureState::Disconnecting(
1823                            Roaming::to_disconnecting(
1824                                "Roam failed; SME failed to initialize LinkState".to_owned(),
1825                                failure,
1826                                state.cfg,
1827                                state.cmd.connect_txn_sink,
1828                                state_change_ctx,
1829                                context,
1830                            ),
1831                        ));
1832                    } else {
1833                        report_roam_finished(&mut state.cmd.connect_txn_sink, failure.into());
1834                        return Err(AfterRoamFailureState::Idle(Idle { cfg: state.cfg }));
1835                    }
1836                }
1837            };
1838
1839            let ssid = state.cmd.bss.ssid.clone();
1840            _ = state_change_ctx.replace(StateChangeContext::Connect {
1841                msg: "Fullmac-initiated roam succeeded".to_owned(),
1842                bssid: state.cmd.bss.bssid.clone(),
1843                ssid,
1844            });
1845
1846            let roam_in_progress = match link_state {
1847                LinkState::LinkUp(_) => {
1848                    report_roam_finished(
1849                        &mut state.cmd.connect_txn_sink,
1850                        RoamResult::Success(Box::new(*state.cmd.bss.clone())),
1851                    );
1852                    None
1853                }
1854                _ => Some(roam_initiator),
1855            };
1856
1857            Ok(Associated {
1858                cfg: state.cfg,
1859                connect_txn_sink: state.cmd.connect_txn_sink,
1860                latest_ap_state: state.cmd.bss,
1861                auth_method: state.auth_method,
1862                last_signal_report_time: now(),
1863                link_state,
1864                protection_ie: state.protection_ie,
1865                // TODO(https://fxbug.dev/82654): Remove `wmm_param` field when wlanstack telemetry is deprecated.
1866                wmm_param,
1867                last_channel_switch_time: None,
1868                reassociation_loop_count: 0,
1869                authentication: state.cmd.authentication,
1870                roam_in_progress,
1871            })
1872        }
1873        // Roam attempt failed.
1874        _ => {
1875            let msg = format!("Roam failed, status_code {:?}", result_fields.status_code);
1876            error!("{}", msg);
1877            let disconnect_info = make_roam_disconnect_info(mlme_event_name, None);
1878            let failure = RoamFailure {
1879                failure_type: RoamFailureType::ReassociationFailure,
1880                selected_bss: Some(*state.cmd.bss.clone()),
1881                disconnect_info,
1882                selected_bssid: state.cmd.bss.bssid,
1883                auth_method: state.auth_method,
1884                status_code: result_fields.status_code,
1885                establish_rsna_failure_reason: None,
1886            };
1887
1888            if result_fields.target_bss_authenticated {
1889                Err(AfterRoamFailureState::Disconnecting(Roaming::to_disconnecting(
1890                    msg,
1891                    failure,
1892                    state.cfg,
1893                    state.cmd.connect_txn_sink,
1894                    state_change_ctx,
1895                    context,
1896                )))
1897            } else {
1898                Err(AfterRoamFailureState::Idle(state.to_idle(msg, failure, state_change_ctx)))
1899            }
1900        }
1901    }
1902}
1903
1904fn update_wmm_ac_param(ac_params: &mut ie::WmmAcParams, update: &fidl_internal::WmmAcParams) {
1905    ac_params.aci_aifsn.set_aifsn(update.aifsn);
1906    ac_params.aci_aifsn.set_acm(update.acm);
1907    ac_params.ecw_min_max.set_ecw_min(update.ecw_min);
1908    ac_params.ecw_min_max.set_ecw_max(update.ecw_max);
1909    ac_params.txop_limit = update.txop_limit;
1910}
1911
1912fn process_sae_updates(updates: UpdateSink, peer_sta_address: MacAddr, context: &mut Context) {
1913    for update in updates {
1914        match update {
1915            SecAssocUpdate::TxSaeFrame(frame) => {
1916                context.mlme_sink.send(MlmeRequest::SaeFrameTx(frame));
1917            }
1918            SecAssocUpdate::SaeAuthStatus(status) => context.mlme_sink.send(
1919                MlmeRequest::SaeHandshakeResp(fidl_mlme::SaeHandshakeResponse {
1920                    peer_sta_address: peer_sta_address.to_array(),
1921                    status_code: match status {
1922                        AuthStatus::Success => fidl_ieee80211::StatusCode::Success,
1923                        AuthStatus::Rejected(reason) => match reason {
1924                            AuthRejectedReason::TooManyRetries => {
1925                                fidl_ieee80211::StatusCode::RejectedSequenceTimeout
1926                            }
1927                            AuthRejectedReason::PmksaExpired | AuthRejectedReason::AuthFailed => {
1928                                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
1929                            }
1930                        },
1931                        AuthStatus::InternalError => {
1932                            fidl_ieee80211::StatusCode::RefusedReasonUnspecified
1933                        }
1934                    },
1935                }),
1936            ),
1937
1938            SecAssocUpdate::ScheduleSaeTimeout(id) => {
1939                // TODO(b/371613444): Plumb timer cancellation down to our SAE lib
1940                // so we don't drop here. Because the SAE library sends timer requests
1941                // into an event sink, we don't have a simple means to make the
1942                // timer cancelation call available.
1943                context.timer.schedule(event::SaeTimeout(id)).drop_without_cancel();
1944            }
1945            _ => (),
1946        }
1947    }
1948}
1949
1950fn process_sae_handshake_ind(
1951    protection: &mut Protection,
1952    ind: fidl_mlme::SaeHandshakeIndication,
1953    context: &mut Context,
1954) -> Result<(), anyhow::Error> {
1955    let supplicant = match protection {
1956        Protection::Rsna(rsna) => &mut rsna.supplicant,
1957        _ => bail!("Unexpected SAE handshake indication"),
1958    };
1959
1960    let mut updates = UpdateSink::default();
1961    supplicant.on_sae_handshake_ind(&mut updates)?;
1962    process_sae_updates(updates, MacAddr::from(ind.peer_sta_address), context);
1963    Ok(())
1964}
1965
1966fn process_sae_frame_rx(
1967    protection: &mut Protection,
1968    frame: fidl_mlme::SaeFrame,
1969    context: &mut Context,
1970) -> Result<(), anyhow::Error> {
1971    let peer_sta_address = MacAddr::from(frame.peer_sta_address);
1972    let supplicant = match protection {
1973        Protection::Rsna(rsna) => &mut rsna.supplicant,
1974        _ => bail!("Unexpected SAE frame received"),
1975    };
1976
1977    let mut updates = UpdateSink::default();
1978    supplicant.on_sae_frame_rx(&mut updates, frame)?;
1979    process_sae_updates(updates, peer_sta_address, context);
1980    Ok(())
1981}
1982
1983#[allow(clippy::single_match, reason = "mass allow for https://fxbug.dev/381896734")]
1984fn process_sae_timeout(
1985    protection: &mut Protection,
1986    bssid: Bssid,
1987    event: Event,
1988    context: &mut Context,
1989) -> Result<(), anyhow::Error> {
1990    match event {
1991        Event::SaeTimeout(timer) => {
1992            let supplicant = match protection {
1993                Protection::Rsna(rsna) => &mut rsna.supplicant,
1994                // Ignore timeouts if we're not using SAE.
1995                _ => return Ok(()),
1996            };
1997
1998            let mut updates = UpdateSink::default();
1999            supplicant.on_sae_timeout(&mut updates, timer.0)?;
2000            process_sae_updates(updates, MacAddr::from(bssid), context);
2001        }
2002        _ => (),
2003    }
2004    Ok(())
2005}
2006
2007fn log_state_change(
2008    start_state: &str,
2009    new_state: &ClientState,
2010    state_change_ctx: Option<StateChangeContext>,
2011    context: &mut Context,
2012) {
2013    if start_state == new_state.state_name() && state_change_ctx.is_none() {
2014        return;
2015    }
2016
2017    match state_change_ctx {
2018        Some(inner) => match inner {
2019            StateChangeContext::Disconnect { msg, disconnect_source } => {
2020                // Only log the disconnect source if an operation had an effect of moving from
2021                // non-idle state to idle state.
2022                if start_state != IDLE_STATE {
2023                    info!(
2024                        "{} => {}, ctx: `{}`, disconnect_source: {:?}",
2025                        start_state,
2026                        new_state.state_name(),
2027                        msg,
2028                        disconnect_source,
2029                    );
2030                }
2031
2032                inspect_log!(context.inspect.state_events.lock(), {
2033                    from: start_state,
2034                    to: new_state.state_name(),
2035                    ctx: msg,
2036                });
2037            }
2038            StateChangeContext::Connect { msg, bssid, ssid } => {
2039                inspect_log!(context.inspect.state_events.lock(), {
2040                    from: start_state,
2041                    to: new_state.state_name(),
2042                    ctx: msg,
2043                    bssid: bssid.to_string(),
2044                    ssid: ssid.to_string(),
2045                });
2046            }
2047            StateChangeContext::Roam { msg, bssid } => {
2048                inspect_log!(context.inspect.state_events.lock(), {
2049                    from: start_state,
2050                    to: new_state.state_name(),
2051                    ctx: msg,
2052                    bssid: bssid.to_string(),
2053                });
2054            }
2055            StateChangeContext::Msg(msg) => {
2056                inspect_log!(context.inspect.state_events.lock(), {
2057                    from: start_state,
2058                    to: new_state.state_name(),
2059                    ctx: msg,
2060                });
2061            }
2062        },
2063        None => {
2064            inspect_log!(context.inspect.state_events.lock(), {
2065                from: start_state,
2066                to: new_state.state_name(),
2067            });
2068        }
2069    }
2070}
2071
2072fn build_wep_set_key_descriptor(bssid: Bssid, key: &WepKey) -> fidl_mlme::SetKeyDescriptor {
2073    let cipher_suite = match key {
2074        WepKey::Wep40(_) => cipher::WEP_40,
2075        WepKey::Wep104(_) => cipher::WEP_104,
2076    };
2077    fidl_mlme::SetKeyDescriptor {
2078        key_type: fidl_mlme::KeyType::Pairwise,
2079        key: key.clone().into(),
2080        key_id: 0,
2081        address: bssid.to_array(),
2082        cipher_suite_oui: OUI.into(),
2083        cipher_suite_type: fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(
2084            cipher_suite.into(),
2085        ),
2086        rsc: 0,
2087    }
2088}
2089
2090/// Custom logging for ConnectCommand because its normal full debug string is too large, and we
2091/// want to reduce how much we log in memory for Inspect. Additionally, in the future, we'd need
2092/// to anonymize information like BSSID and SSID.
2093fn connect_cmd_inspect_summary(cmd: &ConnectCommand) -> String {
2094    let bss = &cmd.bss;
2095    format!(
2096        "ConnectCmd {{ \
2097         capability_info: {capability_info:?}, rates: {rates:?}, \
2098         protected: {protected:?}, channel: {channel}, \
2099         rssi: {rssi:?}, ht_cap: {ht_cap:?}, ht_op: {ht_op:?}, \
2100         vht_cap: {vht_cap:?}, vht_op: {vht_op:?} }}",
2101        capability_info = bss.capability_info,
2102        rates = bss.rates(),
2103        protected = bss.rsne().is_some(),
2104        channel = bss.channel,
2105        rssi = bss.rssi_dbm,
2106        ht_cap = bss.ht_cap().is_some(),
2107        ht_op = bss.ht_op().is_some(),
2108        vht_cap = bss.vht_cap().is_some(),
2109        vht_op = bss.vht_op().is_some()
2110    )
2111}
2112
2113fn send_deauthenticate_request(bssid: &Bssid, mlme_sink: &MlmeSink) {
2114    mlme_sink.send(MlmeRequest::Deauthenticate(fidl_mlme::DeauthenticateRequest {
2115        peer_sta_address: bssid.to_array(),
2116        reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
2117    }));
2118}
2119
2120// Returns a DisconnectInfo for given roam failure parameters. reason_code defaults to UnspecifiedReason.
2121fn make_roam_disconnect_info(
2122    mlme_event_name: fidl_sme::DisconnectMlmeEventName,
2123    reason_code: Option<fidl_ieee80211::ReasonCode>,
2124) -> fidl_sme::DisconnectInfo {
2125    let reason_code = match reason_code {
2126        Some(reason_code) => reason_code,
2127        None => fidl_ieee80211::ReasonCode::UnspecifiedReason,
2128    };
2129    let disconnect_reason = fidl_sme::DisconnectCause { mlme_event_name, reason_code };
2130    let disconnect_source = fidl_sme::DisconnectSource::Mlme(disconnect_reason);
2131    fidl_sme::DisconnectInfo { is_sme_reconnecting: false, disconnect_source }
2132}
2133
2134fn now() -> zx::MonotonicInstant {
2135    zx::MonotonicInstant::get()
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140    use super::*;
2141    use anyhow::format_err;
2142    use assert_matches::assert_matches;
2143    use diagnostics_assertions::{
2144        AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
2145    };
2146    use fidl_fuchsia_wlan_common_security::{Credentials, Protocol};
2147    use fuchsia_async::DurationExt;
2148    use fuchsia_inspect::Inspector;
2149    use futures::channel::mpsc;
2150    use futures::{Stream, StreamExt};
2151    use link_state::{EstablishingRsna, LinkUp};
2152    use std::sync::Arc;
2153    use std::task::Poll;
2154    use wlan_common::bss::Protection as BssProtection;
2155    use wlan_common::channel::{Cbw, Channel};
2156    use wlan_common::ie::fake_ies::{fake_probe_resp_wsc_ie_bytes, get_vendor_ie_bytes_for_wsc_ie};
2157    use wlan_common::ie::rsn::rsne::Rsne;
2158    use wlan_common::test_utils::fake_features::{
2159        fake_security_support, fake_spectrum_management_support_empty,
2160    };
2161    use wlan_common::test_utils::fake_stas::IesOverrides;
2162    use wlan_common::{fake_bss_description, timer};
2163    use wlan_rsn::NegotiatedProtection;
2164    use wlan_rsn::key::exchange::Key;
2165    use wlan_rsn::rsna::SecAssocStatus;
2166    use {
2167        fidl_fuchsia_wlan_common as fidl_common,
2168        fidl_fuchsia_wlan_common_security as fidl_security, fidl_internal,
2169    };
2170
2171    use crate::MlmeStream;
2172    use crate::client::event::RsnaCompletionTimeout;
2173    use crate::client::rsn::Rsna;
2174    use crate::client::test_utils::{
2175        MockSupplicant, MockSupplicantController, create_connect_conf, create_on_wmm_status_resp,
2176        expect_stream_empty, fake_wmm_param, mock_psk_supplicant,
2177    };
2178    use crate::client::{ConnectTransactionStream, RoamFailureType, inspect};
2179    use crate::test_utils::{self, make_wpa1_ie};
2180
2181    #[test]
2182    fn connect_happy_path_unprotected() {
2183        let mut h = TestHelper::new();
2184        let state = idle_state();
2185        let (command, mut connect_txn_stream) = connect_command_one();
2186        let bss = (*command.bss).clone();
2187
2188        // Issue a "connect" command
2189        let state = state.connect(command, &mut h.context);
2190
2191        // (sme->mlme) Expect a ConnectRequest
2192        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(req))) => {
2193            assert_eq!(req, fidl_mlme::ConnectRequest {
2194                selected_bss: bss.clone().into(),
2195                connect_failure_timeout: DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT,
2196                auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
2197                sae_password: vec![],
2198                wep_key: None,
2199                security_ie: vec![],
2200            });
2201        });
2202
2203        // (mlme->sme) Send a ConnectConf as a response
2204        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2205        let _state = state.on_mlme_event(connect_conf, &mut h.context);
2206
2207        // User should be notified that we are connected
2208        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2209            assert_eq!(result, ConnectResult::Success);
2210        });
2211
2212        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2213            usme: contains {
2214                state_events: {
2215                    "0": {
2216                        "@time": AnyNumericProperty,
2217                        ctx: AnyStringProperty,
2218                        from: IDLE_STATE,
2219                        to: CONNECTING_STATE,
2220                        bssid: bss.bssid.to_string(),
2221                        ssid: bss.ssid.to_string(),
2222                    },
2223                    "1": {
2224                        "@time": AnyNumericProperty,
2225                        ctx: AnyStringProperty,
2226                        from: CONNECTING_STATE,
2227                        to: LINK_UP_STATE,
2228                    },
2229                },
2230            },
2231        });
2232    }
2233
2234    #[test]
2235    fn connect_happy_path_protected() {
2236        let mut h = TestHelper::new();
2237        let (supplicant, suppl_mock) = mock_psk_supplicant();
2238
2239        let state = idle_state();
2240        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2241        let bss = (*command.bss).clone();
2242
2243        // Issue a "connect" command
2244        let state = state.connect(command, &mut h.context);
2245
2246        // (sme->mlme) Expect a ConnectRequest
2247        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(req))) => {
2248            assert_eq!(req, fidl_mlme::ConnectRequest {
2249                selected_bss: bss.clone().into(),
2250                connect_failure_timeout: DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT,
2251                auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
2252                sae_password: vec![],
2253                wep_key: None,
2254                security_ie: vec![
2255                    0x30, 18, // Element header
2256                    1, 0, // Version
2257                    0x00, 0x0F, 0xAC, 4, // Group Cipher: CCMP-128
2258                    1, 0, 0x00, 0x0F, 0xAC, 4, // 1 Pairwise Cipher: CCMP-128
2259                    1, 0, 0x00, 0x0F, 0xAC, 2, // 1 AKM: PSK
2260                ],
2261            });
2262        });
2263
2264        // (mlme->sme) Send a ConnectConf as a response
2265        suppl_mock
2266            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
2267        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2268        let state = state.on_mlme_event(connect_conf, &mut h.context);
2269        assert!(suppl_mock.is_supplicant_started());
2270
2271        // (mlme->sme) Send an EapolInd, mock supplicant with key frame
2272        let update = SecAssocUpdate::TxEapolKeyFrame {
2273            frame: test_utils::eapol_key_frame(),
2274            expect_response: true,
2275        };
2276        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2277
2278        expect_eapol_req(&mut h.mlme_stream, bss.bssid);
2279
2280        // (mlme->sme) Send an EapolInd, mock supplicant with keys
2281        let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::ptk()));
2282        let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk()));
2283        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![ptk, gtk]);
2284
2285        expect_set_ptk(&mut h.mlme_stream, bss.bssid);
2286        expect_set_gtk(&mut h.mlme_stream);
2287
2288        let state = on_set_keys_conf(state, &mut h, vec![0, 2]);
2289
2290        // (mlme->sme) Send an EapolInd, mock supplicant with completion status
2291        let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
2292        let _state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2293
2294        expect_set_ctrl_port(&mut h.mlme_stream, bss.bssid, fidl_mlme::ControlledPortState::Open);
2295        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2296            assert_eq!(result, ConnectResult::Success);
2297        });
2298
2299        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2300            usme: contains {
2301                state_events: {
2302                    "0": {
2303                        "@time": AnyNumericProperty,
2304                        ctx: AnyStringProperty,
2305                        from: IDLE_STATE,
2306                        to: CONNECTING_STATE,
2307                        bssid: bss.bssid.to_string(),
2308                        ssid: bss.ssid.to_string(),
2309                    },
2310                    "1": {
2311                        "@time": AnyNumericProperty,
2312                        ctx: AnyStringProperty,
2313                        from: CONNECTING_STATE,
2314                        to: RSNA_STATE,
2315                    },
2316                    "2": {
2317                        "@time": AnyNumericProperty,
2318                        ctx: AnyStringProperty,
2319                        from: RSNA_STATE,
2320                        to: LINK_UP_STATE,
2321                    },
2322                },
2323            },
2324        });
2325    }
2326
2327    #[test]
2328    fn connect_happy_path_wpa1() {
2329        let mut h = TestHelper::new();
2330        let (supplicant, suppl_mock) = mock_psk_supplicant();
2331
2332        let state = idle_state();
2333        let (command, mut connect_txn_stream) = connect_command_wpa1(supplicant);
2334        let bss = (*command.bss).clone();
2335
2336        // Issue a "connect" command
2337        let state = state.connect(command, &mut h.context);
2338
2339        // (sme->mlme) Expect a ConnectRequest
2340        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(req))) => {
2341            assert_eq!(req, fidl_mlme::ConnectRequest {
2342                selected_bss: bss.clone().into(),
2343                connect_failure_timeout: DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT,
2344                auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
2345                sae_password: vec![],
2346                wep_key: None,
2347                security_ie: vec![
2348                    0xdd, 0x16, 0x00, 0x50, 0xf2, // IE header
2349                    0x01, // MSFT specific IE type (WPA)
2350                    0x01, 0x00, // WPA version
2351                    0x00, 0x50, 0xf2, 0x02, // multicast cipher: TKIP
2352                    0x01, 0x00, 0x00, 0x50, 0xf2, 0x02, // 1 unicast cipher
2353                    0x01, 0x00, 0x00, 0x50, 0xf2, 0x02, // 1 AKM: PSK
2354                ],
2355            });
2356        });
2357
2358        // (mlme->sme) Send a ConnectConf as a response
2359        suppl_mock
2360            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
2361        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2362        let state = state.on_mlme_event(connect_conf, &mut h.context);
2363        assert!(suppl_mock.is_supplicant_started());
2364
2365        // (mlme->sme) Send an EapolInd, mock supplicant with key frame
2366        let update = SecAssocUpdate::TxEapolKeyFrame {
2367            frame: test_utils::eapol_key_frame(),
2368            expect_response: false,
2369        };
2370        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2371
2372        expect_eapol_req(&mut h.mlme_stream, bss.bssid);
2373
2374        // (mlme->sme) Send an EapolInd, mock supplicant with keys
2375        let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::wpa1_ptk()));
2376        let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::wpa1_gtk()));
2377        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![ptk, gtk]);
2378
2379        expect_set_wpa1_ptk(&mut h.mlme_stream, bss.bssid);
2380        expect_set_wpa1_gtk(&mut h.mlme_stream);
2381
2382        let state = on_set_keys_conf(state, &mut h, vec![0, 2]);
2383
2384        // (mlme->sme) Send an EapolInd, mock supplicant with completion status
2385        let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
2386        let _state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2387
2388        expect_set_ctrl_port(&mut h.mlme_stream, bss.bssid, fidl_mlme::ControlledPortState::Open);
2389        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2390            assert_eq!(result, ConnectResult::Success);
2391        });
2392
2393        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2394            usme: contains {
2395                state_events: {
2396                    "0": {
2397                        "@time": AnyNumericProperty,
2398                        ctx: AnyStringProperty,
2399                        from: IDLE_STATE,
2400                        to: CONNECTING_STATE,
2401                        bssid: bss.bssid.to_string(),
2402                        ssid: bss.ssid.to_string(),
2403                    },
2404                    "1": {
2405                        "@time": AnyNumericProperty,
2406                        ctx: AnyStringProperty,
2407                        from: CONNECTING_STATE,
2408                        to: RSNA_STATE,
2409                    },
2410                    "2": {
2411                        "@time": AnyNumericProperty,
2412                        ctx: AnyStringProperty,
2413                        from: RSNA_STATE,
2414                        to: LINK_UP_STATE,
2415                    },
2416                },
2417            },
2418        });
2419    }
2420
2421    #[test]
2422    fn connect_happy_path_wep() {
2423        let mut h = TestHelper::new();
2424
2425        let state = idle_state();
2426        let (command, mut connect_txn_stream) = connect_command_wep();
2427        let bss = (*command.bss).clone();
2428
2429        // Issue a "connect" command
2430        let state = state.connect(command, &mut h.context);
2431
2432        // (sme->mlme) Expect a ConnectRequest
2433        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(req))) => {
2434            assert_eq!(req, fidl_mlme::ConnectRequest {
2435                selected_bss: bss.clone().into(),
2436                connect_failure_timeout: DEFAULT_JOIN_AUTH_ASSOC_FAILURE_TIMEOUT,
2437                auth_type: fidl_mlme::AuthenticationTypes::SharedKey,
2438                sae_password: vec![],
2439                wep_key: Some(Box::new(fidl_mlme::SetKeyDescriptor {
2440                    key_type: fidl_mlme::KeyType::Pairwise,
2441                    key: vec![3; 5],
2442                    key_id: 0,
2443                    address: bss.bssid.to_array(),
2444                    cipher_suite_oui: OUI.into(),
2445                    cipher_suite_type: fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(1),
2446                    rsc: 0,
2447                })),
2448                security_ie: vec![],
2449            });
2450        });
2451
2452        // (mlme->sme) Send a ConnectConf as a response
2453        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2454        let _state = state.on_mlme_event(connect_conf, &mut h.context);
2455
2456        // User should be notified that we are connected
2457        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2458            assert_eq!(result, ConnectResult::Success);
2459        });
2460
2461        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2462            usme: contains {
2463                state_events: {
2464                    "0": {
2465                        "@time": AnyNumericProperty,
2466                        ctx: AnyStringProperty,
2467                        from: IDLE_STATE,
2468                        to: CONNECTING_STATE,
2469                        bssid: bss.bssid.to_string(),
2470                        ssid: bss.ssid.to_string(),
2471                    },
2472                    "1": {
2473                        "@time": AnyNumericProperty,
2474                        ctx: AnyStringProperty,
2475                        from: CONNECTING_STATE,
2476                        to: LINK_UP_STATE,
2477                    },
2478                },
2479            },
2480        });
2481    }
2482
2483    #[test]
2484    fn connect_happy_path_wmm() {
2485        let mut h = TestHelper::new();
2486        let state = idle_state();
2487        let (command, mut connect_txn_stream) = connect_command_one();
2488        let bss = (*command.bss).clone();
2489
2490        // Issue a "connect" command
2491        let state = state.connect(command, &mut h.context);
2492
2493        // (sme->mlme) Expect a ConnectRequest
2494        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(_req))));
2495
2496        // (mlme->sme) Send a ConnectConf as a response
2497        let connect_conf = fidl_mlme::MlmeEvent::ConnectConf {
2498            resp: fidl_mlme::ConnectConfirm {
2499                peer_sta_address: bss.bssid.to_array(),
2500                result_code: fidl_ieee80211::StatusCode::Success,
2501                association_id: 42,
2502                association_ies: vec![
2503                    0xdd, 0x18, // Vendor IE header
2504                    0x00, 0x50, 0xf2, 0x02, 0x01, 0x01, // WMM Param header
2505                    0x80, // Qos Info - U-ASPD enabled
2506                    0x00, // reserved
2507                    0x03, 0xa4, 0x00, 0x00, // Best effort AC params
2508                    0x27, 0xa4, 0x00, 0x00, // Background AC params
2509                    0x42, 0x43, 0x5e, 0x00, // Video AC params
2510                    0x62, 0x32, 0x2f, 0x00, // Voice AC params
2511                ],
2512            },
2513        };
2514        let _state = state.on_mlme_event(connect_conf, &mut h.context);
2515
2516        // User should be notified that we are connected
2517        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2518            assert_eq!(result, ConnectResult::Success);
2519        });
2520
2521        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2522            usme: contains {
2523                state_events: {
2524                    "0": {
2525                        "@time": AnyNumericProperty,
2526                        ctx: AnyStringProperty,
2527                        from: IDLE_STATE,
2528                        to: CONNECTING_STATE,
2529                        bssid: bss.bssid.to_string(),
2530                        ssid: bss.ssid.to_string(),
2531                    },
2532                    "1": {
2533                        "@time": AnyNumericProperty,
2534                        ctx: AnyStringProperty,
2535                        from: CONNECTING_STATE,
2536                        to: LINK_UP_STATE,
2537                    },
2538                },
2539            },
2540        });
2541    }
2542
2543    #[test]
2544    fn set_keys_failure() {
2545        let mut h = TestHelper::new();
2546        let (supplicant, suppl_mock) = mock_psk_supplicant();
2547
2548        let state = idle_state();
2549        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2550        let bss = (*command.bss).clone();
2551
2552        // Issue a "connect" command
2553        let state = state.connect(command, &mut h.context);
2554
2555        // (sme->mlme) Expect a ConnectRequest
2556        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(_req))));
2557
2558        // (mlme->sme) Send a ConnectConf as a response
2559        suppl_mock
2560            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
2561        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2562        let state = state.on_mlme_event(connect_conf, &mut h.context);
2563        assert!(suppl_mock.is_supplicant_started());
2564
2565        // (mlme->sme) Send an EapolInd, mock supplicant with key frame
2566        let update = SecAssocUpdate::TxEapolKeyFrame {
2567            frame: test_utils::eapol_key_frame(),
2568            expect_response: false,
2569        };
2570        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2571
2572        expect_eapol_req(&mut h.mlme_stream, bss.bssid);
2573
2574        // (mlme->sme) Send an EapolInd, mock supplicant with keys
2575        let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::ptk()));
2576        let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk()));
2577        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![ptk, gtk]);
2578
2579        expect_set_ptk(&mut h.mlme_stream, bss.bssid);
2580        expect_set_gtk(&mut h.mlme_stream);
2581
2582        // (mlme->sme) Send an EapolInd, mock supplicant with completion status
2583        let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
2584        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2585
2586        // No update until all key confs are received.
2587        assert!(connect_txn_stream.try_next().is_err());
2588
2589        // One key fails to set
2590        let state = state.on_mlme_event(
2591            MlmeEvent::SetKeysConf {
2592                conf: fidl_mlme::SetKeysConfirm {
2593                    results: vec![
2594                        fidl_mlme::SetKeyResult { key_id: 0, status: zx::Status::OK.into_raw() },
2595                        fidl_mlme::SetKeyResult {
2596                            key_id: 2,
2597                            status: zx::Status::INTERNAL.into_raw(),
2598                        },
2599                    ],
2600                },
2601            },
2602            &mut h.context,
2603        );
2604
2605        assert_matches!(connect_txn_stream.try_next(),
2606        Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2607            assert_matches!(result, ConnectResult::Failed(_))
2608        });
2609
2610        let state = exchange_deauth(state, &mut h);
2611        assert_idle(state);
2612
2613        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2614            usme: contains {
2615                state_events: {
2616                    "0": {
2617                        "@time": AnyNumericProperty,
2618                        ctx: AnyStringProperty,
2619                        from: IDLE_STATE,
2620                        to: CONNECTING_STATE,
2621                        bssid: bss.bssid.to_string(),
2622                        ssid: bss.ssid.to_string(),
2623                    },
2624                    "1": {
2625                        "@time": AnyNumericProperty,
2626                        ctx: AnyStringProperty,
2627                        from: CONNECTING_STATE,
2628                        to: RSNA_STATE,
2629                    },
2630                    "2": {
2631                        "@time": AnyNumericProperty,
2632                        ctx: AnyStringProperty,
2633                        from: RSNA_STATE,
2634                        to: DISCONNECTING_STATE,
2635                    },
2636                    "3": {
2637                        "@time": AnyNumericProperty,
2638                        from: DISCONNECTING_STATE,
2639                        to: IDLE_STATE,
2640                    },
2641                },
2642            },
2643        });
2644    }
2645
2646    #[test]
2647    fn deauth_while_connecting() {
2648        let mut h = TestHelper::new();
2649        let (cmd_one, mut connect_txn_stream1) = connect_command_one();
2650        let bss = cmd_one.bss.clone();
2651        let bss_protection = bss.protection();
2652        let state = connecting_state(cmd_one);
2653        let deauth_ind = MlmeEvent::DeauthenticateInd {
2654            ind: fidl_mlme::DeauthenticateIndication {
2655                peer_sta_address: [7, 7, 7, 7, 7, 7],
2656                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
2657                locally_initiated: false,
2658            },
2659        };
2660        let state = state.on_mlme_event(deauth_ind, &mut h.context);
2661        assert_matches!(connect_txn_stream1.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2662            assert_eq!(result, AssociationFailure {
2663                bss_protection,
2664                code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
2665            }
2666            .into());
2667        });
2668        assert_idle(state);
2669
2670        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2671            usme: contains {
2672                state_events: {
2673                    "0": {
2674                        "@time": AnyNumericProperty,
2675                        ctx: AnyStringProperty,
2676                        from: CONNECTING_STATE,
2677                        to: IDLE_STATE,
2678                    },
2679                },
2680            },
2681        });
2682    }
2683
2684    #[test]
2685    fn disassoc_while_connecting() {
2686        let mut h = TestHelper::new();
2687        let (cmd_one, mut connect_txn_stream1) = connect_command_one();
2688        let bss = cmd_one.bss.clone();
2689        let bss_protection = bss.protection();
2690        let state = connecting_state(cmd_one);
2691        let disassoc_ind = MlmeEvent::DisassociateInd {
2692            ind: fidl_mlme::DisassociateIndication {
2693                peer_sta_address: [7, 7, 7, 7, 7, 7],
2694                reason_code: fidl_ieee80211::ReasonCode::PeerkeyMismatch,
2695                locally_initiated: false,
2696            },
2697        };
2698        let state = state.on_mlme_event(disassoc_ind, &mut h.context);
2699        let state = exchange_deauth(state, &mut h);
2700        assert_matches!(connect_txn_stream1.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2701            assert_eq!(result, AssociationFailure {
2702                bss_protection,
2703                code: fidl_ieee80211::StatusCode::SpuriousDeauthOrDisassoc,
2704            }
2705            .into());
2706        });
2707        assert_idle(state);
2708
2709        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2710            usme: contains {
2711                state_events: {
2712                    "0": {
2713                        "@time": AnyNumericProperty,
2714                        ctx: AnyStringProperty,
2715                        from: CONNECTING_STATE,
2716                        to: DISCONNECTING_STATE,
2717                    },
2718                    "1": {
2719                        "@time": AnyNumericProperty,
2720                        from: DISCONNECTING_STATE,
2721                        to: IDLE_STATE,
2722                    },
2723                },
2724            },
2725        });
2726    }
2727
2728    #[test]
2729    fn supplicant_fails_to_start_while_connecting() {
2730        let mut h = TestHelper::new();
2731        let (supplicant, suppl_mock) = mock_psk_supplicant();
2732        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2733        let bss = command.bss.clone();
2734        let state = connecting_state(command);
2735
2736        suppl_mock.set_start_failure(format_err!("failed to start supplicant"));
2737
2738        // (mlme->sme) Send a ConnectConf
2739        let assoc_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2740        let state = state.on_mlme_event(assoc_conf, &mut h.context);
2741
2742        let state = exchange_deauth(state, &mut h);
2743        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2744            assert_eq!(result, EstablishRsnaFailure {
2745                auth_method: Some(auth::MethodName::Psk),
2746                reason: EstablishRsnaFailureReason::StartSupplicantFailed,
2747            }
2748            .into());
2749        });
2750        assert_idle(state);
2751
2752        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2753            usme: contains {
2754                state_events: {
2755                    "0": {
2756                        "@time": AnyNumericProperty,
2757                        ctx: AnyStringProperty,
2758                        from: CONNECTING_STATE,
2759                        to: DISCONNECTING_STATE,
2760                    },
2761                    "1": {
2762                        "@time": AnyNumericProperty,
2763                        from: DISCONNECTING_STATE,
2764                        to: IDLE_STATE,
2765                    },
2766                },
2767            },
2768        });
2769    }
2770
2771    #[test]
2772    fn bad_eapol_frame_while_establishing_rsna() {
2773        let mut h = TestHelper::new();
2774        let (supplicant, suppl_mock) = mock_psk_supplicant();
2775        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2776        let bss = command.bss.clone();
2777        let state = establishing_rsna_state(command);
2778
2779        // doesn't matter what we mock here
2780        let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
2781        suppl_mock.set_on_eapol_frame_updates(vec![update]);
2782
2783        // (mlme->sme) Send an EapolInd with bad eapol data
2784        let eapol_ind = create_eapol_ind(bss.bssid, vec![1, 2, 3, 4]);
2785        let s = state.on_mlme_event(eapol_ind, &mut h.context);
2786
2787        // There should be no message in the connect_txn_stream
2788        assert_matches!(connect_txn_stream.try_next(), Err(_));
2789        assert_matches!(s, ClientState::Associated(state) => {
2790            assert_matches!(&state.link_state, LinkState::EstablishingRsna { .. })});
2791
2792        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
2793
2794        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2795            usme: contains {
2796                state_events: {},
2797                rsn_events: {
2798                    "0": {
2799                        "@time": AnyNumericProperty,
2800                        rx_eapol_frame: AnyBytesProperty,
2801                        status: AnyStringProperty,
2802                    }
2803                },
2804            },
2805        });
2806    }
2807
2808    #[test]
2809    fn supplicant_fails_to_process_eapol_while_establishing_rsna() {
2810        let mut h = TestHelper::new();
2811        let (supplicant, suppl_mock) = mock_psk_supplicant();
2812        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2813        let bss = command.bss.clone();
2814        let state = establishing_rsna_state(command);
2815
2816        suppl_mock.set_on_eapol_frame_failure(format_err!("supplicant::on_eapol_frame fails"));
2817
2818        // (mlme->sme) Send an EapolInd
2819        let eapol_ind = create_eapol_ind(bss.bssid, test_utils::eapol_key_frame().into());
2820        let s = state.on_mlme_event(eapol_ind, &mut h.context);
2821
2822        // There should be no message in the connect_txn_stream
2823        assert_matches!(connect_txn_stream.try_next(), Err(_));
2824        assert_matches!(s, ClientState::Associated(state) => {
2825            assert_matches!(&state.link_state, LinkState::EstablishingRsna { .. })});
2826
2827        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
2828
2829        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2830            usme: contains {
2831                state_events: {},
2832                rsn_events: {
2833                    "0": {
2834                        "@time": AnyNumericProperty,
2835                        rx_eapol_frame: AnyBytesProperty,
2836                        status: AnyStringProperty,
2837                    }
2838                },
2839            },
2840        });
2841    }
2842
2843    #[test]
2844    fn reject_foreign_eapol_frames() {
2845        let mut h = TestHelper::new();
2846        let (supplicant, mock) = mock_psk_supplicant();
2847        let (cmd, _connect_txn_stream) = connect_command_wpa2(supplicant);
2848        let bss = cmd.bss.clone();
2849        let state = link_up_state(cmd);
2850        mock.set_on_eapol_frame_callback(|| {
2851            panic!("eapol frame should not have been processed");
2852        });
2853
2854        // Send an EapolInd from foreign BSS.
2855        let foreign_bssid = Bssid::from([1; 6]);
2856        let eapol_ind = create_eapol_ind(foreign_bssid, test_utils::eapol_key_frame().into());
2857        let state = state.on_mlme_event(eapol_ind, &mut h.context);
2858
2859        // Verify state did not change.
2860        assert_matches!(state, ClientState::Associated(state) => {
2861            assert_matches!(
2862                &state.link_state,
2863                LinkState::LinkUp(state) => assert_matches!(&state.protection, Protection::Rsna(_))
2864            )
2865        });
2866
2867        assert_data_tree!(@executor h.executor, h.inspector, root: {
2868            usme: contains {
2869                state_events: {},
2870                rsn_events:  {
2871                    "0" : {
2872                        "@time": AnyNumericProperty,
2873                        rx_eapol_frame: AnyBytesProperty,
2874                        foreign_bssid: foreign_bssid.to_string(),
2875                        current_bssid: bss.bssid.to_string(),
2876                        status: "rejected (foreign BSS)"
2877                    }
2878                }
2879            }
2880        });
2881    }
2882
2883    #[test]
2884    fn wrong_password_while_establishing_rsna() {
2885        let mut h = TestHelper::new();
2886        let (supplicant, suppl_mock) = mock_psk_supplicant();
2887        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2888        let bss = command.bss.clone();
2889        let state = establishing_rsna_state(command);
2890
2891        // (mlme->sme) Send an EapolInd, mock supplicant with wrong password status
2892        let update = SecAssocUpdate::Status(SecAssocStatus::WrongPassword);
2893        let state = on_eapol_ind(state, &mut h, bss.bssid, &suppl_mock, vec![update]);
2894
2895        expect_deauth_req(&mut h.mlme_stream, bss.bssid, fidl_ieee80211::ReasonCode::StaLeaving);
2896        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2897            assert_eq!(result, EstablishRsnaFailure {
2898                auth_method: Some(auth::MethodName::Psk),
2899                reason: EstablishRsnaFailureReason::InternalError,
2900            }
2901            .into());
2902        });
2903
2904        // (mlme->sme) Send a DeauthenticateConf as a response
2905        let deauth_conf = MlmeEvent::DeauthenticateConf {
2906            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: bss.bssid.to_array() },
2907        };
2908        let state = state.on_mlme_event(deauth_conf, &mut h.context);
2909        assert_idle(state);
2910
2911        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
2912            usme: contains {
2913                state_events: {
2914                    "0": {
2915                        "@time": AnyNumericProperty,
2916                        ctx: AnyStringProperty,
2917                        from: RSNA_STATE,
2918                        to: DISCONNECTING_STATE,
2919                    },
2920                    "1": {
2921                        "@time": AnyNumericProperty,
2922                        from: DISCONNECTING_STATE,
2923                        to: IDLE_STATE,
2924                    },
2925                },
2926            },
2927        });
2928    }
2929
2930    fn expect_next_event_at_deadline<E: std::fmt::Debug>(
2931        executor: &mut fuchsia_async::TestExecutor,
2932        mut timed_event_stream: impl Stream<Item = timer::Event<E>> + std::marker::Unpin,
2933        deadline: fuchsia_async::MonotonicInstant,
2934    ) -> timer::Event<E> {
2935        assert_matches!(executor.run_until_stalled(&mut timed_event_stream.next()), Poll::Pending);
2936        loop {
2937            let next_deadline = executor.wake_next_timer().expect("expected pending timer");
2938            executor.set_fake_time(next_deadline);
2939            if next_deadline == deadline {
2940                return assert_matches!(
2941                            executor.run_until_stalled(&mut timed_event_stream.next()),
2942                            Poll::Ready(Some(timed_event)) => timed_event
2943                );
2944            } else {
2945                // Assert that the timer has been cancelled, since we haven't yet
2946                // reached the expected deadline.
2947                assert_matches!(
2948                    executor.run_until_stalled(&mut timed_event_stream.next()),
2949                    Poll::Pending
2950                );
2951            }
2952        }
2953    }
2954
2955    #[test]
2956    fn simple_rsna_response_timeout_with_unresponsive_ap() {
2957        let mut h = TestHelper::new_with_fake_time();
2958        h.executor.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
2959        let (supplicant, suppl_mock) = mock_psk_supplicant();
2960        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
2961        let bss = command.bss.clone();
2962
2963        // Start in an "Connecting" state
2964        let state = ClientState::from(testing::new_state(Connecting {
2965            cfg: ClientConfig::default(),
2966            cmd: command,
2967            protection_ie: None,
2968            reassociation_loop_count: 0,
2969        }));
2970        suppl_mock
2971            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
2972        let assoc_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
2973        let rsna_response_deadline =
2974            zx::MonotonicDuration::from_millis(event::RSNA_RESPONSE_TIMEOUT_MILLIS).after_now();
2975        let state = state.on_mlme_event(assoc_conf, &mut h.context);
2976        assert!(suppl_mock.is_supplicant_started());
2977
2978        // Advance to the response timeout and setup a failure reason
2979        let mut timed_event_stream = timer::make_async_timed_event_stream(h.time_stream);
2980        let timed_event = expect_next_event_at_deadline(
2981            &mut h.executor,
2982            &mut timed_event_stream,
2983            rsna_response_deadline,
2984        );
2985        assert_matches!(timed_event.event, Event::RsnaResponseTimeout(..));
2986        suppl_mock.set_on_rsna_response_timeout(EstablishRsnaFailureReason::RsnaResponseTimeout(
2987            wlan_rsn::Error::EapolHandshakeNotStarted,
2988        ));
2989        let state = state.handle_timeout(timed_event.event, &mut h.context);
2990
2991        // Check that SME sends a deauthenticate request and fails the connection
2992        expect_deauth_req(&mut h.mlme_stream, bss.bssid, fidl_ieee80211::ReasonCode::StaLeaving);
2993        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
2994            assert_eq!(result, EstablishRsnaFailure {
2995                auth_method: Some(auth::MethodName::Psk),
2996                reason: EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::EapolHandshakeNotStarted),
2997            }.into());
2998        });
2999
3000        // (mlme->sme) Send a DeauthenticateConf as a response
3001        let deauth_conf = MlmeEvent::DeauthenticateConf {
3002            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: bss.bssid.to_array() },
3003        };
3004        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3005        assert_idle(state);
3006
3007        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
3008            usme: contains {
3009                state_events: {
3010                    "0": {
3011                        "@time": AnyNumericProperty,
3012                        ctx: AnyStringProperty,
3013                        from: CONNECTING_STATE,
3014                        to: RSNA_STATE,
3015                    },
3016                    "1": {
3017                        "@time": AnyNumericProperty,
3018                        ctx: AnyStringProperty,
3019                        from: RSNA_STATE,
3020                        to: DISCONNECTING_STATE,
3021                    },
3022                    "2": {
3023                        "@time": AnyNumericProperty,
3024                        from: DISCONNECTING_STATE,
3025                        to: IDLE_STATE,
3026                    },
3027                },
3028            },
3029        });
3030    }
3031
3032    #[test]
3033    fn simple_retransmission_timeout_with_responsive_ap() {
3034        let mut h = TestHelper::new_with_fake_time();
3035        h.executor.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
3036
3037        let (supplicant, suppl_mock) = mock_psk_supplicant();
3038        let (command, _connect_txn_stream) = connect_command_wpa2(supplicant);
3039        let bssid = command.bss.bssid;
3040
3041        // Start in an "Connecting" state
3042        let state = ClientState::from(testing::new_state(Connecting {
3043            cfg: ClientConfig::default(),
3044            cmd: command,
3045            protection_ie: None,
3046            reassociation_loop_count: 0,
3047        }));
3048        suppl_mock
3049            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
3050        let assoc_conf = create_connect_conf(bssid, fidl_ieee80211::StatusCode::Success);
3051        let state = state.on_mlme_event(assoc_conf, &mut h.context);
3052        assert!(suppl_mock.is_supplicant_started());
3053
3054        let mut timed_event_stream = timer::make_async_timed_event_stream(h.time_stream);
3055
3056        // Setup mock response to transmit an EAPOL frame upon receipt of an EAPOL frame.
3057        let tx_eapol_frame_update_sink = vec![SecAssocUpdate::TxEapolKeyFrame {
3058            frame: test_utils::eapol_key_frame(),
3059            expect_response: true,
3060        }];
3061        suppl_mock.set_on_eapol_frame_updates(tx_eapol_frame_update_sink.clone());
3062
3063        // Send an initial EAPOL frame to SME
3064        let eapol_ind = MlmeEvent::EapolInd {
3065            ind: fidl_mlme::EapolIndication {
3066                src_addr: bssid.to_array(),
3067                dst_addr: fake_device_info().sta_addr,
3068                data: test_utils::eapol_key_frame().into(),
3069            },
3070        };
3071        let mut state = state.on_mlme_event(eapol_ind, &mut h.context);
3072
3073        // Cycle through the RSNA retransmissions and retransmission timeouts
3074        let mock_number_of_retransmissions = 5;
3075        for i in 0..=mock_number_of_retransmissions {
3076            expect_eapol_req(&mut h.mlme_stream, bssid);
3077            expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3078
3079            let rsna_retransmission_deadline =
3080                zx::MonotonicDuration::from_millis(event::RSNA_RETRANSMISSION_TIMEOUT_MILLIS)
3081                    .after_now();
3082            let timed_event = expect_next_event_at_deadline(
3083                &mut h.executor,
3084                &mut timed_event_stream,
3085                rsna_retransmission_deadline,
3086            );
3087            assert_matches!(timed_event.event, Event::RsnaRetransmissionTimeout(_));
3088
3089            if i < mock_number_of_retransmissions {
3090                suppl_mock
3091                    .set_on_rsna_retransmission_timeout_updates(tx_eapol_frame_update_sink.clone());
3092            }
3093            state = state.handle_timeout(timed_event.event, &mut h.context);
3094        }
3095
3096        // Check that the connection does not fail
3097        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3098
3099        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
3100            usme: contains {
3101                state_events: {
3102                    "0": {
3103                        "@time": AnyNumericProperty,
3104                        ctx: AnyStringProperty,
3105                        from: CONNECTING_STATE,
3106                        to: RSNA_STATE,
3107                    },
3108                },
3109                rsn_events: {
3110                    "0": {
3111                        "@time": AnyNumericProperty,
3112                        "rsna_status": "PmkSaEstablished"
3113                    },
3114                    "1": {
3115                        "@time": AnyNumericProperty,
3116                        "rx_eapol_frame": AnyBytesProperty,
3117                        "status": "processed",
3118                    },
3119                    "2": {
3120                        "@time": AnyNumericProperty,
3121                        "tx_eapol_frame": AnyBytesProperty,
3122                    },
3123                    "3": {
3124                        "@time": AnyNumericProperty,
3125                        "tx_eapol_frame": AnyBytesProperty,
3126                    },
3127                    "4": {
3128                        "@time": AnyNumericProperty,
3129                        "tx_eapol_frame": AnyBytesProperty,
3130                    },
3131                    "5": {
3132                        "@time": AnyNumericProperty,
3133                        "tx_eapol_frame": AnyBytesProperty,
3134                    },
3135                    "6": {
3136                        "@time": AnyNumericProperty,
3137                        "tx_eapol_frame": AnyBytesProperty,
3138                    },
3139                    "7": {
3140                        "@time": AnyNumericProperty,
3141                        "tx_eapol_frame": AnyBytesProperty,
3142                    },
3143                }
3144            },
3145        });
3146    }
3147
3148    #[test]
3149    fn retransmission_timeouts_do_not_extend_response_timeout() {
3150        let mut h = TestHelper::new_with_fake_time();
3151        h.executor.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
3152
3153        let (supplicant, suppl_mock) = mock_psk_supplicant();
3154        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
3155        let bss = command.bss.clone();
3156
3157        // Start in an "Connecting" state
3158        let state = ClientState::from(testing::new_state(Connecting {
3159            cfg: ClientConfig::default(),
3160            cmd: command,
3161            protection_ie: None,
3162            reassociation_loop_count: 0,
3163        }));
3164        suppl_mock
3165            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
3166        let assoc_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
3167        let state = state.on_mlme_event(assoc_conf, &mut h.context);
3168        assert!(suppl_mock.is_supplicant_started());
3169
3170        let mut timed_event_stream = timer::make_async_timed_event_stream(h.time_stream);
3171
3172        // Setup mock response to transmit an EAPOL frame upon receipt of an EAPOL frame.
3173        let tx_eapol_frame_update_sink = vec![SecAssocUpdate::TxEapolKeyFrame {
3174            frame: test_utils::eapol_key_frame(),
3175            expect_response: true,
3176        }];
3177        suppl_mock.set_on_eapol_frame_updates(tx_eapol_frame_update_sink.clone());
3178
3179        // Send an initial EAPOL frame to SME
3180        let eapol_ind = MlmeEvent::EapolInd {
3181            ind: fidl_mlme::EapolIndication {
3182                src_addr: bss.bssid.to_array(),
3183                dst_addr: fake_device_info().sta_addr,
3184                data: test_utils::eapol_key_frame().into(),
3185            },
3186        };
3187
3188        let rsna_response_deadline =
3189            zx::MonotonicDuration::from_millis(event::RSNA_RESPONSE_TIMEOUT_MILLIS).after_now();
3190
3191        let mut state = state.on_mlme_event(eapol_ind, &mut h.context);
3192
3193        // Cycle through the RSNA retransmission timeouts
3194        let mock_number_of_retransmissions = 5;
3195        for i in 0..=mock_number_of_retransmissions {
3196            expect_eapol_req(&mut h.mlme_stream, bss.bssid);
3197            expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3198
3199            let rsna_retransmission_deadline =
3200                zx::MonotonicDuration::from_millis(event::RSNA_RETRANSMISSION_TIMEOUT_MILLIS)
3201                    .after_now();
3202            let timed_event = expect_next_event_at_deadline(
3203                &mut h.executor,
3204                &mut timed_event_stream,
3205                rsna_retransmission_deadline,
3206            );
3207            assert_matches!(timed_event.event, Event::RsnaRetransmissionTimeout(_));
3208
3209            if i < mock_number_of_retransmissions {
3210                suppl_mock
3211                    .set_on_rsna_retransmission_timeout_updates(tx_eapol_frame_update_sink.clone());
3212            }
3213            state = state.handle_timeout(timed_event.event, &mut h.context);
3214        }
3215
3216        // Expire the RSNA response timeout
3217        let timeout = expect_next_event_at_deadline(
3218            &mut h.executor,
3219            &mut timed_event_stream,
3220            rsna_response_deadline,
3221        );
3222        assert_matches!(timeout.event, Event::RsnaResponseTimeout(..));
3223        suppl_mock.set_on_rsna_response_timeout(EstablishRsnaFailureReason::RsnaResponseTimeout(
3224            wlan_rsn::Error::EapolHandshakeIncomplete("PTKSA never initialized".to_string()),
3225        ));
3226        let state = state.handle_timeout(timeout.event, &mut h.context);
3227
3228        expect_deauth_req(&mut h.mlme_stream, bss.bssid, fidl_ieee80211::ReasonCode::StaLeaving);
3229        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
3230            assert_eq!(result, EstablishRsnaFailure {
3231                auth_method: Some(auth::MethodName::Psk),
3232                reason: EstablishRsnaFailureReason::RsnaResponseTimeout(wlan_rsn::Error::EapolHandshakeIncomplete("PTKSA never initialized".to_string())),
3233            }.into());
3234        });
3235
3236        // (mlme->sme) Send a DeauthenticateConf as a response
3237        let deauth_conf = MlmeEvent::DeauthenticateConf {
3238            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: bss.bssid.to_array() },
3239        };
3240        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3241        assert_idle(state);
3242
3243        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
3244            usme: contains {
3245                state_events: {
3246                    "0": {
3247                        "@time": AnyNumericProperty,
3248                        ctx: AnyStringProperty,
3249                        from: CONNECTING_STATE,
3250                        to: RSNA_STATE,
3251                    },
3252                    "1": {
3253                        "@time": AnyNumericProperty,
3254                        ctx: AnyStringProperty,
3255                        from: RSNA_STATE,
3256                        to: DISCONNECTING_STATE,
3257                    },
3258                    "2": {
3259                        "@time": AnyNumericProperty,
3260                        from: DISCONNECTING_STATE,
3261                        to: IDLE_STATE,
3262                    },
3263                },
3264                rsn_events: {
3265                    "0": {
3266                        "@time": AnyNumericProperty,
3267                        "rsna_status": "PmkSaEstablished"
3268                    },
3269                    "1": {
3270                        "@time": AnyNumericProperty,
3271                        "rx_eapol_frame": AnyBytesProperty,
3272                        "status": "processed",
3273                    },
3274                    "2": {
3275                        "@time": AnyNumericProperty,
3276                        "tx_eapol_frame": AnyBytesProperty,
3277                    },
3278                    "3": {
3279                        "@time": AnyNumericProperty,
3280                        "tx_eapol_frame": AnyBytesProperty,
3281                    },
3282                    "4": {
3283                        "@time": AnyNumericProperty,
3284                        "tx_eapol_frame": AnyBytesProperty,
3285                    },
3286                    "5": {
3287                        "@time": AnyNumericProperty,
3288                        "tx_eapol_frame": AnyBytesProperty,
3289                    },
3290                    "6": {
3291                        "@time": AnyNumericProperty,
3292                        "tx_eapol_frame": AnyBytesProperty,
3293                    },
3294                    "7": {
3295                        "@time": AnyNumericProperty,
3296                        "tx_eapol_frame": AnyBytesProperty,
3297                    }
3298                }
3299            },
3300        });
3301    }
3302
3303    #[test]
3304    fn simple_completion_timeout_with_responsive_ap() {
3305        let mut h = TestHelper::new_with_fake_time();
3306        h.executor.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
3307
3308        let (supplicant, suppl_mock) = mock_psk_supplicant();
3309        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
3310        let bss = command.bss.clone();
3311
3312        // Start in an "Connecting" state
3313        let state = ClientState::from(testing::new_state(Connecting {
3314            cfg: ClientConfig::default(),
3315            cmd: command,
3316            protection_ie: None,
3317            reassociation_loop_count: 0,
3318        }));
3319        suppl_mock
3320            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
3321        let assoc_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
3322        let state = state.on_mlme_event(assoc_conf, &mut h.context);
3323        assert!(suppl_mock.is_supplicant_started());
3324        let rsna_completion_deadline =
3325            zx::MonotonicDuration::from_millis(event::RSNA_COMPLETION_TIMEOUT_MILLIS).after_now();
3326
3327        let mut timed_event_stream = timer::make_async_timed_event_stream(h.time_stream);
3328
3329        // Setup mock response to transmit an EAPOL frame upon receipt of an EAPOL frame
3330        let tx_eapol_frame_update_sink = vec![SecAssocUpdate::TxEapolKeyFrame {
3331            frame: test_utils::eapol_key_frame(),
3332            expect_response: true,
3333        }];
3334        suppl_mock.set_on_eapol_frame_updates(tx_eapol_frame_update_sink.clone());
3335
3336        // Send an initial EAPOL frame to SME.
3337        let eapol_ind = fidl_mlme::EapolIndication {
3338            src_addr: bss.bssid.to_array(),
3339            dst_addr: fake_device_info().sta_addr,
3340            data: test_utils::eapol_key_frame().into(),
3341        };
3342        let mut state =
3343            state.on_mlme_event(MlmeEvent::EapolInd { ind: eapol_ind.clone() }, &mut h.context);
3344        expect_eapol_req(&mut h.mlme_stream, bss.bssid);
3345        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3346
3347        let mut rsna_retransmission_deadline =
3348            zx::MonotonicDuration::from_millis(event::RSNA_RETRANSMISSION_TIMEOUT_MILLIS)
3349                .after_now();
3350        let mut rsna_response_deadline =
3351            zx::MonotonicDuration::from_millis(event::RSNA_RESPONSE_TIMEOUT_MILLIS).after_now();
3352
3353        let mock_just_before_progress_frames = 2;
3354        let just_before_duration = zx::MonotonicDuration::from_nanos(1);
3355        for _ in 0..mock_just_before_progress_frames {
3356            // Expire the restransmission timeout
3357            let timed_event = expect_next_event_at_deadline(
3358                &mut h.executor,
3359                &mut timed_event_stream,
3360                rsna_retransmission_deadline,
3361            );
3362            assert_matches!(timed_event.event, Event::RsnaRetransmissionTimeout(_));
3363            state = state.handle_timeout(timed_event.event, &mut h.context);
3364            expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3365
3366            // Receive a frame just before the response timeout would have expired.
3367            h.executor.set_fake_time(rsna_response_deadline - just_before_duration);
3368            assert!(!h.executor.wake_expired_timers());
3369            // Setup mock response to transmit another EAPOL frame
3370            suppl_mock.set_on_eapol_frame_updates(tx_eapol_frame_update_sink.clone());
3371            state =
3372                state.on_mlme_event(MlmeEvent::EapolInd { ind: eapol_ind.clone() }, &mut h.context);
3373            expect_eapol_req(&mut h.mlme_stream, bss.bssid);
3374            expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3375            rsna_retransmission_deadline =
3376                zx::MonotonicDuration::from_millis(event::RSNA_RETRANSMISSION_TIMEOUT_MILLIS)
3377                    .after_now();
3378            rsna_response_deadline =
3379                zx::MonotonicDuration::from_millis(event::RSNA_RESPONSE_TIMEOUT_MILLIS).after_now();
3380        }
3381
3382        // Expire the final retransmission timeout
3383        let timed_event = expect_next_event_at_deadline(
3384            &mut h.executor,
3385            &mut timed_event_stream,
3386            rsna_retransmission_deadline,
3387        );
3388        assert_matches!(timed_event.event, Event::RsnaRetransmissionTimeout(_));
3389        state = state.handle_timeout(timed_event.event, &mut h.context);
3390        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3391
3392        // Advance to the completion timeout and setup a failure reason
3393        let timed_event = expect_next_event_at_deadline(
3394            &mut h.executor,
3395            &mut timed_event_stream,
3396            rsna_completion_deadline,
3397        );
3398        assert_matches!(timed_event.event, Event::RsnaCompletionTimeout(RsnaCompletionTimeout {}));
3399        suppl_mock.set_on_rsna_completion_timeout(
3400            EstablishRsnaFailureReason::RsnaCompletionTimeout(
3401                wlan_rsn::Error::EapolHandshakeIncomplete("PTKSA never initialized".to_string()),
3402            ),
3403        );
3404        let state = state.handle_timeout(timed_event.event, &mut h.context);
3405
3406        // Check that SME sends a deauthenticate request and fails the connection
3407        expect_deauth_req(&mut h.mlme_stream, bss.bssid, fidl_ieee80211::ReasonCode::StaLeaving);
3408        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
3409            assert_eq!(result, EstablishRsnaFailure {
3410                auth_method: Some(auth::MethodName::Psk),
3411                reason: EstablishRsnaFailureReason::RsnaCompletionTimeout(wlan_rsn::Error::EapolHandshakeIncomplete("PTKSA never initialized".to_string())),
3412            }.into());
3413        });
3414
3415        // (mlme->sme) Send a DeauthenticateConf as a response
3416        let deauth_conf = MlmeEvent::DeauthenticateConf {
3417            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: bss.bssid.to_array() },
3418        };
3419        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3420        assert_idle(state);
3421
3422        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
3423            usme: contains {
3424                state_events: {
3425                    "0": {
3426                        "@time": AnyNumericProperty,
3427                        ctx: AnyStringProperty,
3428                        from: CONNECTING_STATE,
3429                        to: RSNA_STATE,
3430                    },
3431                    "1": {
3432                        "@time": AnyNumericProperty,
3433                        ctx: AnyStringProperty,
3434                        from: RSNA_STATE,
3435                        to: DISCONNECTING_STATE,
3436                    },
3437                    "2": {
3438                        "@time": AnyNumericProperty,
3439                        from: DISCONNECTING_STATE,
3440                        to: IDLE_STATE,
3441                    },
3442                },
3443                rsn_events: {
3444                    "0": {
3445                        "@time": AnyNumericProperty,
3446                        "rsna_status": "PmkSaEstablished"
3447                    },
3448                    "1": {
3449                        "@time": AnyNumericProperty,
3450                        "rx_eapol_frame": AnyBytesProperty,
3451                        "status": "processed",
3452                    },
3453                    "2": {
3454                        "@time": AnyNumericProperty,
3455                        "tx_eapol_frame": AnyBytesProperty,
3456                    },
3457                    "3": {
3458                        "@time": AnyNumericProperty,
3459                        "rx_eapol_frame": AnyBytesProperty,
3460                        "status": "processed",
3461                    },
3462                    "4": {
3463                        "@time": AnyNumericProperty,
3464                        "tx_eapol_frame": AnyBytesProperty,
3465                    },
3466                    "5": {
3467                        "@time": AnyNumericProperty,
3468                        "rx_eapol_frame": AnyBytesProperty,
3469                        "status": "processed",
3470                    },
3471                    "6": {
3472                        "@time": AnyNumericProperty,
3473                        "tx_eapol_frame": AnyBytesProperty,
3474                    },
3475                }
3476            },
3477        });
3478    }
3479
3480    #[test]
3481    fn gtk_rotation_during_link_up() {
3482        let mut h = TestHelper::new();
3483        let (supplicant, suppl_mock) = mock_psk_supplicant();
3484        let (cmd, mut connect_txn_stream) = connect_command_wpa2(supplicant);
3485        let bssid = cmd.bss.bssid;
3486        let state = link_up_state(cmd);
3487
3488        // (mlme->sme) Send an EapolInd, mock supplication with key frame and GTK
3489        let key_frame = SecAssocUpdate::TxEapolKeyFrame {
3490            frame: test_utils::eapol_key_frame(),
3491            expect_response: true,
3492        };
3493        let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk()));
3494        let mut state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![key_frame, gtk]);
3495
3496        // EAPoL frame is sent out, but state still remains the same
3497        expect_eapol_req(&mut h.mlme_stream, bssid);
3498        expect_set_gtk(&mut h.mlme_stream);
3499        expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
3500        assert_matches!(&state, ClientState::Associated(state) => {
3501            assert_matches!(&state.link_state, LinkState::LinkUp { .. });
3502        });
3503
3504        // Any timeout is ignored
3505        let (_, timed_event, _) = h.time_stream.try_next().unwrap().expect("expect timed event");
3506        state = state.handle_timeout(timed_event.event, &mut h.context);
3507        assert_matches!(&state, ClientState::Associated(state) => {
3508            assert_matches!(&state.link_state, LinkState::LinkUp { .. });
3509        });
3510
3511        // No new ConnectResult is sent
3512        assert_matches!(connect_txn_stream.try_next(), Err(_));
3513    }
3514
3515    #[test]
3516    fn connect_while_link_up() {
3517        let mut h = TestHelper::new();
3518        let (cmd1, mut connect_txn_stream1) = connect_command_one();
3519        let (cmd2, mut connect_txn_stream2) = connect_command_two();
3520        let state = link_up_state(cmd1);
3521        let state = state.connect(cmd2, &mut h.context);
3522        let state = exchange_deauth(state, &mut h);
3523
3524        // First stream should be dropped already
3525        assert_matches!(connect_txn_stream1.try_next(), Ok(None));
3526        // Second stream should either have event or is empty, but is not dropped
3527        assert_matches!(connect_txn_stream2.try_next(), Ok(Some(_)) | Err(_));
3528
3529        // (sme->mlme) Expect a ConnectRequest
3530        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(req))) => {
3531            assert_eq!(req.selected_bss, (*connect_command_two().0.bss).into());
3532        });
3533        assert_connecting(state, &connect_command_two().0.bss);
3534    }
3535
3536    async fn expect_state_events_link_up_roaming_link_up(
3537        inspector: &Inspector,
3538        selected_bss: BssDescription,
3539    ) {
3540        assert_data_tree!(inspector, root: contains {
3541            usme: contains {
3542                state_events: {
3543                    "0": {
3544                        "@time": AnyNumericProperty,
3545                        bssid: selected_bss.bssid.to_string(),
3546                        ctx: AnyStringProperty,
3547                        from: LINK_UP_STATE,
3548                        to: ROAMING_STATE,
3549                    },
3550                    "1": {
3551                        "@time": AnyNumericProperty,
3552                        bssid: selected_bss.bssid.to_string(),
3553                        ctx: AnyStringProperty,
3554                        from: ROAMING_STATE,
3555                        ssid: selected_bss.ssid.to_string(),
3556                        to: LINK_UP_STATE,
3557                    },
3558                },
3559            },
3560        });
3561    }
3562
3563    #[test]
3564    fn fullmac_initiated_roam_happy_path_unprotected() {
3565        let mut h = TestHelper::new();
3566        let (cmd, mut connect_txn_stream) = connect_command_one();
3567        let mut selected_bss = cmd.bss.clone();
3568        let state = link_up_state(cmd);
3569        let selected_bssid = [0, 1, 2, 3, 4, 5];
3570        selected_bss.bssid = selected_bssid.into();
3571        #[allow(
3572            clippy::redundant_field_names,
3573            reason = "mass allow for https://fxbug.dev/381896734"
3574        )]
3575        let roam_start_ind = MlmeEvent::RoamStartInd {
3576            ind: fidl_mlme::RoamStartIndication {
3577                selected_bssid: selected_bssid,
3578                original_association_maintained: false,
3579                selected_bss: (*selected_bss).clone().into(),
3580            },
3581        };
3582        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
3583        assert_roaming(&state);
3584
3585        let mut association_ies = vec![];
3586        association_ies.extend_from_slice(selected_bss.ies());
3587        let ind = fidl_mlme::RoamResultIndication {
3588            selected_bssid,
3589            status_code: fidl_ieee80211::StatusCode::Success,
3590            original_association_maintained: false,
3591            target_bss_authenticated: true,
3592            association_id: 42,
3593            association_ies,
3594        };
3595        let roam_result_ind_event = MlmeEvent::RoamResultInd { ind: ind.clone() };
3596        let state = state.on_mlme_event(roam_result_ind_event, &mut h.context);
3597        assert_matches!(&state, ClientState::Associated(state) => {
3598            assert_matches!(&state.link_state, LinkState::LinkUp { .. });
3599        });
3600
3601        // User should be notified that the roam succeeded.
3602        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnRoamResult {result})) => {
3603            assert_eq!(result, RoamResult::Success(Box::new(*selected_bss.clone())));
3604        });
3605
3606        h.executor.run_singlethreaded(expect_state_events_link_up_roaming_link_up(
3607            &h.inspector,
3608            *selected_bss.clone(),
3609        ));
3610    }
3611
3612    #[test]
3613    fn policy_initiated_roam_happy_path_unprotected() {
3614        let mut h = TestHelper::new();
3615        let (cmd, mut connect_txn_stream) = connect_command_one();
3616        let mut selected_bss = cmd.bss.clone();
3617        let state = link_up_state(cmd);
3618        let selected_bssid = [0, 1, 2, 3, 4, 5];
3619        selected_bss.bssid = selected_bssid.into();
3620
3621        let fidl_selected_bss = fidl_common::BssDescription::from(*selected_bss.clone());
3622        let state = state.roam(&mut h.context, fidl_selected_bss);
3623
3624        assert_roaming(&state);
3625
3626        let mut association_ies = vec![];
3627        association_ies.extend_from_slice(selected_bss.ies());
3628        let conf = fidl_mlme::RoamConfirm {
3629            selected_bssid,
3630            status_code: fidl_ieee80211::StatusCode::Success,
3631            original_association_maintained: false,
3632            target_bss_authenticated: true,
3633            association_id: 42,
3634            association_ies,
3635        };
3636        let roam_conf_event = MlmeEvent::RoamConf { conf: conf.clone() };
3637        let state = state.on_mlme_event(roam_conf_event, &mut h.context);
3638        assert_matches!(&state, ClientState::Associated(state) => {
3639            assert_matches!(&state.link_state, LinkState::LinkUp { .. });
3640        });
3641
3642        // User should be notified that the roam succeeded.
3643        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnRoamResult {result})) => {
3644            assert_eq!(result, RoamResult::Success(Box::new(*selected_bss.clone())));
3645        });
3646
3647        h.executor.run_singlethreaded(expect_state_events_link_up_roaming_link_up(
3648            &h.inspector,
3649            *selected_bss.clone(),
3650        ));
3651    }
3652
3653    async fn expect_state_events_link_up_roaming_rsna(
3654        inspector: &Inspector,
3655        selected_bss: BssDescription,
3656    ) {
3657        assert_data_tree!(inspector, root: contains {
3658            usme: contains {
3659                state_events: {
3660                    "0": {
3661                        "@time": AnyNumericProperty,
3662                        bssid: selected_bss.bssid.to_string(),
3663                        ctx: AnyStringProperty,
3664                        from: LINK_UP_STATE,
3665                        to: ROAMING_STATE,
3666                    },
3667                    "1": {
3668                        "@time": AnyNumericProperty,
3669                        bssid: selected_bss.bssid.to_string(),
3670                        ctx: AnyStringProperty,
3671                        from: ROAMING_STATE,
3672                        ssid: selected_bss.ssid.to_string(),
3673                        to: RSNA_STATE,
3674                    },
3675                },
3676            },
3677        });
3678    }
3679
3680    #[test]
3681    fn fullmac_initiated_roam_happy_path_protected() {
3682        let mut h = TestHelper::new();
3683        let (supplicant, suppl_mock) = mock_psk_supplicant();
3684
3685        let (cmd, _) = connect_command_wpa2(supplicant);
3686        let bss = (*cmd.bss).clone();
3687        let mut selected_bss = bss.clone();
3688        let selected_bssid = [1, 2, 3, 4, 5, 6];
3689        selected_bss.bssid = selected_bssid.into();
3690        let association_ies = selected_bss.ies().to_vec();
3691
3692        let state = link_up_state(cmd);
3693        // Initiate a roam attempt.
3694        #[allow(
3695            clippy::redundant_field_names,
3696            reason = "mass allow for https://fxbug.dev/381896734"
3697        )]
3698        let roam_start_ind = MlmeEvent::RoamStartInd {
3699            ind: fidl_mlme::RoamStartIndication {
3700                selected_bssid: selected_bssid,
3701                original_association_maintained: false,
3702                selected_bss: selected_bss.clone().into(),
3703            },
3704        };
3705        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
3706        assert_roaming(&state);
3707
3708        // Real supplicant would be reset here. Reset the mock supplicant.
3709        suppl_mock
3710            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
3711
3712        let ind = fidl_mlme::RoamResultIndication {
3713            selected_bssid,
3714            status_code: fidl_ieee80211::StatusCode::Success,
3715            original_association_maintained: false,
3716            target_bss_authenticated: true,
3717            association_id: 42,
3718            association_ies,
3719        };
3720        let roam_result_ind_event = MlmeEvent::RoamResultInd { ind: ind.clone() };
3721        let state = state.on_mlme_event(roam_result_ind_event, &mut h.context);
3722
3723        assert_matches!(&state, ClientState::Associated(state)  => {
3724            assert_matches!(&state.link_state, LinkState::EstablishingRsna { .. });
3725        });
3726
3727        h.executor.run_singlethreaded(expect_state_events_link_up_roaming_rsna(
3728            &h.inspector,
3729            selected_bss,
3730        ));
3731
3732        // Note: because a new supplicant is created for the roam to the target, we can't easily
3733        // test the 802.1X portion of the roam.
3734    }
3735
3736    #[test]
3737    fn policy_initiated_roam_happy_path_protected() {
3738        let mut h = TestHelper::new();
3739        let (supplicant, suppl_mock) = mock_psk_supplicant();
3740
3741        let (cmd, _) = connect_command_wpa2(supplicant);
3742        let bss = (*cmd.bss).clone();
3743        let mut selected_bss = bss.clone();
3744        let selected_bssid = [1, 2, 3, 4, 5, 6];
3745        selected_bss.bssid = selected_bssid.into();
3746        let association_ies = selected_bss.ies().to_vec();
3747
3748        let state = link_up_state(cmd);
3749        // Initiate a roam attempt.
3750        let fidl_selected_bss = fidl_common::BssDescription::from(selected_bss.clone());
3751        let state = state.roam(&mut h.context, fidl_selected_bss);
3752
3753        assert_roaming(&state);
3754
3755        // Real supplicant would be reset here. Reset the mock supplicant.
3756        suppl_mock
3757            .set_start_updates(vec![SecAssocUpdate::Status(SecAssocStatus::PmkSaEstablished)]);
3758
3759        let conf = fidl_mlme::RoamConfirm {
3760            selected_bssid,
3761            status_code: fidl_ieee80211::StatusCode::Success,
3762            original_association_maintained: false,
3763            target_bss_authenticated: true,
3764            association_id: 42,
3765            association_ies,
3766        };
3767        let roam_conf_event = MlmeEvent::RoamConf { conf: conf.clone() };
3768        let state = state.on_mlme_event(roam_conf_event, &mut h.context);
3769
3770        assert_matches!(&state, ClientState::Associated(state)  => {
3771            assert_matches!(&state.link_state, LinkState::EstablishingRsna { .. });
3772        });
3773
3774        h.executor.run_singlethreaded(expect_state_events_link_up_roaming_rsna(
3775            &h.inspector,
3776            selected_bss,
3777        ));
3778
3779        // Note: because a new supplicant is created for the roam to the target, we can't easily
3780        // test the 802.1X portion of the roam.
3781    }
3782
3783    fn expect_roam_failure_emitted(
3784        failure_type: RoamFailureType,
3785        status_code: fidl_ieee80211::StatusCode,
3786        selected_bssid: [u8; 6],
3787        mlme_event_name: fidl_sme::DisconnectMlmeEventName,
3788        connect_txn_stream: &mut ConnectTransactionStream,
3789    ) {
3790        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnRoamResult { result })) => {
3791            assert_matches!(result, RoamResult::Failed(failure) => {
3792                assert_eq!(failure.failure_type, failure_type);
3793                assert_eq!(failure.status_code, status_code);
3794                assert_eq!(failure.selected_bssid, selected_bssid.into());
3795                assert_matches!(failure.disconnect_info.disconnect_source, fidl_sme::DisconnectSource::Mlme(cause) => {
3796                    assert_eq!(cause.mlme_event_name, mlme_event_name);
3797                });
3798            });
3799        });
3800    }
3801
3802    // An all-zero BssDescription: malformed, in particular due to empty IEs (missing SSID).
3803    fn malformed_bss_description() -> fidl_common::BssDescription {
3804        fidl_common::BssDescription {
3805            bssid: [0, 0, 0, 0, 0, 0],
3806            bss_type: fidl_common::BssType::Infrastructure,
3807            beacon_period: 0,
3808            capability_info: 0,
3809            ies: Vec::new(),
3810            channel: fidl_ieee80211::WlanChannel {
3811                cbw: fidl_ieee80211::ChannelBandwidth::Cbw20,
3812                primary: 0,
3813                secondary80: 0,
3814            },
3815            rssi_dbm: 0,
3816            snr_db: 0,
3817        }
3818    }
3819
3820    async fn expect_state_events_link_up_disconnecting_idle(inspector: &Inspector) {
3821        assert_data_tree!(inspector, root: contains {
3822            usme: contains {
3823                state_events: {
3824                    "0": {
3825                        "@time": AnyNumericProperty,
3826                        ctx: AnyStringProperty,
3827                        from: LINK_UP_STATE,
3828                        to: DISCONNECTING_STATE,
3829                    },
3830                    "1": {
3831                        "@time": AnyNumericProperty,
3832                        from: DISCONNECTING_STATE,
3833                        to: IDLE_STATE,
3834                    },
3835                },
3836            },
3837        });
3838    }
3839
3840    #[test]
3841    fn malformed_roam_start_ind_causes_disconnect() {
3842        let mut h = TestHelper::new();
3843        let (cmd, mut connect_txn_stream) = connect_command_one();
3844        let state = link_up_state(cmd);
3845        // Note: this is intentionally malformed. Roam cannot proceed without the missing data, such
3846        // as the IEs.
3847        let selected_bss = malformed_bss_description();
3848        // Note that this BSSID does not match the BssDescription.
3849        let selected_bssid = [0, 1, 2, 3, 4, 5];
3850        let roam_start_ind = MlmeEvent::RoamStartInd {
3851            ind: fidl_mlme::RoamStartIndication {
3852                selected_bssid,
3853                original_association_maintained: false,
3854                selected_bss,
3855            },
3856        };
3857        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
3858
3859        // Check that SME sends a deauthenticate request to target BSS, since roam started.
3860        expect_deauth_req(
3861            &mut h.mlme_stream,
3862            selected_bssid.into(),
3863            fidl_ieee80211::ReasonCode::StaLeaving,
3864        );
3865
3866        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
3867        let deauth_conf = MlmeEvent::DeauthenticateConf {
3868            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: selected_bssid },
3869        };
3870        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3871        assert_idle(state);
3872
3873        // Roam failure will have the target BSSID from the roam start ind.
3874        expect_roam_failure_emitted(
3875            RoamFailureType::RoamStartMalformedFailure,
3876            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
3877            selected_bssid,
3878            fidl_sme::DisconnectMlmeEventName::RoamStartIndication,
3879            &mut connect_txn_stream,
3880        );
3881
3882        h.executor.run_singlethreaded(expect_state_events_link_up_disconnecting_idle(&h.inspector));
3883    }
3884
3885    #[test]
3886    fn malformed_roam_req_causes_disconnect() {
3887        let mut h = TestHelper::new();
3888        let (cmd, mut connect_txn_stream) = connect_command_one();
3889        let original_bssid = cmd.bss.bssid;
3890        let state = link_up_state(cmd);
3891        // Note: this is intentionally malformed. Roam cannot proceed without the missing data, such
3892        // as the IEs.
3893        let selected_bss = malformed_bss_description();
3894
3895        let state = state.roam(&mut h.context, selected_bss.clone());
3896
3897        // Check that SME sends a deauthenticate request to the current BSS, since roam has not started.
3898        expect_deauth_req(
3899            &mut h.mlme_stream,
3900            original_bssid,
3901            fidl_ieee80211::ReasonCode::StaLeaving,
3902        );
3903
3904        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
3905        let deauth_conf = MlmeEvent::DeauthenticateConf {
3906            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: original_bssid.to_array() },
3907        };
3908        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3909        assert_idle(state);
3910
3911        // Malformed roam request will have the target BSSID from the roam request.
3912        expect_roam_failure_emitted(
3913            RoamFailureType::RoamRequestMalformedFailure,
3914            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
3915            selected_bss.bssid,
3916            fidl_sme::DisconnectMlmeEventName::RoamRequest,
3917            &mut connect_txn_stream,
3918        );
3919
3920        h.executor.run_singlethreaded(expect_state_events_link_up_disconnecting_idle(&h.inspector));
3921    }
3922
3923    #[test]
3924    fn roam_req_with_incorrect_security_ies_causes_disconnect_on_protected_network() {
3925        let mut h = TestHelper::new();
3926        let (supplicant, _suppl_mock) = mock_psk_supplicant();
3927
3928        let (cmd, mut connect_txn_stream) = connect_command_wpa2(supplicant);
3929        let original_bssid = cmd.bss.bssid;
3930
3931        // Note: intentionally incorrect security config is created for this test.
3932        let mut selected_bss = fake_bss_description!(Wpa1, ssid: Ssid::try_from("wpa2").unwrap());
3933        let selected_bssid = [3, 2, 1, 0, 9, 8];
3934        selected_bss.bssid = selected_bssid.into();
3935
3936        let state = link_up_state(cmd);
3937
3938        let state = state.roam(&mut h.context, selected_bss.into());
3939
3940        // Check that SME sends a deauthenticate request to original BSS.
3941        expect_deauth_req(
3942            &mut h.mlme_stream,
3943            original_bssid,
3944            fidl_ieee80211::ReasonCode::StaLeaving,
3945        );
3946
3947        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
3948        let deauth_conf = MlmeEvent::DeauthenticateConf {
3949            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: original_bssid.to_array() },
3950        };
3951        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3952        assert_idle(state);
3953
3954        expect_roam_failure_emitted(
3955            RoamFailureType::SelectNetworkFailure,
3956            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
3957            selected_bssid,
3958            fidl_sme::DisconnectMlmeEventName::RoamRequest,
3959            &mut connect_txn_stream,
3960        );
3961
3962        h.executor.run_singlethreaded(expect_state_events_link_up_disconnecting_idle(&h.inspector));
3963    }
3964
3965    #[test]
3966    fn roam_start_ind_with_incorrect_security_ies_causes_disconnect_on_protected_network() {
3967        let mut h = TestHelper::new();
3968        let (supplicant, _suppl_mock) = mock_psk_supplicant();
3969
3970        let (cmd, mut connect_txn_stream) = connect_command_wpa2(supplicant);
3971        // Note: intentionally incorrect security config is created for this test.
3972        let mut selected_bss = fake_bss_description!(Wpa1, ssid: Ssid::try_from("wpa2").unwrap());
3973        let selected_bssid = [3, 2, 1, 0, 9, 8];
3974        selected_bss.bssid = selected_bssid.into();
3975
3976        let state = link_up_state(cmd);
3977        let roam_start_ind = MlmeEvent::RoamStartInd {
3978            ind: fidl_mlme::RoamStartIndication {
3979                selected_bssid,
3980                original_association_maintained: false,
3981                selected_bss: selected_bss.into(),
3982            },
3983        };
3984        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
3985
3986        // Check that SME sends a deauthenticate request.
3987        expect_deauth_req(
3988            &mut h.mlme_stream,
3989            selected_bssid.into(),
3990            fidl_ieee80211::ReasonCode::StaLeaving,
3991        );
3992
3993        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
3994        let deauth_conf = MlmeEvent::DeauthenticateConf {
3995            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: selected_bssid },
3996        };
3997        let state = state.on_mlme_event(deauth_conf, &mut h.context);
3998        assert_idle(state);
3999
4000        expect_roam_failure_emitted(
4001            RoamFailureType::SelectNetworkFailure,
4002            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
4003            selected_bssid,
4004            fidl_sme::DisconnectMlmeEventName::RoamStartIndication,
4005            &mut connect_txn_stream,
4006        );
4007
4008        h.executor.run_singlethreaded(expect_state_events_link_up_disconnecting_idle(&h.inspector));
4009    }
4010
4011    #[test]
4012    fn roam_start_ind_ignored_while_idle() {
4013        let mut h = TestHelper::new();
4014        let (cmd, mut connect_txn_stream) = connect_command_one();
4015        let mut selected_bss = cmd.bss.clone();
4016        let state = idle_state();
4017        let selected_bssid = [0, 1, 2, 3, 4, 5];
4018        selected_bss.bssid = selected_bssid.into();
4019        let roam_start_ind = MlmeEvent::RoamStartInd {
4020            ind: fidl_mlme::RoamStartIndication {
4021                selected_bssid,
4022                original_association_maintained: false,
4023                selected_bss: (*selected_bss).into(),
4024            },
4025        };
4026        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
4027        assert_idle(state);
4028
4029        assert_matches!(connect_txn_stream.try_next(), Err(_));
4030    }
4031
4032    #[test]
4033    fn roam_req_ignored_while_idle() {
4034        let mut h = TestHelper::new();
4035        let (cmd, mut connect_txn_stream) = connect_command_one();
4036        let mut selected_bss = cmd.bss.clone();
4037        let state = idle_state();
4038        let selected_bssid = [0, 1, 2, 3, 4, 5];
4039        selected_bss.bssid = selected_bssid.into();
4040        let fidl_selected_bss = fidl_common::BssDescription::from(*selected_bss.clone());
4041
4042        let state = state.roam(&mut h.context, fidl_selected_bss);
4043
4044        assert_idle(state);
4045
4046        assert_matches!(connect_txn_stream.try_next(), Err(_));
4047    }
4048
4049    #[test]
4050    fn roam_start_ind_ignored_while_connecting() {
4051        let mut h = TestHelper::new();
4052        let (cmd, mut connect_txn_stream) = connect_command_one();
4053        let original_bss = cmd.bss.clone();
4054        let mut selected_bss = cmd.bss.clone();
4055        let state = connecting_state(cmd);
4056        let selected_bssid = [0, 1, 2, 3, 4, 5];
4057        selected_bss.bssid = selected_bssid.into();
4058        let roam_start_ind = MlmeEvent::RoamStartInd {
4059            ind: fidl_mlme::RoamStartIndication {
4060                selected_bssid,
4061                original_association_maintained: false,
4062                selected_bss: (*selected_bss).into(),
4063            },
4064        };
4065        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
4066        assert_connecting(state, &original_bss);
4067
4068        // Nothing should be sent upward.
4069        assert_matches!(connect_txn_stream.try_next(), Ok(None));
4070    }
4071
4072    #[test]
4073    fn roam_req_ignored_while_connecting() {
4074        let mut h = TestHelper::new();
4075        let (cmd, mut connect_txn_stream) = connect_command_one();
4076        let original_bss = cmd.bss.clone();
4077        let mut selected_bss = cmd.bss.clone();
4078        let state = connecting_state(cmd);
4079        let selected_bssid = [0, 1, 2, 3, 4, 5];
4080        selected_bss.bssid = selected_bssid.into();
4081        let fidl_selected_bss = fidl_common::BssDescription::from(*selected_bss.clone());
4082
4083        let state = state.roam(&mut h.context, fidl_selected_bss);
4084
4085        assert_connecting(state, &original_bss);
4086
4087        // Nothing should be sent upward.
4088        assert_matches!(connect_txn_stream.try_next(), Ok(None));
4089    }
4090
4091    #[test]
4092    fn roam_start_ind_ignored_while_disconnecting() {
4093        let mut h = TestHelper::new();
4094        let (cmd, mut connect_txn_stream) = connect_command_one();
4095        let mut selected_bss = cmd.bss.clone();
4096        let state = disconnecting_state(PostDisconnectAction::BeginConnect { cmd });
4097        let selected_bssid = [0, 1, 2, 3, 4, 5];
4098        selected_bss.bssid = selected_bssid.into();
4099        let roam_start_ind = MlmeEvent::RoamStartInd {
4100            ind: fidl_mlme::RoamStartIndication {
4101                selected_bssid,
4102                original_association_maintained: false,
4103                selected_bss: (*selected_bss).into(),
4104            },
4105        };
4106        let state = state.on_mlme_event(roam_start_ind, &mut h.context);
4107        assert_disconnecting(state);
4108
4109        // Nothing should be sent upward.
4110        assert_matches!(connect_txn_stream.try_next(), Ok(None));
4111    }
4112
4113    #[test]
4114    fn roam_req_ignored_while_disconnecting() {
4115        let mut h = TestHelper::new();
4116        let (cmd, mut connect_txn_stream) = connect_command_one();
4117        let mut selected_bss = cmd.bss.clone();
4118        let state = disconnecting_state(PostDisconnectAction::BeginConnect { cmd });
4119        let selected_bssid = [0, 1, 2, 3, 4, 5];
4120        selected_bss.bssid = selected_bssid.into();
4121        let fidl_selected_bss = fidl_common::BssDescription::from(*selected_bss.clone());
4122
4123        let state = state.roam(&mut h.context, fidl_selected_bss);
4124
4125        assert_disconnecting(state);
4126
4127        // Nothing should be sent upward.
4128        assert_matches!(connect_txn_stream.try_next(), Ok(None));
4129    }
4130
4131    async fn expect_state_events_roaming_disconnecting_idle(inspector: &Inspector) {
4132        assert_data_tree!(inspector, root: contains {
4133            usme: contains {
4134                state_events: {
4135                    "0": {
4136                        "@time": AnyNumericProperty,
4137                        ctx: AnyStringProperty,
4138                        from: ROAMING_STATE,
4139                        to: DISCONNECTING_STATE,
4140                    },
4141                    "1": {
4142                        "@time": AnyNumericProperty,
4143                        from: DISCONNECTING_STATE,
4144                        to: IDLE_STATE,
4145                    },
4146                },
4147            },
4148        });
4149    }
4150
4151    #[test]
4152    fn roam_result_ind_with_failure_causes_disconnect() {
4153        let mut h = TestHelper::new();
4154        let (cmd, mut connect_txn_stream) = connect_command_one();
4155        let selected_bssid = [1, 2, 3, 4, 5, 6];
4156        let state = roaming_state(cmd, selected_bssid.into());
4157        let status_code = fidl_ieee80211::StatusCode::RefusedUnauthenticatedAccessNotSupported;
4158        #[allow(
4159            clippy::redundant_field_names,
4160            reason = "mass allow for https://fxbug.dev/381896734"
4161        )]
4162        let roam_result_ind = MlmeEvent::RoamResultInd {
4163            ind: fidl_mlme::RoamResultIndication {
4164                selected_bssid: selected_bssid,
4165                status_code,
4166                original_association_maintained: false,
4167                target_bss_authenticated: true,
4168                association_id: 0,
4169                association_ies: Vec::new(),
4170            },
4171        };
4172        let state = state.on_mlme_event(roam_result_ind, &mut h.context);
4173
4174        // Check that SME sends a deauthenticate request.
4175        expect_deauth_req(
4176            &mut h.mlme_stream,
4177            selected_bssid.into(),
4178            fidl_ieee80211::ReasonCode::StaLeaving,
4179        );
4180
4181        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
4182        let deauth_conf = MlmeEvent::DeauthenticateConf {
4183            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: selected_bssid },
4184        };
4185        let state = state.on_mlme_event(deauth_conf, &mut h.context);
4186        assert_idle(state);
4187
4188        expect_roam_failure_emitted(
4189            RoamFailureType::ReassociationFailure,
4190            status_code,
4191            selected_bssid,
4192            fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
4193            &mut connect_txn_stream,
4194        );
4195
4196        h.executor.run_singlethreaded(expect_state_events_roaming_disconnecting_idle(&h.inspector));
4197    }
4198
4199    #[test]
4200    fn malformed_roam_result_ind_causes_disconnect() {
4201        let mut h = TestHelper::new();
4202        let (cmd, mut connect_txn_stream) = connect_command_one();
4203        let selected_bssid = [1, 2, 3, 4, 5, 6];
4204        let mismatched_bssid = [9, 8, 7, 6, 5, 4];
4205        let state = roaming_state(cmd, selected_bssid.into());
4206        let status_code = fidl_ieee80211::StatusCode::Success;
4207        let roam_result_ind = MlmeEvent::RoamResultInd {
4208            ind: fidl_mlme::RoamResultIndication {
4209                selected_bssid: mismatched_bssid,
4210                status_code,
4211                original_association_maintained: false,
4212                target_bss_authenticated: true,
4213                association_id: 0,
4214                association_ies: Vec::new(),
4215            },
4216        };
4217        let state = state.on_mlme_event(roam_result_ind, &mut h.context);
4218
4219        // Check that SME sends a deauthenticate request.
4220        expect_deauth_req(
4221            &mut h.mlme_stream,
4222            selected_bssid.into(),
4223            fidl_ieee80211::ReasonCode::StaLeaving,
4224        );
4225
4226        // (mlme->sme) Send a DeauthenticateConf as a response, to advance to the post disconnect action.
4227        let deauth_conf = MlmeEvent::DeauthenticateConf {
4228            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: selected_bssid },
4229        };
4230        let state = state.on_mlme_event(deauth_conf, &mut h.context);
4231        assert_idle(state);
4232
4233        expect_roam_failure_emitted(
4234            RoamFailureType::RoamResultMalformedFailure,
4235            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
4236            selected_bssid,
4237            fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
4238            &mut connect_txn_stream,
4239        );
4240
4241        h.executor.run_singlethreaded(expect_state_events_roaming_disconnecting_idle(&h.inspector));
4242    }
4243
4244    fn make_disconnect_request(
4245        h: &mut TestHelper,
4246    ) -> (
4247        <fidl_sme::ClientSmeProxy as fidl_sme::ClientSmeProxyInterface>::DisconnectResponseFut,
4248        fidl_sme::ClientSmeDisconnectResponder,
4249    ) {
4250        let (proxy, mut stream) =
4251            fidl::endpoints::create_proxy_and_stream::<fidl_sme::ClientSmeMarker>();
4252        let mut disconnect_fut =
4253            proxy.disconnect(fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme);
4254        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Pending);
4255        let responder = assert_matches!(
4256            h.executor.run_singlethreaded(stream.next()).unwrap().unwrap(),
4257            fidl_sme::ClientSmeRequest::Disconnect{ responder, .. } => responder);
4258        (disconnect_fut, responder)
4259    }
4260
4261    #[test]
4262    fn disconnect_while_idle() {
4263        let mut h = TestHelper::new();
4264        let (mut disconnect_fut, responder) = make_disconnect_request(&mut h);
4265        let new_state = idle_state().disconnect(
4266            &mut h.context,
4267            fidl_sme::UserDisconnectReason::WlanSmeUnitTesting,
4268            responder,
4269        );
4270        assert_idle(new_state);
4271        // Expect no messages to the MLME
4272        assert!(h.mlme_stream.try_next().is_err());
4273        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Ready(Ok(())));
4274
4275        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4276            usme: contains {
4277                state_events: {
4278                    "0": {
4279                        "@time": AnyNumericProperty,
4280                        ctx: AnyStringProperty,
4281                        from: IDLE_STATE,
4282                        to: IDLE_STATE,
4283                    },
4284                }
4285            },
4286        });
4287    }
4288
4289    #[test]
4290    fn disconnect_while_connecting() {
4291        let mut h = TestHelper::new();
4292        let (cmd, mut connect_txn_stream) = connect_command_one();
4293        let state = connecting_state(cmd);
4294        let state = disconnect(state, &mut h, fidl_sme::UserDisconnectReason::WlanSmeUnitTesting);
4295        let state = exchange_deauth(state, &mut h);
4296        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect: false })) => {
4297            assert_eq!(result, ConnectResult::Canceled);
4298        });
4299        assert_idle(state);
4300
4301        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4302            usme: contains {
4303                state_events: {
4304                    "0": {
4305                        "@time": AnyNumericProperty,
4306                        ctx: AnyStringProperty,
4307                        from: CONNECTING_STATE,
4308                        to: DISCONNECTING_STATE,
4309                    },
4310                    "1": {
4311                        "@time": AnyNumericProperty,
4312                        from: DISCONNECTING_STATE,
4313                        to: IDLE_STATE,
4314                    },
4315                },
4316            },
4317        });
4318    }
4319
4320    #[test]
4321    fn disconnect_while_link_up() {
4322        let mut h = TestHelper::new();
4323        let state = link_up_state(connect_command_one().0);
4324        let state = disconnect(state, &mut h, fidl_sme::UserDisconnectReason::FailedToConnect);
4325        let state = exchange_deauth(state, &mut h);
4326        assert_idle(state);
4327
4328        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4329            usme: contains {
4330                state_events: {
4331                    "0": {
4332                        "@time": AnyNumericProperty,
4333                        ctx: AnyStringProperty,
4334                        from: LINK_UP_STATE,
4335                        to: DISCONNECTING_STATE,
4336                    },
4337                    "1": {
4338                        "@time": AnyNumericProperty,
4339                        from: DISCONNECTING_STATE,
4340                        to: IDLE_STATE,
4341                    },
4342                },
4343            },
4344        });
4345    }
4346
4347    #[test]
4348    fn timeout_during_disconnect() {
4349        let mut h = TestHelper::new();
4350        let (cmd, _connect_txn_stream) = connect_command_one();
4351        let mut state = link_up_state(cmd);
4352
4353        let (mut disconnect_fut, responder) = make_disconnect_request(&mut h);
4354        state = state.disconnect(
4355            &mut h.context,
4356            fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme,
4357            responder,
4358        );
4359        assert_matches!(&state, ClientState::Disconnecting(_));
4360
4361        let timed_event =
4362            assert_matches!(h.time_stream.try_next(), Ok(Some((_, timed_event, _))) => timed_event);
4363        assert_matches!(timed_event.event, Event::DeauthenticateTimeout(..));
4364
4365        let state = state.handle_timeout(timed_event.event, &mut h.context);
4366        assert_idle(state);
4367        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Ready(Ok(())));
4368
4369        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4370            usme: contains {
4371                state_events: {
4372                    "0": {
4373                        "@time": AnyNumericProperty,
4374                        ctx: AnyStringProperty,
4375                        from: LINK_UP_STATE,
4376                        to: DISCONNECTING_STATE,
4377                    },
4378                    "1": {
4379                        "@time": AnyNumericProperty,
4380                        ctx: AnyStringProperty,
4381                        from: DISCONNECTING_STATE,
4382                        to: IDLE_STATE,
4383                    },
4384                },
4385            },
4386        });
4387    }
4388
4389    #[test]
4390    fn new_connect_while_disconnecting() {
4391        let mut h = TestHelper::new();
4392        let (cmd1, _connect_txn_stream) = connect_command_one();
4393        let (cmd2, _connect_txn_stream) = connect_command_two();
4394        let bss2 = cmd2.bss.clone();
4395        let state = link_up_state(cmd1);
4396        let (mut disconnect_fut, responder) = make_disconnect_request(&mut h);
4397        let state = state.disconnect(
4398            &mut h.context,
4399            fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme,
4400            responder,
4401        );
4402
4403        let disconnecting =
4404            assert_matches!(&state, ClientState::Disconnecting(disconnecting) => disconnecting);
4405        assert_matches!(&disconnecting.action, PostDisconnectAction::RespondDisconnect { .. });
4406        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Pending);
4407
4408        let state = state.connect(cmd2, &mut h.context);
4409        let disconnecting =
4410            assert_matches!(&state, ClientState::Disconnecting(disconnecting) => disconnecting);
4411        assert_matches!(&disconnecting.action, PostDisconnectAction::BeginConnect { .. });
4412        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Ready(Ok(())));
4413        let state = exchange_deauth(state, &mut h);
4414        assert_connecting(state, &connect_command_two().0.bss);
4415
4416        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4417            usme: contains {
4418                state_events: {
4419                    "0": {
4420                        "@time": AnyNumericProperty,
4421                        ctx: AnyStringProperty,
4422                        from: LINK_UP_STATE,
4423                        to: DISCONNECTING_STATE,
4424                    },
4425                    "1": {
4426                        "@time": AnyNumericProperty,
4427                        ctx: AnyStringProperty,
4428                        from: DISCONNECTING_STATE,
4429                        to: CONNECTING_STATE,
4430                        bssid: bss2.bssid.to_string(),
4431                        ssid: bss2.ssid.to_string(),
4432                    },
4433                },
4434            },
4435        });
4436    }
4437
4438    #[test]
4439    fn disconnect_while_disconnecting_for_pending_connect() {
4440        let mut h = TestHelper::new();
4441        let (cmd, mut connect_txn_stream) = connect_command_one();
4442        let state = disconnecting_state(PostDisconnectAction::BeginConnect { cmd });
4443
4444        let (_fut, responder) = make_disconnect_request(&mut h);
4445        let state = state.disconnect(
4446            &mut h.context,
4447            fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme,
4448            responder,
4449        );
4450        let disconnecting =
4451            assert_matches!(&state, ClientState::Disconnecting(disconnecting) => disconnecting);
4452        assert_matches!(&disconnecting.action, PostDisconnectAction::RespondDisconnect { .. });
4453
4454        let result = assert_matches!(
4455            connect_txn_stream.try_next(),
4456            Ok(Some(ConnectTransactionEvent::OnConnectResult { result, .. })) => result
4457        );
4458        assert_eq!(result, ConnectResult::Canceled);
4459
4460        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4461            usme: contains {
4462                state_events: {
4463                    "0": {
4464                        "@time": AnyNumericProperty,
4465                        ctx: AnyStringProperty,
4466                        from: DISCONNECTING_STATE,
4467                        to: DISCONNECTING_STATE,
4468                    }
4469                },
4470            },
4471        });
4472    }
4473
4474    #[test]
4475    fn increment_att_id_on_connect() {
4476        let mut h = TestHelper::new();
4477        let state = idle_state();
4478        assert_eq!(h.context.att_id, 0);
4479
4480        let state = state.connect(connect_command_one().0, &mut h.context);
4481        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(_))));
4482        assert_eq!(h.context.att_id, 1);
4483
4484        let state = disconnect(state, &mut h, fidl_sme::UserDisconnectReason::WlanSmeUnitTesting);
4485        let state = exchange_deauth(state, &mut h);
4486        assert_eq!(h.context.att_id, 1);
4487
4488        let state = state.connect(connect_command_two().0, &mut h.context);
4489        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(_))));
4490        assert_eq!(h.context.att_id, 2);
4491
4492        let state = state.connect(connect_command_one().0, &mut h.context);
4493        let _state = exchange_deauth(state, &mut h);
4494        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Connect(_))));
4495        assert_eq!(h.context.att_id, 3);
4496    }
4497
4498    #[test]
4499    fn increment_att_id_on_disassociate_ind() {
4500        let mut h = TestHelper::new();
4501        let (cmd, _connect_txn_stream) = connect_command_one();
4502        let bss = cmd.bss.clone();
4503        let state = link_up_state(cmd);
4504        assert_eq!(h.context.att_id, 0);
4505
4506        let disassociate_ind = MlmeEvent::DisassociateInd {
4507            ind: fidl_mlme::DisassociateIndication {
4508                peer_sta_address: [0, 0, 0, 0, 0, 0],
4509                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
4510                locally_initiated: false,
4511            },
4512        };
4513
4514        let state = state.on_mlme_event(disassociate_ind, &mut h.context);
4515        assert_matches!(&state, ClientState::Connecting(connecting) =>
4516                            assert_eq!(connecting.reassociation_loop_count, 1));
4517        assert_connecting(state, &bss);
4518        assert_eq!(h.context.att_id, 1);
4519    }
4520
4521    #[test]
4522    fn abort_connect_after_max_associate_retries() {
4523        let mut h = TestHelper::new();
4524        let (supplicant, _suppl_mock) = mock_psk_supplicant();
4525        let (command, mut connect_txn_stream) = connect_command_wpa2(supplicant);
4526        let mut state = establishing_rsna_state(command);
4527
4528        match &mut state {
4529            ClientState::Associated(associated) => {
4530                associated.reassociation_loop_count = MAX_REASSOCIATIONS_WITHOUT_LINK_UP;
4531            }
4532            _ => unreachable!(),
4533        }
4534
4535        let disassociate_ind = MlmeEvent::DisassociateInd {
4536            ind: fidl_mlme::DisassociateIndication {
4537                peer_sta_address: [0, 0, 0, 0, 0, 0],
4538                reason_code: fidl_ieee80211::ReasonCode::UnacceptablePowerCapability,
4539                locally_initiated: true,
4540            },
4541        };
4542        let state = state.on_mlme_event(disassociate_ind, &mut h.context);
4543
4544        // We should notify of a disconnect and stop any retries.
4545        assert_disconnecting(state);
4546        let info = assert_matches!(
4547            connect_txn_stream.try_next(),
4548            Ok(Some(ConnectTransactionEvent::OnDisconnect { info })) => info
4549        );
4550        assert!(!info.is_sme_reconnecting);
4551
4552        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4553            usme: contains {
4554                state_events: {
4555                    "0": {
4556                        "@time": AnyNumericProperty,
4557                        ctx: AnyStringProperty,
4558                        from: RSNA_STATE,
4559                        to: DISCONNECTING_STATE,
4560                    }
4561                },
4562            },
4563        });
4564    }
4565
4566    #[test]
4567    fn do_not_log_disconnect_ctx_on_disassoc_from_non_link_up() {
4568        let mut h = TestHelper::new();
4569        let (supplicant, _suppl_mock) = mock_psk_supplicant();
4570        let (command, _connect_txn_stream) = connect_command_wpa2(supplicant);
4571        let state = establishing_rsna_state(command);
4572
4573        let disassociate_ind = MlmeEvent::DisassociateInd {
4574            ind: fidl_mlme::DisassociateIndication {
4575                peer_sta_address: [0, 0, 0, 0, 0, 0],
4576                reason_code: fidl_ieee80211::ReasonCode::UnacceptablePowerCapability,
4577                locally_initiated: true,
4578            },
4579        };
4580        let state = state.on_mlme_event(disassociate_ind, &mut h.context);
4581        assert_connecting(
4582            state,
4583            &fake_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap()),
4584        );
4585
4586        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4587            usme: contains {
4588                state_events: {
4589                    "0": {
4590                        "@time": AnyNumericProperty,
4591                        ctx: AnyStringProperty,
4592                        from: RSNA_STATE,
4593                        to: CONNECTING_STATE,
4594                    }
4595                },
4596            },
4597        });
4598    }
4599
4600    #[test]
4601    fn disconnect_reported_on_deauth_ind() {
4602        let mut h = TestHelper::new();
4603        let (cmd, mut connect_txn_stream) = connect_command_one();
4604        let state = link_up_state(cmd);
4605
4606        let deauth_ind = MlmeEvent::DeauthenticateInd {
4607            ind: fidl_mlme::DeauthenticateIndication {
4608                peer_sta_address: [0, 0, 0, 0, 0, 0],
4609                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
4610                locally_initiated: true,
4611            },
4612        };
4613
4614        let _state = state.on_mlme_event(deauth_ind, &mut h.context);
4615        let fidl_info = assert_matches!(
4616            connect_txn_stream.try_next(),
4617            Ok(Some(ConnectTransactionEvent::OnDisconnect { info })) => info
4618        );
4619        assert!(!fidl_info.is_sme_reconnecting);
4620        assert_eq!(
4621            fidl_info.disconnect_source,
4622            fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
4623                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
4624                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
4625            })
4626        );
4627
4628        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4629            usme: contains {
4630                state_events: {
4631                    "0": {
4632                        "@time": AnyNumericProperty,
4633                        ctx: AnyStringProperty,
4634                        from: LINK_UP_STATE,
4635                        to: IDLE_STATE,
4636                    }
4637                },
4638            },
4639        });
4640    }
4641
4642    #[test]
4643    fn disconnect_reported_on_disassoc_ind_then_reconnect_successfully() {
4644        let mut h = TestHelper::new();
4645        let (cmd, mut connect_txn_stream) = connect_command_one();
4646        let bss = cmd.bss.clone();
4647        let state = link_up_state(cmd);
4648
4649        let deauth_ind = MlmeEvent::DisassociateInd {
4650            ind: fidl_mlme::DisassociateIndication {
4651                peer_sta_address: [0, 0, 0, 0, 0, 0],
4652                reason_code: fidl_ieee80211::ReasonCode::ReasonInactivity,
4653                locally_initiated: true,
4654            },
4655        };
4656
4657        let state = state.on_mlme_event(deauth_ind, &mut h.context);
4658        let info = assert_matches!(
4659            connect_txn_stream.try_next(),
4660            Ok(Some(ConnectTransactionEvent::OnDisconnect { info })) => info
4661        );
4662        assert!(info.is_sme_reconnecting);
4663        assert_eq!(
4664            info.disconnect_source,
4665            fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
4666                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
4667                reason_code: fidl_ieee80211::ReasonCode::ReasonInactivity,
4668            })
4669        );
4670
4671        // Check that reconnect is attempted
4672        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Reconnect(req))) => {
4673            assert_eq!(&req.peer_sta_address, bss.bssid.as_array());
4674        });
4675
4676        // (mlme->sme) Send a ConnectConf
4677        let connect_conf = create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::Success);
4678        let _state = state.on_mlme_event(connect_conf, &mut h.context);
4679
4680        // User should be notified that we are reconnected
4681        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect })) => {
4682            assert_eq!(result, ConnectResult::Success);
4683            assert!(is_reconnect);
4684        });
4685
4686        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4687            usme: contains {
4688                state_events: {
4689                    "0": {
4690                        "@time": AnyNumericProperty,
4691                        ctx: AnyStringProperty,
4692                        from: LINK_UP_STATE,
4693                        to: CONNECTING_STATE,
4694                    },
4695                    "1": {
4696                        "@time": AnyNumericProperty,
4697                        ctx: AnyStringProperty,
4698                        from: CONNECTING_STATE,
4699                        to: LINK_UP_STATE,
4700                    }
4701                },
4702            },
4703        });
4704    }
4705
4706    #[test]
4707    fn disconnect_reported_on_disassoc_ind_then_reconnect_unsuccessfully() {
4708        let mut h = TestHelper::new();
4709        let (cmd, mut connect_txn_stream) = connect_command_one();
4710        let bss = cmd.bss.clone();
4711        let state = link_up_state(cmd);
4712
4713        let disassoc_ind = MlmeEvent::DisassociateInd {
4714            ind: fidl_mlme::DisassociateIndication {
4715                peer_sta_address: [0, 0, 0, 0, 0, 0],
4716                reason_code: fidl_ieee80211::ReasonCode::ReasonInactivity,
4717                locally_initiated: true,
4718            },
4719        };
4720
4721        let state = state.on_mlme_event(disassoc_ind, &mut h.context);
4722        assert_matches!(
4723            connect_txn_stream.try_next(),
4724            Ok(Some(ConnectTransactionEvent::OnDisconnect { .. }))
4725        );
4726
4727        // Check that reconnect is attempted
4728        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Reconnect(req))) => {
4729            assert_eq!(&req.peer_sta_address, bss.bssid.as_array());
4730        });
4731
4732        // (mlme->sme) Send a ConnectConf
4733        let connect_conf =
4734            create_connect_conf(bss.bssid, fidl_ieee80211::StatusCode::RefusedReasonUnspecified);
4735        let state = state.on_mlme_event(connect_conf, &mut h.context);
4736
4737        let state = exchange_deauth(state, &mut h);
4738        assert_idle(state);
4739
4740        // User should be notified that reconnection attempt failed
4741        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnConnectResult { result, is_reconnect })) => {
4742            assert_eq!(result, AssociationFailure {
4743                bss_protection: BssProtection::Open,
4744                code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
4745            }.into());
4746            assert!(is_reconnect);
4747        });
4748
4749        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4750            usme: contains {
4751                state_events: {
4752                    "0": {
4753                        "@time": AnyNumericProperty,
4754                        ctx: AnyStringProperty,
4755                        from: LINK_UP_STATE,
4756                        to: CONNECTING_STATE,
4757                    },
4758                    "1": {
4759                        "@time": AnyNumericProperty,
4760                        ctx: AnyStringProperty,
4761                        from: CONNECTING_STATE,
4762                        to: DISCONNECTING_STATE,
4763                    },
4764                    "2": {
4765                        "@time": AnyNumericProperty,
4766                        from: DISCONNECTING_STATE,
4767                        to: IDLE_STATE,
4768                    },
4769                },
4770            },
4771        });
4772    }
4773
4774    #[test]
4775    fn disconnect_reported_on_manual_disconnect() {
4776        let mut h = TestHelper::new();
4777        let (cmd, mut connect_txn_stream) = connect_command_one();
4778        let state = link_up_state(cmd);
4779
4780        let state = disconnect(state, &mut h, fidl_sme::UserDisconnectReason::WlanSmeUnitTesting);
4781        assert_idle(state);
4782        let info = assert_matches!(
4783            connect_txn_stream.try_next(),
4784            Ok(Some(ConnectTransactionEvent::OnDisconnect { info })) => info
4785        );
4786        assert!(!info.is_sme_reconnecting);
4787        assert_eq!(
4788            info.disconnect_source,
4789            fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::WlanSmeUnitTesting)
4790        );
4791
4792        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4793            usme: contains {
4794                state_events: {
4795                    "0": {
4796                        "@time": AnyNumericProperty,
4797                        ctx: AnyStringProperty,
4798                        from: LINK_UP_STATE,
4799                        to: DISCONNECTING_STATE,
4800                    },
4801                    "1": {
4802                        "@time": AnyNumericProperty,
4803                        from: DISCONNECTING_STATE,
4804                        to: IDLE_STATE,
4805                    }
4806                },
4807            },
4808        });
4809    }
4810
4811    #[test]
4812    fn disconnect_reported_on_manual_disconnect_with_wsc() {
4813        let mut h = TestHelper::new();
4814        let (mut cmd, mut connect_txn_stream) = connect_command_one();
4815        *cmd.bss = fake_bss_description!(Open,
4816            ssid: Ssid::try_from("bar").unwrap(),
4817            bssid: [8; 6],
4818            rssi_dbm: 60,
4819            snr_db: 30,
4820            ies_overrides: IesOverrides::new().set_raw(
4821                get_vendor_ie_bytes_for_wsc_ie(&fake_probe_resp_wsc_ie_bytes()).expect("getting vendor ie bytes")
4822        ));
4823
4824        let state = link_up_state(cmd);
4825        let state = disconnect(state, &mut h, fidl_sme::UserDisconnectReason::WlanSmeUnitTesting);
4826        assert_idle(state);
4827
4828        let info = assert_matches!(
4829            connect_txn_stream.try_next(),
4830            Ok(Some(ConnectTransactionEvent::OnDisconnect { info })) => info
4831        );
4832        assert!(!info.is_sme_reconnecting);
4833        assert_eq!(
4834            info.disconnect_source,
4835            fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::WlanSmeUnitTesting)
4836        );
4837
4838        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4839            usme: contains {
4840                state_events: {
4841                    "0": {
4842                        "@time": AnyNumericProperty,
4843                        ctx: AnyStringProperty,
4844                        from: LINK_UP_STATE,
4845                        to: DISCONNECTING_STATE,
4846                    },
4847                    "1": {
4848                        "@time": AnyNumericProperty,
4849                        from: DISCONNECTING_STATE,
4850                        to: IDLE_STATE,
4851                    }
4852                },
4853            },
4854        });
4855    }
4856
4857    #[test]
4858    fn bss_channel_switch_ind() {
4859        let mut h = TestHelper::new();
4860        let (mut cmd, mut connect_txn_stream) = connect_command_one();
4861        *cmd.bss = fake_bss_description!(Open,
4862            ssid: Ssid::try_from("bar").unwrap(),
4863            bssid: [8; 6],
4864            channel: Channel::new(1, Cbw::Cbw20),
4865        );
4866        let state = link_up_state(cmd);
4867
4868        let input_info = fidl_internal::ChannelSwitchInfo { new_channel: 36 };
4869        let switch_ind = MlmeEvent::OnChannelSwitched { info: input_info };
4870
4871        assert_matches!(&state, ClientState::Associated(state) => {
4872            assert_eq!(state.latest_ap_state.channel.primary, 1);
4873        });
4874        let state = state.on_mlme_event(switch_ind, &mut h.context);
4875        assert_matches!(state, ClientState::Associated(state) => {
4876            assert_eq!(state.latest_ap_state.channel.primary, 36);
4877        });
4878
4879        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnChannelSwitched { info })) => {
4880            assert_eq!(info, input_info);
4881        });
4882    }
4883
4884    #[test]
4885    fn connect_failure_rsne_wrapped_in_legacy_wpa() {
4886        let (supplicant, _suppl_mock) = mock_psk_supplicant();
4887
4888        let (mut command, _connect_txn_stream) = connect_command_wpa2(supplicant);
4889        let bss = command.bss.clone();
4890        // Take the RSNA and wrap it in LegacyWpa to make it invalid.
4891        if let Protection::Rsna(rsna) = command.protection {
4892            command.protection = Protection::LegacyWpa(rsna);
4893        } else {
4894            panic!("command is guaranteed to be contain legacy wpa");
4895        };
4896
4897        let mut h = TestHelper::new();
4898        let state = idle_state().connect(command, &mut h.context);
4899
4900        // State did not change to Connecting because command is invalid, thus ignored.
4901        assert_matches!(state, ClientState::Idle(_));
4902
4903        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4904            usme: contains {
4905                state_events: {
4906                    "0": {
4907                        "@time": AnyNumericProperty,
4908                        ctx: AnyStringProperty,
4909                        from: IDLE_STATE,
4910                        to: IDLE_STATE,
4911                        bssid: bss.bssid.to_string(),
4912                        ssid: bss.ssid.to_string(),
4913                    }
4914                },
4915            },
4916        });
4917    }
4918
4919    #[test]
4920    fn connect_failure_legacy_wpa_wrapped_in_rsna() {
4921        let (supplicant, _suppl_mock) = mock_psk_supplicant();
4922
4923        let (mut command, _connect_txn_stream) = connect_command_wpa1(supplicant);
4924        let bss = command.bss.clone();
4925        // Take the LegacyWpa RSNA and wrap it in Rsna to make it invalid.
4926        if let Protection::LegacyWpa(rsna) = command.protection {
4927            command.protection = Protection::Rsna(rsna);
4928        } else {
4929            panic!("command is guaranteed to be contain legacy wpa");
4930        };
4931
4932        let mut h = TestHelper::new();
4933        let state = idle_state();
4934        let state = state.connect(command, &mut h.context);
4935
4936        // State did not change to Connecting because command is invalid, thus ignored.
4937        assert_matches!(state, ClientState::Idle(_));
4938
4939        assert_data_tree!(@executor h.executor, h.inspector, root: contains {
4940            usme: contains {
4941                state_events: {
4942                    "0": {
4943                        "@time": AnyNumericProperty,
4944                        ctx: AnyStringProperty,
4945                        from: IDLE_STATE,
4946                        to: IDLE_STATE,
4947                        bssid: bss.bssid.to_string(),
4948                        ssid: bss.ssid.to_string(),
4949                    }
4950                },
4951            },
4952        });
4953    }
4954
4955    #[test]
4956    fn status_returns_last_rssi_snr() {
4957        let mut h = TestHelper::new();
4958        let time_a = now();
4959
4960        let (cmd, mut connect_txn_stream) = connect_command_one();
4961        let state = link_up_state(cmd);
4962        let input_ind = fidl_internal::SignalReportIndication { rssi_dbm: -42, snr_db: 20 };
4963        let state = state.on_mlme_event(MlmeEvent::SignalReport { ind: input_ind }, &mut h.context);
4964        let serving_ap_info = assert_matches!(state.status(),
4965                                                     ClientSmeStatus::Connected(serving_ap_info) =>
4966                                                     serving_ap_info);
4967        assert_eq!(serving_ap_info.rssi_dbm, -42);
4968        assert_eq!(serving_ap_info.snr_db, 20);
4969        assert!(serving_ap_info.signal_report_time > time_a);
4970        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnSignalReport { ind })) => {
4971            assert_eq!(input_ind, ind);
4972        });
4973
4974        let time_b = now();
4975        let signal_report_time = assert_matches!(state.status(),
4976                                                 ClientSmeStatus::Connected(serving_ap_info) =>
4977                                                 serving_ap_info.signal_report_time);
4978        assert!(signal_report_time < time_b);
4979
4980        let input_ind = fidl_internal::SignalReportIndication { rssi_dbm: -24, snr_db: 10 };
4981        let state = state.on_mlme_event(MlmeEvent::SignalReport { ind: input_ind }, &mut h.context);
4982        let serving_ap_info = assert_matches!(state.status(),
4983                                                     ClientSmeStatus::Connected(serving_ap_info) =>
4984                                                     serving_ap_info);
4985        assert_eq!(serving_ap_info.rssi_dbm, -24);
4986        assert_eq!(serving_ap_info.snr_db, 10);
4987        let signal_report_time = assert_matches!(state.status(),
4988                                                 ClientSmeStatus::Connected(serving_ap_info) =>
4989                                                 serving_ap_info.signal_report_time);
4990        assert!(signal_report_time > time_b);
4991        assert_matches!(connect_txn_stream.try_next(), Ok(Some(ConnectTransactionEvent::OnSignalReport { ind })) => {
4992            assert_eq!(input_ind, ind);
4993        });
4994
4995        let time_c = now();
4996        let signal_report_time = assert_matches!(state.status(),
4997                                                 ClientSmeStatus::Connected(serving_ap_info) =>
4998                                                 serving_ap_info.signal_report_time);
4999        assert!(signal_report_time < time_c);
5000    }
5001
5002    fn test_sae_frame_rx_tx(
5003        mock_supplicant_controller: MockSupplicantController,
5004        state: ClientState,
5005    ) -> ClientState {
5006        let mut h = TestHelper::new();
5007        let frame_rx = fidl_mlme::SaeFrame {
5008            peer_sta_address: [0xaa; 6],
5009            status_code: fidl_ieee80211::StatusCode::Success,
5010            seq_num: 1,
5011            sae_fields: vec![1, 2, 3, 4, 5],
5012        };
5013        let frame_tx = fidl_mlme::SaeFrame {
5014            peer_sta_address: [0xbb; 6],
5015            status_code: fidl_ieee80211::StatusCode::Success,
5016            seq_num: 2,
5017            sae_fields: vec![1, 2, 3, 4, 5, 6, 7, 8],
5018        };
5019        mock_supplicant_controller
5020            .set_on_sae_frame_rx_updates(vec![SecAssocUpdate::TxSaeFrame(frame_tx)]);
5021        let state =
5022            state.on_mlme_event(MlmeEvent::OnSaeFrameRx { frame: frame_rx }, &mut h.context);
5023        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::SaeFrameTx(_))));
5024        state
5025    }
5026
5027    #[test]
5028    fn sae_sends_frame_in_connecting() {
5029        let (supplicant, suppl_mock) = mock_psk_supplicant();
5030        let (cmd, _connect_txn_stream) = connect_command_wpa3(supplicant);
5031        let state = connecting_state(cmd);
5032        let end_state = test_sae_frame_rx_tx(suppl_mock, state);
5033        assert_matches!(end_state, ClientState::Connecting(_))
5034    }
5035
5036    fn test_sae_frame_ind_resp(
5037        mock_supplicant_controller: MockSupplicantController,
5038        state: ClientState,
5039    ) -> ClientState {
5040        let mut h = TestHelper::new();
5041        let ind = fidl_mlme::SaeHandshakeIndication { peer_sta_address: [0xaa; 6] };
5042        // For the purposes of the test, skip the rx/tx and just say we succeeded.
5043        mock_supplicant_controller.set_on_sae_handshake_ind_updates(vec![
5044            SecAssocUpdate::SaeAuthStatus(AuthStatus::Success),
5045        ]);
5046        let state = state.on_mlme_event(MlmeEvent::OnSaeHandshakeInd { ind }, &mut h.context);
5047
5048        let resp = assert_matches!(
5049            h.mlme_stream.try_next(),
5050            Ok(Some(MlmeRequest::SaeHandshakeResp(resp))) => resp);
5051        assert_eq!(resp.status_code, fidl_ieee80211::StatusCode::Success);
5052        state
5053    }
5054
5055    #[test]
5056    fn sae_ind_in_connecting() {
5057        let (supplicant, suppl_mock) = mock_psk_supplicant();
5058        let (cmd, _connect_txn_stream) = connect_command_wpa3(supplicant);
5059        let state = connecting_state(cmd);
5060        let end_state = test_sae_frame_ind_resp(suppl_mock, state);
5061        assert_matches!(end_state, ClientState::Connecting(_))
5062    }
5063
5064    fn test_sae_timeout(
5065        mock_supplicant_controller: MockSupplicantController,
5066        state: ClientState,
5067    ) -> ClientState {
5068        let mut h = TestHelper::new();
5069        let frame_tx = fidl_mlme::SaeFrame {
5070            peer_sta_address: [0xbb; 6],
5071            status_code: fidl_ieee80211::StatusCode::Success,
5072            seq_num: 2,
5073            sae_fields: vec![1, 2, 3, 4, 5, 6, 7, 8],
5074        };
5075        mock_supplicant_controller
5076            .set_on_sae_timeout_updates(vec![SecAssocUpdate::TxSaeFrame(frame_tx)]);
5077        let state = state.handle_timeout(event::SaeTimeout(2).into(), &mut h.context);
5078        assert_matches!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::SaeFrameTx(_))));
5079        state
5080    }
5081
5082    fn test_sae_timeout_failure(
5083        mock_supplicant_controller: MockSupplicantController,
5084        state: ClientState,
5085    ) {
5086        let mut h = TestHelper::new();
5087        mock_supplicant_controller
5088            .set_on_sae_timeout_failure(anyhow::anyhow!("Failed to process timeout"));
5089        let state = state.handle_timeout(event::SaeTimeout(2).into(), &mut h.context);
5090        let state = exchange_deauth(state, &mut h);
5091        assert_matches!(state, ClientState::Idle(_))
5092    }
5093
5094    #[test]
5095    fn sae_timeout_in_connecting() {
5096        let (supplicant, suppl_mock) = mock_psk_supplicant();
5097        let (cmd, _connect_txn_stream) = connect_command_wpa3(supplicant);
5098        let state = connecting_state(cmd);
5099        let end_state = test_sae_timeout(suppl_mock, state);
5100        assert_matches!(end_state, ClientState::Connecting(_));
5101    }
5102
5103    #[test]
5104    fn sae_timeout_failure_in_connecting() {
5105        let (supplicant, suppl_mock) = mock_psk_supplicant();
5106        let (cmd, _connect_txn_stream) = connect_command_wpa3(supplicant);
5107        let state = connecting_state(cmd);
5108        test_sae_timeout_failure(suppl_mock, state);
5109    }
5110
5111    #[test]
5112    fn update_wmm_ac_params_new() {
5113        let mut h = TestHelper::new();
5114        let wmm_param = None;
5115        let state = link_up_state_with_wmm(connect_command_one().0, wmm_param);
5116
5117        let state = state.on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_OK), &mut h.context);
5118        assert_matches!(state, ClientState::Associated(state) => {
5119            assert_matches!(state.wmm_param, Some(wmm_param) => {
5120                assert!(wmm_param.wmm_info.ap_wmm_info().uapsd());
5121                assert_wmm_param_acs(&wmm_param);
5122            })
5123        });
5124    }
5125
5126    #[test]
5127    fn update_wmm_ac_params_existing() {
5128        let mut h = TestHelper::new();
5129
5130        let existing_wmm_param =
5131            *ie::parse_wmm_param(&fake_wmm_param().bytes[..]).expect("parse wmm");
5132        existing_wmm_param.wmm_info.ap_wmm_info().set_uapsd(false);
5133        let state = link_up_state_with_wmm(connect_command_one().0, Some(existing_wmm_param));
5134
5135        let state = state.on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_OK), &mut h.context);
5136        assert_matches!(state, ClientState::Associated(state) => {
5137            assert_matches!(state.wmm_param, Some(wmm_param) => {
5138                assert!(wmm_param.wmm_info.ap_wmm_info().uapsd());
5139                assert_wmm_param_acs(&wmm_param);
5140            })
5141        });
5142    }
5143
5144    #[test]
5145    fn update_wmm_ac_params_fails() {
5146        let mut h = TestHelper::new();
5147
5148        let existing_wmm_param =
5149            *ie::parse_wmm_param(&fake_wmm_param().bytes[..]).expect("parse wmm");
5150        let state = link_up_state_with_wmm(connect_command_one().0, Some(existing_wmm_param));
5151
5152        let state = state
5153            .on_mlme_event(create_on_wmm_status_resp(zx::sys::ZX_ERR_UNAVAILABLE), &mut h.context);
5154        assert_matches!(state, ClientState::Associated(state) => {
5155            assert_matches!(state.wmm_param, Some(wmm_param) => {
5156                assert_eq!(wmm_param, existing_wmm_param);
5157            })
5158        });
5159    }
5160
5161    fn assert_wmm_param_acs(wmm_param: &ie::WmmParam) {
5162        assert_eq!(wmm_param.ac_be_params.aci_aifsn.aifsn(), 1);
5163        assert!(!wmm_param.ac_be_params.aci_aifsn.acm());
5164        assert_eq!(wmm_param.ac_be_params.ecw_min_max.ecw_min(), 2);
5165        assert_eq!(wmm_param.ac_be_params.ecw_min_max.ecw_max(), 3);
5166        assert_eq!({ wmm_param.ac_be_params.txop_limit }, 4);
5167
5168        assert_eq!(wmm_param.ac_bk_params.aci_aifsn.aifsn(), 5);
5169        assert!(!wmm_param.ac_bk_params.aci_aifsn.acm());
5170        assert_eq!(wmm_param.ac_bk_params.ecw_min_max.ecw_min(), 6);
5171        assert_eq!(wmm_param.ac_bk_params.ecw_min_max.ecw_max(), 7);
5172        assert_eq!({ wmm_param.ac_bk_params.txop_limit }, 8);
5173
5174        assert_eq!(wmm_param.ac_vi_params.aci_aifsn.aifsn(), 9);
5175        assert!(wmm_param.ac_vi_params.aci_aifsn.acm());
5176        assert_eq!(wmm_param.ac_vi_params.ecw_min_max.ecw_min(), 10);
5177        assert_eq!(wmm_param.ac_vi_params.ecw_min_max.ecw_max(), 11);
5178        assert_eq!({ wmm_param.ac_vi_params.txop_limit }, 12);
5179
5180        assert_eq!(wmm_param.ac_vo_params.aci_aifsn.aifsn(), 13);
5181        assert!(wmm_param.ac_vo_params.aci_aifsn.acm());
5182        assert_eq!(wmm_param.ac_vo_params.ecw_min_max.ecw_min(), 14);
5183        assert_eq!(wmm_param.ac_vo_params.ecw_min_max.ecw_max(), 15);
5184        assert_eq!({ wmm_param.ac_vo_params.txop_limit }, 16);
5185    }
5186
5187    // Helper functions and data structures for tests
5188    struct TestHelper {
5189        mlme_stream: MlmeStream,
5190        time_stream: timer::EventStream<Event>,
5191        context: Context,
5192        // Inspector is kept so that root node doesn't automatically get removed from VMO
5193        inspector: Inspector,
5194        // Executor is needed as a time provider for the [`inspect_log!`] macro which panics
5195        // without a fuchsia_async executor set up
5196        executor: fuchsia_async::TestExecutor,
5197    }
5198
5199    impl TestHelper {
5200        fn new_(with_fake_time: bool) -> Self {
5201            let executor = if with_fake_time {
5202                fuchsia_async::TestExecutor::new_with_fake_time()
5203            } else {
5204                fuchsia_async::TestExecutor::new()
5205            };
5206
5207            let (mlme_sink, mlme_stream) = mpsc::unbounded();
5208            let (timer, time_stream) = timer::create_timer();
5209            let inspector = Inspector::default();
5210            let context = Context {
5211                device_info: Arc::new(fake_device_info()),
5212                mlme_sink: MlmeSink::new(mlme_sink),
5213                timer,
5214                att_id: 0,
5215                inspect: Arc::new(inspect::SmeTree::new(
5216                    inspector.clone(),
5217                    inspector.root().create_child("usme"),
5218                    &test_utils::fake_device_info([1u8; 6].into()),
5219                    &fake_spectrum_management_support_empty(),
5220                )),
5221                security_support: fake_security_support(),
5222            };
5223            TestHelper { mlme_stream, time_stream, context, inspector, executor }
5224        }
5225        fn new() -> Self {
5226            Self::new_(false)
5227        }
5228        fn new_with_fake_time() -> Self {
5229            Self::new_(true)
5230        }
5231    }
5232
5233    fn on_eapol_ind(
5234        state: ClientState,
5235        helper: &mut TestHelper,
5236        bssid: Bssid,
5237        suppl_mock: &MockSupplicantController,
5238        update_sink: UpdateSink,
5239    ) -> ClientState {
5240        suppl_mock.set_on_eapol_frame_updates(update_sink);
5241        // (mlme->sme) Send an EapolInd
5242        let eapol_ind = create_eapol_ind(bssid, test_utils::eapol_key_frame().into());
5243        state.on_mlme_event(eapol_ind, &mut helper.context)
5244    }
5245
5246    fn on_set_keys_conf(
5247        state: ClientState,
5248        helper: &mut TestHelper,
5249        key_ids: Vec<u16>,
5250    ) -> ClientState {
5251        state.on_mlme_event(
5252            MlmeEvent::SetKeysConf {
5253                conf: fidl_mlme::SetKeysConfirm {
5254                    results: key_ids
5255                        .into_iter()
5256                        .map(|key_id| fidl_mlme::SetKeyResult {
5257                            key_id,
5258                            status: zx::Status::OK.into_raw(),
5259                        })
5260                        .collect(),
5261                },
5262            },
5263            &mut helper.context,
5264        )
5265    }
5266
5267    fn create_eapol_ind(bssid: Bssid, data: Vec<u8>) -> MlmeEvent {
5268        MlmeEvent::EapolInd {
5269            ind: fidl_mlme::EapolIndication {
5270                src_addr: bssid.to_array(),
5271                dst_addr: fake_device_info().sta_addr,
5272                data,
5273            },
5274        }
5275    }
5276
5277    fn exchange_deauth(state: ClientState, h: &mut TestHelper) -> ClientState {
5278        // (sme->mlme) Expect a DeauthenticateRequest
5279        let peer_sta_address = assert_matches!(
5280            h.mlme_stream.try_next(),
5281            Ok(Some(MlmeRequest::Deauthenticate(req))) => req.peer_sta_address
5282        );
5283
5284        // (mlme->sme) Send a DeauthenticateConf as a response
5285        let deauth_conf = MlmeEvent::DeauthenticateConf {
5286            resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address },
5287        };
5288        state.on_mlme_event(deauth_conf, &mut h.context)
5289    }
5290
5291    fn expect_set_ctrl_port(
5292        mlme_stream: &mut MlmeStream,
5293        bssid: Bssid,
5294        state: fidl_mlme::ControlledPortState,
5295    ) {
5296        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetCtrlPort(req))) => {
5297            assert_eq!(&req.peer_sta_address, bssid.as_array());
5298            assert_eq!(req.state, state);
5299        });
5300    }
5301
5302    fn expect_deauth_req(
5303        mlme_stream: &mut MlmeStream,
5304        bssid: Bssid,
5305        reason_code: fidl_ieee80211::ReasonCode,
5306    ) {
5307        // (sme->mlme) Expect a DeauthenticateRequest
5308        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Deauthenticate(req))) => {
5309            assert_eq!(bssid.as_array(), &req.peer_sta_address);
5310            assert_eq!(reason_code, req.reason_code);
5311        });
5312    }
5313
5314    #[track_caller]
5315    fn expect_eapol_req(mlme_stream: &mut MlmeStream, bssid: Bssid) {
5316        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Eapol(req))) => {
5317            assert_eq!(req.src_addr, fake_device_info().sta_addr);
5318            assert_eq!(&req.dst_addr, bssid.as_array());
5319            assert_eq!(req.data, Vec::<u8>::from(test_utils::eapol_key_frame()));
5320        });
5321    }
5322
5323    fn expect_set_ptk(mlme_stream: &mut MlmeStream, bssid: Bssid) {
5324        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => {
5325            assert_eq!(set_keys_req.keylist.len(), 1);
5326            let k = set_keys_req.keylist.first().expect("expect key descriptor");
5327            assert_eq!(k.key, vec![0xCCu8; test_utils::cipher().tk_bytes().unwrap() as usize]);
5328            assert_eq!(k.key_id, 0);
5329            assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise);
5330            assert_eq!(&k.address, bssid.as_array());
5331            assert_eq!(k.rsc, 0);
5332            assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]);
5333            assert_eq!(k.cipher_suite_type, fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(4));
5334        });
5335    }
5336
5337    fn expect_set_gtk(mlme_stream: &mut MlmeStream) {
5338        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => {
5339            assert_eq!(set_keys_req.keylist.len(), 1);
5340            let k = set_keys_req.keylist.first().expect("expect key descriptor");
5341            assert_eq!(&k.key[..], &test_utils::gtk_bytes()[..]);
5342            assert_eq!(k.key_id, 2);
5343            assert_eq!(k.key_type, fidl_mlme::KeyType::Group);
5344            assert_eq!(k.address, [0xFFu8; 6]);
5345            assert_eq!(k.rsc, 0);
5346            assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]);
5347            assert_eq!(k.cipher_suite_type, fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(4));
5348        });
5349    }
5350
5351    fn expect_set_wpa1_ptk(mlme_stream: &mut MlmeStream, bssid: Bssid) {
5352        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => {
5353            assert_eq!(set_keys_req.keylist.len(), 1);
5354            let k = set_keys_req.keylist.first().expect("expect key descriptor");
5355            assert_eq!(k.key, vec![0xCCu8; test_utils::wpa1_cipher().tk_bytes().unwrap() as usize]);
5356            assert_eq!(k.key_id, 0);
5357            assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise);
5358            assert_eq!(&k.address, bssid.as_array());
5359            assert_eq!(k.rsc, 0);
5360            assert_eq!(k.cipher_suite_oui, [0x00, 0x50, 0xF2]);
5361            assert_eq!(k.cipher_suite_type, fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(2));
5362        });
5363    }
5364
5365    fn expect_set_wpa1_gtk(mlme_stream: &mut MlmeStream) {
5366        assert_matches!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => {
5367            assert_eq!(set_keys_req.keylist.len(), 1);
5368            let k = set_keys_req.keylist.first().expect("expect key descriptor");
5369            assert_eq!(&k.key[..], &test_utils::wpa1_gtk_bytes()[..]);
5370            assert_eq!(k.key_id, 2);
5371            assert_eq!(k.key_type, fidl_mlme::KeyType::Group);
5372            assert_eq!(k.address, [0xFFu8; 6]);
5373            assert_eq!(k.rsc, 0);
5374            assert_eq!(k.cipher_suite_oui, [0x00, 0x50, 0xF2]);
5375            assert_eq!(k.cipher_suite_type, fidl_ieee80211::CipherSuiteType::from_primitive_allow_unknown(2));
5376        });
5377    }
5378
5379    fn connect_command_one() -> (ConnectCommand, ConnectTransactionStream) {
5380        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5381        let cmd = ConnectCommand {
5382            bss: Box::new(fake_bss_description!(Open,
5383                ssid: Ssid::try_from("foo").unwrap(),
5384                bssid: [7, 7, 7, 7, 7, 7],
5385                rssi_dbm: 60,
5386                snr_db: 30
5387            )),
5388            connect_txn_sink,
5389            protection: Protection::Open,
5390            authentication: Authentication { protocol: Protocol::Open, credentials: None },
5391        };
5392        (cmd, connect_txn_stream)
5393    }
5394
5395    fn connect_command_two() -> (ConnectCommand, ConnectTransactionStream) {
5396        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5397        let cmd = ConnectCommand {
5398            bss: Box::new(
5399                fake_bss_description!(Open, ssid: Ssid::try_from("bar").unwrap(), bssid: [8, 8, 8, 8, 8, 8]),
5400            ),
5401            connect_txn_sink,
5402            protection: Protection::Open,
5403            authentication: Authentication { protocol: Protocol::Open, credentials: None },
5404        };
5405        (cmd, connect_txn_stream)
5406    }
5407
5408    fn connect_command_wep() -> (ConnectCommand, ConnectTransactionStream) {
5409        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5410        let cmd = ConnectCommand {
5411            bss: Box::new(fake_bss_description!(Wep, ssid: Ssid::try_from("wep").unwrap())),
5412            connect_txn_sink,
5413            protection: Protection::Wep(WepKey::Wep40([3; 5])),
5414            authentication: Authentication { protocol: Protocol::Wep, credentials: None },
5415        };
5416        (cmd, connect_txn_stream)
5417    }
5418
5419    fn connect_command_wpa1(
5420        supplicant: MockSupplicant,
5421    ) -> (ConnectCommand, ConnectTransactionStream) {
5422        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5423        let wpa_ie = make_wpa1_ie();
5424        let cmd = ConnectCommand {
5425            bss: Box::new(fake_bss_description!(Wpa1, ssid: Ssid::try_from("wpa1").unwrap())),
5426            connect_txn_sink,
5427            protection: Protection::LegacyWpa(Rsna {
5428                negotiated_protection: NegotiatedProtection::from_legacy_wpa(&wpa_ie)
5429                    .expect("invalid NegotiatedProtection"),
5430                supplicant: Box::new(supplicant),
5431            }),
5432            authentication: Authentication { protocol: Protocol::Wpa1, credentials: None },
5433        };
5434        (cmd, connect_txn_stream)
5435    }
5436
5437    fn connect_command_wpa2(
5438        supplicant: MockSupplicant,
5439    ) -> (ConnectCommand, ConnectTransactionStream) {
5440        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5441        let bss = fake_bss_description!(Wpa2, ssid: Ssid::try_from("wpa2").unwrap());
5442        let rsne = Rsne::wpa2_rsne();
5443        let credentials = Some(Box::new(Credentials::Wpa(
5444            fidl_security::WpaCredentials::Passphrase("password".into()),
5445        )));
5446        let cmd = ConnectCommand {
5447            bss: Box::new(bss),
5448            connect_txn_sink,
5449            protection: Protection::Rsna(Rsna {
5450                negotiated_protection: NegotiatedProtection::from_rsne(&rsne)
5451                    .expect("invalid NegotiatedProtection"),
5452                supplicant: Box::new(supplicant),
5453            }),
5454            authentication: Authentication { protocol: Protocol::Wpa2Personal, credentials },
5455        };
5456        (cmd, connect_txn_stream)
5457    }
5458
5459    fn connect_command_wpa3(
5460        supplicant: MockSupplicant,
5461    ) -> (ConnectCommand, ConnectTransactionStream) {
5462        let (connect_txn_sink, connect_txn_stream) = ConnectTransactionSink::new_unbounded();
5463        let bss = fake_bss_description!(Wpa3, ssid: Ssid::try_from("wpa3").unwrap());
5464        let rsne = Rsne::wpa3_rsne();
5465        let cmd = ConnectCommand {
5466            bss: Box::new(bss),
5467            connect_txn_sink,
5468            protection: Protection::Rsna(Rsna {
5469                negotiated_protection: NegotiatedProtection::from_rsne(&rsne)
5470                    .expect("invalid NegotiatedProtection"),
5471                supplicant: Box::new(supplicant),
5472            }),
5473            authentication: Authentication { protocol: Protocol::Wpa3Personal, credentials: None },
5474        };
5475        (cmd, connect_txn_stream)
5476    }
5477
5478    fn idle_state() -> ClientState {
5479        testing::new_state(Idle { cfg: ClientConfig::default() }).into()
5480    }
5481
5482    fn assert_idle(state: ClientState) {
5483        assert_matches!(&state, ClientState::Idle(_));
5484    }
5485
5486    fn disconnect(
5487        mut state: ClientState,
5488        h: &mut TestHelper,
5489        reason: fidl_sme::UserDisconnectReason,
5490    ) -> ClientState {
5491        let bssid = match &state {
5492            ClientState::Connecting(state) => state.cmd.bss.bssid,
5493            ClientState::Associated(state) => state.latest_ap_state.bssid,
5494            other => panic!("Unexpected state {other:?} when disconnecting"),
5495        };
5496        let (mut disconnect_fut, responder) = make_disconnect_request(h);
5497        state = state.disconnect(&mut h.context, reason, responder);
5498        assert_matches!(&state, ClientState::Disconnecting(_));
5499        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Pending);
5500        let state = state.on_mlme_event(
5501            fidl_mlme::MlmeEvent::DeauthenticateConf {
5502                resp: fidl_mlme::DeauthenticateConfirm { peer_sta_address: bssid.to_array() },
5503            },
5504            &mut h.context,
5505        );
5506        assert_matches!(h.executor.run_until_stalled(&mut disconnect_fut), Poll::Ready(Ok(())));
5507        state
5508    }
5509
5510    fn connecting_state(cmd: ConnectCommand) -> ClientState {
5511        testing::new_state(Connecting {
5512            cfg: ClientConfig::default(),
5513            cmd,
5514            protection_ie: None,
5515            reassociation_loop_count: 0,
5516        })
5517        .into()
5518    }
5519
5520    fn assert_connecting(state: ClientState, bss: &BssDescription) {
5521        assert_matches!(&state, ClientState::Connecting(connecting) => {
5522            assert_eq!(connecting.cmd.bss.as_ref(), bss);
5523        });
5524    }
5525
5526    fn assert_roaming(state: &ClientState) {
5527        assert_matches!(state, ClientState::Roaming(_));
5528    }
5529
5530    fn assert_disconnecting(state: ClientState) {
5531        assert_matches!(&state, ClientState::Disconnecting(_));
5532    }
5533
5534    fn establishing_rsna_state(cmd: ConnectCommand) -> ClientState {
5535        let auth_method = cmd.protection.rsn_auth_method();
5536        let rsna = assert_matches!(cmd.protection, Protection::Rsna(rsna) => rsna);
5537        let link_state = testing::new_state(EstablishingRsna {
5538            rsna,
5539            rsna_completion_timeout: None,
5540            rsna_response_timeout: None,
5541            rsna_retransmission_timeout: None,
5542            handshake_complete: false,
5543            pending_key_ids: Default::default(),
5544        })
5545        .into();
5546        testing::new_state(Associated {
5547            cfg: ClientConfig::default(),
5548            latest_ap_state: cmd.bss,
5549            auth_method,
5550            connect_txn_sink: cmd.connect_txn_sink,
5551            last_signal_report_time: zx::MonotonicInstant::ZERO,
5552            link_state,
5553            protection_ie: None,
5554            wmm_param: None,
5555            last_channel_switch_time: None,
5556            reassociation_loop_count: 0,
5557            authentication: cmd.authentication,
5558            roam_in_progress: None,
5559        })
5560        .into()
5561    }
5562
5563    fn link_up_state(cmd: ConnectCommand) -> ClientState {
5564        link_up_state_with_wmm(cmd, None)
5565    }
5566
5567    fn link_up_state_with_wmm(cmd: ConnectCommand, wmm_param: Option<ie::WmmParam>) -> ClientState {
5568        let auth_method = cmd.protection.rsn_auth_method();
5569        let link_state =
5570            testing::new_state(LinkUp { protection: cmd.protection, since: now() }).into();
5571        testing::new_state(Associated {
5572            cfg: ClientConfig::default(),
5573            connect_txn_sink: cmd.connect_txn_sink,
5574            latest_ap_state: cmd.bss,
5575            auth_method,
5576            last_signal_report_time: zx::MonotonicInstant::ZERO,
5577            link_state,
5578            protection_ie: None,
5579            wmm_param,
5580            last_channel_switch_time: None,
5581            reassociation_loop_count: 0,
5582            authentication: cmd.authentication,
5583            roam_in_progress: None,
5584        })
5585        .into()
5586    }
5587
5588    fn roaming_state(cmd: ConnectCommand, selected_bssid: Bssid) -> ClientState {
5589        let auth_method = cmd.protection.rsn_auth_method();
5590        let mut selected_bss = cmd.bss.clone();
5591        selected_bss.bssid = selected_bssid;
5592        testing::new_state(Roaming {
5593            cfg: ClientConfig::default(),
5594            cmd: ConnectCommand {
5595                bss: selected_bss,
5596                connect_txn_sink: cmd.connect_txn_sink,
5597                protection: cmd.protection,
5598                authentication: cmd.authentication,
5599            },
5600            auth_method,
5601            protection_ie: None,
5602        })
5603        .into()
5604    }
5605
5606    fn disconnecting_state(action: PostDisconnectAction) -> ClientState {
5607        testing::new_state(Disconnecting { cfg: ClientConfig::default(), action, _timeout: None })
5608            .into()
5609    }
5610
5611    fn fake_device_info() -> fidl_mlme::DeviceInfo {
5612        test_utils::fake_device_info([0, 1, 2, 3, 4, 5].into())
5613    }
5614}