wlan_sme/client/state/
mod.rs

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