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