| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| mod link_state; |
| |
| use { |
| crate::{ |
| capabilities::{intersect_with_ap_as_client, ClientCapabilities}, |
| client::{ |
| bss::ClientConfig, |
| capabilities::derive_join_channel_and_capabilities, |
| event::{self, Event}, |
| info::{DisconnectInfo, DisconnectSource}, |
| internal::Context, |
| protection::{build_protection_ie, Protection, ProtectionIe}, |
| report_connect_finished, AssociationFailure, ConnectFailure, ConnectResult, |
| EstablishRsnaFailure, EstablishRsnaFailureReason, Status, |
| }, |
| clone_utils::clone_bss_desc, |
| phy_selection::derive_phy_cbw, |
| responder::Responder, |
| sink::MlmeSink, |
| timer::EventId, |
| MlmeRequest, |
| }, |
| anyhow::bail, |
| fidl_fuchsia_wlan_mlme::{self as fidl_mlme, BssDescription, MlmeEvent}, |
| fuchsia_inspect_contrib::{inspect_log, log::InspectBytes}, |
| fuchsia_zircon as zx, |
| link_state::LinkState, |
| log::{error, info, warn}, |
| static_assertions::assert_eq_size, |
| std::convert::TryInto, |
| wep_deprecated, |
| wlan_common::{ |
| bss::BssDescriptionExt, |
| channel::Channel, |
| format::MacFmt, |
| ie::{ |
| self, |
| rsn::{akm, cipher}, |
| }, |
| mac::Bssid, |
| RadioConfig, |
| }, |
| wlan_rsn::{ |
| auth, |
| rsna::{AuthStatus, SecAssocUpdate, UpdateSink}, |
| }, |
| wlan_statemachine::*, |
| zerocopy::AsBytes, |
| }; |
| const DEFAULT_JOIN_FAILURE_TIMEOUT: u32 = 20; // beacon intervals |
| const DEFAULT_AUTH_FAILURE_TIMEOUT: u32 = 20; // beacon intervals |
| |
| const IDLE_STATE: &str = "IdleState"; |
| const JOINING_STATE: &str = "JoiningState"; |
| const AUTHENTICATING_STATE: &str = "AuthenticatingState"; |
| const ASSOCIATING_STATE: &str = "AssociatingState"; |
| const RSNA_STATE: &str = "EstablishingRsnaState"; |
| const LINK_UP_STATE: &str = "LinkUpState"; |
| |
| #[derive(Debug)] |
| pub struct ConnectCommand { |
| pub bss: Box<BssDescription>, |
| pub responder: Option<Responder<ConnectResult>>, |
| pub protection: Protection, |
| pub radio_cfg: RadioConfig, |
| } |
| |
| #[derive(Debug)] |
| pub struct Idle { |
| cfg: ClientConfig, |
| } |
| |
| #[derive(Debug)] |
| pub struct Joining { |
| cfg: ClientConfig, |
| cmd: ConnectCommand, |
| chan: Channel, |
| cap: Option<ClientCapabilities>, |
| protection_ie: Option<ProtectionIe>, |
| } |
| |
| #[derive(Debug)] |
| pub struct Authenticating { |
| cfg: ClientConfig, |
| cmd: ConnectCommand, |
| chan: Channel, |
| cap: Option<ClientCapabilities>, |
| protection_ie: Option<ProtectionIe>, |
| } |
| |
| #[derive(Debug)] |
| pub struct Associating { |
| cfg: ClientConfig, |
| cmd: ConnectCommand, |
| chan: Channel, |
| cap: Option<ClientCapabilities>, |
| protection_ie: Option<ProtectionIe>, |
| } |
| |
| #[derive(Debug)] |
| pub struct Associated { |
| cfg: ClientConfig, |
| responder: Option<Responder<ConnectResult>>, |
| bss: Box<BssDescription>, |
| auth_method: Option<auth::MethodName>, |
| last_rssi: i8, |
| last_snr: i8, |
| link_state: LinkState, |
| radio_cfg: RadioConfig, |
| chan: Channel, |
| cap: Option<ClientCapabilities>, |
| protection_ie: Option<ProtectionIe>, |
| wmm_param: Option<ie::WmmParam>, |
| last_channel_switch_time: Option<zx::Time>, |
| } |
| |
| statemachine!( |
| #[derive(Debug)] |
| pub enum ClientState, |
| () => Idle, |
| Idle => Joining, |
| Joining => [Authenticating, Idle], |
| Authenticating => [Associating, Idle], |
| Associating => [Associated, Idle], |
| // We transition back to Associating on a disassociation ind. |
| Associated => [Idle, Associating], |
| ); |
| |
| /// Context surrounding the state change, for Inspect logging |
| pub enum StateChangeContext { |
| Disconnect { |
| msg: String, |
| reason_code: u16, |
| /// True if disconnect is initiated within the device. |
| /// False if disconnect happens due to frame sent by AP. |
| locally_initiated: bool, |
| }, |
| Msg(String), |
| } |
| |
| trait StateChangeContextExt { |
| fn set_msg(&mut self, msg: String); |
| } |
| |
| impl StateChangeContextExt for Option<StateChangeContext> { |
| fn set_msg(&mut self, msg: String) { |
| match self { |
| Some(ctx) => match ctx { |
| StateChangeContext::Disconnect { msg: ref mut inner, .. } => *inner = msg, |
| StateChangeContext::Msg(inner) => *inner = msg, |
| }, |
| None => { |
| self.replace(StateChangeContext::Msg(msg)); |
| } |
| } |
| } |
| } |
| |
| impl Joining { |
| fn on_join_conf( |
| self, |
| conf: fidl_mlme::JoinConfirm, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Authenticating, Idle> { |
| match conf.result_code { |
| fidl_mlme::JoinResultCodes::Success => { |
| context.info.report_auth_started(); |
| if let Protection::Wep(ref key) = self.cmd.protection { |
| install_wep_key(context, self.cmd.bss.bssid.clone(), key); |
| context.mlme_sink.send(MlmeRequest::Authenticate( |
| wep_deprecated::make_mlme_authenticate_request( |
| self.cmd.bss.bssid.clone(), |
| DEFAULT_AUTH_FAILURE_TIMEOUT, |
| ), |
| )); |
| } else { |
| let auth_type = match &self.cmd.protection { |
| Protection::Rsna(rsna) => match rsna.negotiated_protection.akm.suite_type { |
| akm::SAE => fidl_mlme::AuthenticationTypes::Sae, |
| _ => fidl_mlme::AuthenticationTypes::OpenSystem, |
| }, |
| _ => fidl_mlme::AuthenticationTypes::OpenSystem, |
| }; |
| context.mlme_sink.send(MlmeRequest::Authenticate( |
| fidl_mlme::AuthenticateRequest { |
| peer_sta_address: self.cmd.bss.bssid.clone(), |
| auth_type, |
| auth_failure_timeout: DEFAULT_AUTH_FAILURE_TIMEOUT, |
| sae_password: None, |
| }, |
| )); |
| } |
| state_change_ctx.set_msg("successful join".to_string()); |
| Ok(Authenticating { |
| cfg: self.cfg, |
| cmd: self.cmd, |
| chan: self.chan, |
| cap: self.cap, |
| protection_ie: self.protection_ie, |
| }) |
| } |
| other => { |
| error!("Join request failed with result code {:?}", other); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed(ConnectFailure::JoinFailure(other)), |
| ); |
| state_change_ctx.set_msg(format!("join failed; result code: {:?}", other)); |
| Err(Idle { cfg: self.cfg }) |
| } |
| } |
| } |
| } |
| |
| impl Authenticating { |
| fn on_authenticate_conf( |
| self, |
| conf: fidl_mlme::AuthenticateConfirm, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Associating, Idle> { |
| match conf.result_code { |
| fidl_mlme::AuthenticateResultCodes::Success => { |
| context.info.report_assoc_started(); |
| send_mlme_assoc_req( |
| Bssid(self.cmd.bss.bssid.clone()), |
| self.cap.as_ref(), |
| &self.protection_ie, |
| &context.mlme_sink, |
| ); |
| state_change_ctx.set_msg("successful authentication".to_string()); |
| Ok(Associating { |
| cfg: self.cfg, |
| cmd: self.cmd, |
| chan: self.chan, |
| cap: self.cap, |
| |
| protection_ie: self.protection_ie, |
| }) |
| } |
| other => { |
| error!("Authenticate request failed with result code {:?}", other); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed(ConnectFailure::AuthenticationFailure(other)), |
| ); |
| state_change_ctx.set_msg(format!("auth failed; result code: {:?}", other)); |
| Err(Idle { cfg: self.cfg }) |
| } |
| } |
| } |
| |
| fn on_deauthenticate_ind( |
| self, |
| ind: fidl_mlme::DeauthenticateIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Idle { |
| error!( |
| "authentication request failed due to spurious deauthentication: {:?}", |
| ind.reason_code |
| ); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed(ConnectFailure::AuthenticationFailure( |
| fidl_mlme::AuthenticateResultCodes::Refused, |
| )), |
| ); |
| state_change_ctx.replace(StateChangeContext::Disconnect { |
| msg: format!( |
| "received DeauthenticateInd msg while authenticating; reason code {:?}", |
| ind.reason_code |
| ), |
| reason_code: ind.reason_code.into_primitive(), |
| locally_initiated: ind.locally_initiated, |
| }); |
| Idle { cfg: self.cfg } |
| } |
| |
| // Sae management functions |
| |
| fn on_sae_handshake_ind( |
| &mut self, |
| ind: fidl_mlme::SaeHandshakeIndication, |
| context: &mut Context, |
| ) -> Result<(), anyhow::Error> { |
| let supplicant = match &mut self.cmd.protection { |
| Protection::Rsna(rsna) => &mut rsna.supplicant, |
| _ => bail!("Unexpected SAE handshake indication"), |
| }; |
| |
| let mut updates = UpdateSink::default(); |
| supplicant.on_sae_handshake_ind(&mut updates)?; |
| process_sae_updates(updates, ind.peer_sta_address, context); |
| Ok(()) |
| } |
| |
| fn on_sae_frame_rx( |
| &mut self, |
| frame: fidl_mlme::SaeFrame, |
| context: &mut Context, |
| ) -> Result<(), anyhow::Error> { |
| let peer_sta_address = frame.peer_sta_address.clone(); |
| let supplicant = match &mut self.cmd.protection { |
| Protection::Rsna(rsna) => &mut rsna.supplicant, |
| _ => bail!("Unexpected SAE frame recieved"), |
| }; |
| |
| let mut updates = UpdateSink::default(); |
| supplicant.on_sae_frame_rx(&mut updates, frame)?; |
| process_sae_updates(updates, peer_sta_address, context); |
| Ok(()) |
| } |
| |
| fn handle_timeout( |
| mut self, |
| _event_id: EventId, |
| event: Event, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Self, Idle> { |
| match event { |
| Event::SaeTimeout(timer) => { |
| let supplicant = match &mut self.cmd.protection { |
| Protection::Rsna(rsna) => &mut rsna.supplicant, |
| _ => return Ok(self), |
| }; |
| |
| let mut updates = UpdateSink::default(); |
| if let Err(e) = supplicant.on_sae_timeout(&mut updates, timer.0) { |
| // An error in handling a timeout means that we may have no way to abort a |
| // failed handshake. Drop to idle. |
| state_change_ctx.set_msg(format!("failed to handle SAE timeout: {:?}", e)); |
| return Err(Idle { cfg: self.cfg }); |
| } |
| process_sae_updates(updates, self.cmd.bss.bssid, context); |
| } |
| _ => (), |
| } |
| Ok(self) |
| } |
| } |
| |
| impl Associating { |
| fn on_associate_conf( |
| self, |
| conf: fidl_mlme::AssociateConfirm, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Associated, Idle> { |
| let auth_method = self.cmd.protection.get_rsn_auth_method(); |
| let wmm_param = |
| conf.wmm_param.as_ref().and_then(|p| match ie::parse_wmm_param(&p.bytes[..]) { |
| Ok(param) => Some(*param), |
| Err(e) => { |
| warn!( |
| "Fail parsing assoc conf WMM param. Bytes: {:?}. Error: {}", |
| &p.bytes[..], |
| e |
| ); |
| None |
| } |
| }); |
| let link_state = match conf.result_code { |
| fidl_mlme::AssociateResultCodes::Success => { |
| context.info.report_assoc_success(context.att_id); |
| if let Some(cap) = self.cap.as_ref() { |
| let negotiated_cap = intersect_with_ap_as_client(cap, &conf.into()); |
| // TODO(eyw): Enable this check once we switch to Rust MLME which populates |
| // associate confirm with IEs. |
| if negotiated_cap.rates.is_empty() { |
| // This is unlikely to happen with any spec-compliant AP. In case the |
| // user somehow decided to connect to a malicious AP, reject and reset. |
| error!( |
| "Associate terminated because AP's capabilities in association \ |
| response is different from beacon" |
| ); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed(AssociationFailure{ |
| bss_protection: self.cmd.bss.get_protection(), |
| code: fidl_mlme::AssociateResultCodes::RefusedCapabilitiesMismatch, |
| }.into()), |
| ); |
| state_change_ctx.set_msg(format!( |
| "failed associating; AP's capabilites changed between beacon and\ |
| association response" |
| )); |
| return Err(Idle { cfg: self.cfg }); |
| } |
| context.mlme_sink.send(MlmeRequest::FinalizeAssociation( |
| negotiated_cap.to_fidl_negotiated_capabilities(&self.chan), |
| )) |
| } |
| |
| match LinkState::new(self.cmd.protection, context) { |
| Ok(link_state) => link_state, |
| Err(failure_reason) => { |
| state_change_ctx.set_msg(format!("failed to initialized LinkState")); |
| send_deauthenticate_request(&self.cmd.bss, &context.mlme_sink); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| EstablishRsnaFailure { auth_method, reason: failure_reason }.into(), |
| ); |
| return Err(Idle { cfg: self.cfg }); |
| } |
| } |
| } |
| other => { |
| error!("Associate request failed with result code {:?}", other); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection: self.cmd.bss.get_protection(), |
| code: other, |
| } |
| .into(), |
| ), |
| ); |
| state_change_ctx.set_msg(format!("failed associating; result code: {:?}", other)); |
| return Err(Idle { cfg: self.cfg }); |
| } |
| }; |
| state_change_ctx.set_msg("successful assoc".to_string()); |
| |
| let mut responder = self.cmd.responder; |
| if let LinkState::LinkUp(_) = link_state { |
| report_connect_finished(responder.take(), context, ConnectResult::Success); |
| } |
| |
| Ok(Associated { |
| cfg: self.cfg, |
| responder, |
| auth_method, |
| last_rssi: self.cmd.bss.rssi_dbm, |
| last_snr: self.cmd.bss.snr_db, |
| bss: self.cmd.bss, |
| link_state, |
| radio_cfg: self.cmd.radio_cfg, |
| chan: self.chan, |
| cap: self.cap, |
| protection_ie: self.protection_ie, |
| wmm_param, |
| last_channel_switch_time: None, |
| }) |
| } |
| |
| fn on_deauthenticate_ind( |
| self, |
| ind: fidl_mlme::DeauthenticateIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Idle { |
| error!( |
| "association request failed due to spurious deauthentication: {:?}", |
| ind.reason_code |
| ); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection: self.cmd.bss.get_protection(), |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| } |
| .into(), |
| ), |
| ); |
| state_change_ctx.replace(StateChangeContext::Disconnect { |
| msg: format!( |
| "received DeauthenticateInd msg while associating; reason code {:?}", |
| ind.reason_code |
| ), |
| reason_code: ind.reason_code.into_primitive(), |
| locally_initiated: ind.locally_initiated, |
| }); |
| Idle { cfg: self.cfg } |
| } |
| |
| fn on_disassociate_ind( |
| self, |
| ind: fidl_mlme::DisassociateIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Idle { |
| error!("association request failed due to spurious disassociation: {:?}", ind.reason_code); |
| report_connect_finished( |
| self.cmd.responder, |
| context, |
| ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection: self.cmd.bss.get_protection(), |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| } |
| .into(), |
| ), |
| ); |
| state_change_ctx.replace(StateChangeContext::Disconnect { |
| msg: format!( |
| "received DisassociateInd msg while associating; reason code {:?}", |
| ind.reason_code |
| ), |
| reason_code: ind.reason_code, |
| locally_initiated: ind.locally_initiated, |
| }); |
| |
| Idle { cfg: self.cfg } |
| } |
| } |
| |
| impl Associated { |
| fn on_disassociate_ind( |
| self, |
| ind: fidl_mlme::DisassociateIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Associating { |
| let (mut protection, connected_duration) = self.link_state.disconnect(); |
| |
| if let Some(duration) = connected_duration { |
| let disconnect_info = DisconnectInfo { |
| connected_duration: duration, |
| last_rssi: self.last_rssi, |
| last_snr: self.last_snr, |
| bssid: self.bss.bssid, |
| ssid: self.bss.ssid.clone(), |
| channel: Channel::from_fidl(self.bss.chan), |
| reason_code: ind.reason_code, |
| disconnect_source: if ind.locally_initiated { |
| DisconnectSource::Mlme |
| } else { |
| DisconnectSource::Ap |
| }, |
| time_since_channel_switch: self.last_channel_switch_time.map(|t| now() - t), |
| }; |
| context.info.report_disconnect(disconnect_info); |
| } |
| |
| // Client is disassociating. The ESS-SA must be kept alive but reset. |
| if let Protection::Rsna(rsna) = &mut protection { |
| rsna.supplicant.reset(); |
| } |
| |
| context.att_id += 1; |
| let cmd = ConnectCommand { |
| bss: self.bss, |
| responder: self.responder, |
| protection, |
| radio_cfg: self.radio_cfg, |
| }; |
| send_mlme_assoc_req( |
| Bssid(cmd.bss.bssid.clone()), |
| self.cap.as_ref(), |
| &self.protection_ie, |
| &context.mlme_sink, |
| ); |
| state_change_ctx.set_msg("received DisassociateInd msg".to_string()); |
| Associating { |
| cfg: self.cfg, |
| cmd, |
| chan: self.chan, |
| cap: self.cap, |
| protection_ie: self.protection_ie, |
| } |
| } |
| |
| fn on_deauthenticate_ind( |
| self, |
| ind: fidl_mlme::DeauthenticateIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Idle { |
| let (_, connected_duration) = self.link_state.disconnect(); |
| match connected_duration { |
| Some(duration) => { |
| let disconnect_info = DisconnectInfo { |
| connected_duration: duration, |
| last_rssi: self.last_rssi, |
| last_snr: self.last_snr, |
| bssid: self.bss.bssid, |
| ssid: self.bss.ssid.clone(), |
| channel: Channel::from_fidl(self.bss.chan), |
| reason_code: ind.reason_code.into_primitive(), |
| disconnect_source: if ind.locally_initiated { |
| DisconnectSource::Mlme |
| } else { |
| DisconnectSource::Ap |
| }, |
| time_since_channel_switch: self.last_channel_switch_time.map(|t| now() - t), |
| }; |
| context.info.report_disconnect(disconnect_info); |
| } |
| None => { |
| let connect_result = EstablishRsnaFailure { |
| auth_method: self.auth_method, |
| reason: EstablishRsnaFailureReason::InternalError, |
| } |
| .into(); |
| report_connect_finished(self.responder, context, connect_result); |
| } |
| } |
| |
| state_change_ctx.replace(StateChangeContext::Disconnect { |
| msg: format!("received DeauthenticateInd msg; reason code {:?}", ind.reason_code), |
| reason_code: ind.reason_code.into_primitive(), |
| locally_initiated: ind.locally_initiated, |
| }); |
| Idle { cfg: self.cfg } |
| } |
| |
| fn on_eapol_ind( |
| self, |
| ind: fidl_mlme::EapolIndication, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Self, Idle> { |
| // Ignore unexpected EAPoL frames. |
| if !self.bss.needs_eapol_exchange() { |
| return Ok(self); |
| } |
| |
| // Reject EAPoL frames from other BSS. |
| if ind.src_addr != self.bss.bssid { |
| let eapol_pdu = &ind.data[..]; |
| inspect_log!(context.inspect.rsn_events.lock(), { |
| rx_eapol_frame: InspectBytes(&eapol_pdu), |
| foreign_bssid: ind.src_addr.to_mac_str(), |
| foreign_bssid_hash: context.inspect.hasher.hash_mac_addr(ind.src_addr), |
| current_bssid: self.bss.bssid.to_mac_str(), |
| current_bssid_hash: context.inspect.hasher.hash_mac_addr(self.bss.bssid), |
| status: "rejected (foreign BSS)", |
| }); |
| return Ok(self); |
| } |
| |
| let link_state = |
| match self.link_state.on_eapol_ind(ind, &self.bss, state_change_ctx, context) { |
| Ok(link_state) => link_state, |
| Err(failure_reason) => { |
| report_connect_finished( |
| self.responder, |
| context, |
| EstablishRsnaFailure { |
| auth_method: self.auth_method, |
| reason: failure_reason, |
| } |
| .into(), |
| ); |
| send_deauthenticate_request(&self.bss, &context.mlme_sink); |
| return Err(Idle { cfg: self.cfg }); |
| } |
| }; |
| |
| let mut responder = self.responder; |
| if let LinkState::LinkUp(_) = link_state { |
| context.info.report_rsna_established(context.att_id); |
| report_connect_finished(responder.take(), context, ConnectResult::Success); |
| } |
| |
| Ok(Self { link_state, responder, ..self }) |
| } |
| |
| fn on_channel_switched(&mut self, info: fidl_mlme::ChannelSwitchInfo) { |
| self.bss.chan.primary = info.new_channel; |
| self.last_channel_switch_time.replace(now()); |
| } |
| |
| fn handle_timeout( |
| self, |
| event_id: EventId, |
| event: Event, |
| state_change_ctx: &mut Option<StateChangeContext>, |
| context: &mut Context, |
| ) -> Result<Self, Idle> { |
| match self.link_state.handle_timeout(event_id, event, state_change_ctx, context) { |
| Ok(link_state) => Ok(Associated { link_state, ..self }), |
| Err(failure_reason) => { |
| report_connect_finished( |
| self.responder, |
| context, |
| EstablishRsnaFailure { auth_method: self.auth_method, reason: failure_reason } |
| .into(), |
| ); |
| send_deauthenticate_request(&self.bss, &context.mlme_sink); |
| Err(Idle { cfg: self.cfg }) |
| } |
| } |
| } |
| } |
| |
| impl ClientState { |
| pub fn new(cfg: ClientConfig) -> Self { |
| Self::from(State::new(Idle { cfg })) |
| } |
| |
| fn state_name(&self) -> &'static str { |
| match self { |
| Self::Idle(_) => IDLE_STATE, |
| Self::Joining(_) => JOINING_STATE, |
| Self::Authenticating(_) => AUTHENTICATING_STATE, |
| Self::Associating(_) => ASSOCIATING_STATE, |
| Self::Associated(state) => match state.link_state { |
| LinkState::EstablishingRsna(_) => RSNA_STATE, |
| LinkState::LinkUp(_) => LINK_UP_STATE, |
| _ => unreachable!(), |
| }, |
| } |
| } |
| |
| pub fn on_mlme_event(self, event: MlmeEvent, context: &mut Context) -> Self { |
| let start_state = self.state_name(); |
| let mut state_change_ctx: Option<StateChangeContext> = None; |
| |
| let new_state = match self { |
| Self::Idle(_) => { |
| warn!("Unexpected MLME message while Idle: {:?}", event); |
| self |
| } |
| Self::Joining(state) => match event { |
| MlmeEvent::JoinConf { resp } => { |
| let (transition, joining) = state.release_data(); |
| match joining.on_join_conf(resp, &mut state_change_ctx, context) { |
| Ok(authenticating) => transition.to(authenticating).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| _ => state.into(), |
| }, |
| Self::Authenticating(state) => match event { |
| MlmeEvent::AuthenticateConf { resp } => { |
| let (transition, authenticating) = state.release_data(); |
| match authenticating.on_authenticate_conf(resp, &mut state_change_ctx, context) |
| { |
| Ok(associating) => transition.to(associating).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| MlmeEvent::OnSaeHandshakeInd { ind } => { |
| let (transition, mut authenticating) = state.release_data(); |
| if let Err(e) = authenticating.on_sae_handshake_ind(ind, context) { |
| error!("Failed to process SaeHandshakeInd: {:?}", e); |
| } |
| transition.to(authenticating).into() |
| } |
| MlmeEvent::OnSaeFrameRx { frame } => { |
| let (transition, mut authenticating) = state.release_data(); |
| if let Err(e) = authenticating.on_sae_frame_rx(frame, context) { |
| error!("Failed to process SaeFrameRx: {:?}", e); |
| } |
| transition.to(authenticating).into() |
| } |
| MlmeEvent::DeauthenticateInd { ind } => { |
| let (transition, authenticating) = state.release_data(); |
| let idle = |
| authenticating.on_deauthenticate_ind(ind, &mut state_change_ctx, context); |
| transition.to(idle).into() |
| } |
| _ => state.into(), |
| }, |
| Self::Associating(state) => match event { |
| MlmeEvent::AssociateConf { resp } => { |
| let (transition, associating) = state.release_data(); |
| match associating.on_associate_conf(resp, &mut state_change_ctx, context) { |
| Ok(associated) => transition.to(associated).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| MlmeEvent::DeauthenticateInd { ind } => { |
| let (transition, associating) = state.release_data(); |
| let idle = |
| associating.on_deauthenticate_ind(ind, &mut state_change_ctx, context); |
| transition.to(idle).into() |
| } |
| MlmeEvent::DisassociateInd { ind } => { |
| let (transition, associating) = state.release_data(); |
| let idle = associating.on_disassociate_ind(ind, &mut state_change_ctx, context); |
| transition.to(idle).into() |
| } |
| _ => state.into(), |
| }, |
| Self::Associated(mut state) => match event { |
| MlmeEvent::DisassociateInd { ind } => { |
| let (transition, associated) = state.release_data(); |
| let associating = |
| associated.on_disassociate_ind(ind, &mut state_change_ctx, context); |
| transition.to(associating).into() |
| } |
| MlmeEvent::DeauthenticateInd { ind } => { |
| let (transition, associated) = state.release_data(); |
| let idle = |
| associated.on_deauthenticate_ind(ind, &mut state_change_ctx, context); |
| transition.to(idle).into() |
| } |
| MlmeEvent::SignalReport { ind } => { |
| state.last_rssi = ind.rssi_dbm; |
| state.last_snr = ind.snr_db; |
| state.into() |
| } |
| MlmeEvent::EapolInd { ind } => { |
| let (transition, associated) = state.release_data(); |
| match associated.on_eapol_ind(ind, &mut state_change_ctx, context) { |
| Ok(associated) => transition.to(associated).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| MlmeEvent::OnChannelSwitched { info } => { |
| state.on_channel_switched(info); |
| state.into() |
| } |
| _ => state.into(), |
| }, |
| }; |
| |
| log_state_change(start_state, &new_state, state_change_ctx, context); |
| new_state |
| } |
| |
| pub fn handle_timeout(self, event_id: EventId, event: Event, context: &mut Context) -> Self { |
| let start_state = self.state_name(); |
| let mut state_change_ctx: Option<StateChangeContext> = None; |
| |
| let new_state = match self { |
| Self::Authenticating(state) => { |
| let (transition, authenticating) = state.release_data(); |
| match authenticating.handle_timeout(event_id, event, &mut state_change_ctx, context) |
| { |
| Ok(authenticating) => transition.to(authenticating).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| Self::Associated(state) => { |
| let (transition, associated) = state.release_data(); |
| match associated.handle_timeout(event_id, event, &mut state_change_ctx, context) { |
| Ok(associated) => transition.to(associated).into(), |
| Err(idle) => transition.to(idle).into(), |
| } |
| } |
| _ => self, |
| }; |
| |
| log_state_change(start_state, &new_state, state_change_ctx, context); |
| new_state |
| } |
| |
| pub fn connect(self, cmd: ConnectCommand, context: &mut Context) -> Self { |
| let (chan, cap) = match derive_join_channel_and_capabilities( |
| Channel::from_fidl(cmd.bss.chan), |
| cmd.radio_cfg.cbw, |
| &cmd.bss.rates[..], |
| &context.device_info, |
| ) { |
| Ok(chan_and_cap) => chan_and_cap, |
| Err(e) => { |
| error!("Failed building join capabilities: {}", e); |
| return self; |
| } |
| }; |
| |
| let cap = if context.is_softmac { Some(cap) } else { None }; |
| |
| // Derive RSN (for WPA2) or Vendor IEs (for WPA1) or neither(WEP/non-protected). |
| let protection_ie = match build_protection_ie(&cmd.protection) { |
| Ok(ie) => ie, |
| Err(e) => { |
| error!("Failed to build protection IEs: {}", e); |
| return self; |
| } |
| }; |
| |
| let start_state = self.state_name(); |
| let cfg = self.disconnect_internal(context); |
| |
| let mut selected_bss = clone_bss_desc(&cmd.bss); |
| let (phy_to_use, cbw_to_use) = |
| derive_phy_cbw(&selected_bss, &context.device_info, &cmd.radio_cfg); |
| selected_bss.chan.cbw = cbw_to_use; |
| |
| context.mlme_sink.send(MlmeRequest::Join(fidl_mlme::JoinRequest { |
| selected_bss, |
| join_failure_timeout: DEFAULT_JOIN_FAILURE_TIMEOUT, |
| nav_sync_delay: 0, |
| op_rates: vec![], |
| phy: phy_to_use, |
| cbw: cbw_to_use, |
| })); |
| context.att_id += 1; |
| |
| let msg = connect_cmd_inspect_summary(&cmd); |
| inspect_log!(context.inspect.state_events.lock(), { |
| from: start_state, |
| to: JOINING_STATE, |
| ctx: msg, |
| bssid: cmd.bss.bssid.to_mac_str(), |
| bssid_hash: context.inspect.hasher.hash_mac_addr(cmd.bss.bssid), |
| ssid: String::from_utf8_lossy(&cmd.bss.ssid[..]).as_ref(), |
| ssid_hash: context.inspect.hasher.hash(&cmd.bss.ssid[..]), |
| }); |
| let state = Self::new(cfg.clone()); |
| match state { |
| Self::Idle(state) => { |
| state.transition_to(Joining { cfg, cmd, chan, cap, protection_ie }).into() |
| } |
| _ => unreachable!(), |
| } |
| } |
| |
| pub fn disconnect(self, context: &mut Context) -> Self { |
| let reason_code = fidl_mlme::ReasonCode::LeavingNetworkDeauth.into_primitive(); |
| let locally_initiated = true; |
| if let Self::Associated(state) = &self { |
| if let LinkState::LinkUp(link_up) = &state.link_state { |
| let disconnect_info = DisconnectInfo { |
| connected_duration: link_up.connected_duration(), |
| last_rssi: state.last_rssi, |
| last_snr: state.last_snr, |
| bssid: state.bss.bssid, |
| ssid: state.bss.ssid.clone(), |
| channel: Channel::from_fidl(state.bss.chan), |
| reason_code, |
| disconnect_source: DisconnectSource::User, |
| time_since_channel_switch: state.last_channel_switch_time.map(|t| now() - t), |
| }; |
| context.info.report_disconnect(disconnect_info); |
| } |
| } |
| let start_state = self.state_name(); |
| let new_state = Self::new(self.disconnect_internal(context)); |
| |
| let state_change_ctx = Some(StateChangeContext::Disconnect { |
| msg: "disconnect command".to_string(), |
| reason_code, |
| locally_initiated, |
| }); |
| log_state_change(start_state, &new_state, state_change_ctx, context); |
| new_state |
| } |
| |
| fn disconnect_internal(self, context: &mut Context) -> ClientConfig { |
| match self { |
| Self::Idle(state) => state.cfg, |
| Self::Joining(state) => { |
| let (_, state) = state.release_data(); |
| report_connect_finished(state.cmd.responder, context, ConnectResult::Canceled); |
| state.cfg |
| } |
| Self::Authenticating(state) => { |
| let (_, state) = state.release_data(); |
| report_connect_finished(state.cmd.responder, context, ConnectResult::Canceled); |
| state.cfg |
| } |
| Self::Associating(state) => { |
| let (_, state) = state.release_data(); |
| report_connect_finished(state.cmd.responder, context, ConnectResult::Canceled); |
| send_deauthenticate_request(&state.cmd.bss, &context.mlme_sink); |
| state.cfg |
| } |
| Self::Associated(state) => { |
| send_deauthenticate_request(&state.bss, &context.mlme_sink); |
| state.cfg |
| } |
| } |
| } |
| |
| // Cancel any connect that is in progress. No-op if client is already idle or connected. |
| pub fn cancel_ongoing_connect(self, context: &mut Context) -> Self { |
| // Only move to idle if client is not already connected. Technically, SME being in |
| // transition state does not necessarily mean that a (manual) connect attempt is |
| // in progress (since DisassociateInd moves SME to transition state). However, the |
| // main thing we are concerned about is that we don't disconnect from an already |
| // connected state until the new connect attempt succeeds in selecting BSS. |
| if self.in_transition_state() { |
| Self::new(self.disconnect_internal(context)) |
| } else { |
| self |
| } |
| } |
| |
| fn in_transition_state(&self) -> bool { |
| match self { |
| Self::Idle(_) => false, |
| Self::Associated(state) => match state.link_state { |
| LinkState::LinkUp { .. } => false, |
| _ => true, |
| }, |
| _ => true, |
| } |
| } |
| |
| pub fn status(&self) -> Status { |
| match self { |
| Self::Idle(_) => Status { connected_to: None, connecting_to: None }, |
| Self::Joining(joining) => { |
| Status { connected_to: None, connecting_to: Some(joining.cmd.bss.ssid.clone()) } |
| } |
| Self::Authenticating(authenticating) => Status { |
| connected_to: None, |
| connecting_to: Some(authenticating.cmd.bss.ssid.clone()), |
| }, |
| Self::Associating(associating) => { |
| Status { connected_to: None, connecting_to: Some(associating.cmd.bss.ssid.clone()) } |
| } |
| Self::Associated(associated) => match associated.link_state { |
| LinkState::EstablishingRsna { .. } => { |
| Status { connected_to: None, connecting_to: Some(associated.bss.ssid.clone()) } |
| } |
| LinkState::LinkUp { .. } => Status { |
| connected_to: { |
| let mut bss = associated |
| .cfg |
| .convert_bss_description(&associated.bss, associated.wmm_param); |
| bss.rx_dbm = associated.last_rssi; |
| bss.snr_db = associated.last_snr; |
| Some(bss) |
| }, |
| connecting_to: None, |
| }, |
| _ => unreachable!(), |
| }, |
| } |
| } |
| } |
| |
| fn process_sae_updates(updates: UpdateSink, peer_sta_address: [u8; 6], context: &mut Context) { |
| for update in updates { |
| match update { |
| SecAssocUpdate::TxSaeFrame(frame) => { |
| context.mlme_sink.send(MlmeRequest::SaeFrameTx(frame)); |
| } |
| SecAssocUpdate::SaeAuthStatus(status) => context.mlme_sink.send( |
| MlmeRequest::SaeHandshakeResp(fidl_mlme::SaeHandshakeResponse { |
| peer_sta_address, |
| result_code: match status { |
| AuthStatus::Success => fidl_mlme::AuthenticateResultCodes::Success, |
| AuthStatus::Rejected => fidl_mlme::AuthenticateResultCodes::Refused, |
| AuthStatus::InternalError => fidl_mlme::AuthenticateResultCodes::Refused, |
| }, |
| }), |
| ), |
| SecAssocUpdate::ScheduleSaeTimeout(id) => { |
| context.timer.schedule(event::SaeTimeout(id)); |
| } |
| _ => (), |
| } |
| } |
| } |
| |
| fn log_state_change( |
| start_state: &str, |
| new_state: &ClientState, |
| state_change_ctx: Option<StateChangeContext>, |
| context: &mut Context, |
| ) { |
| if start_state == new_state.state_name() && state_change_ctx.is_none() { |
| return; |
| } |
| |
| match state_change_ctx { |
| Some(inner) => match inner { |
| // Only log the `disconnect_ctx` if an operation had an effect of moving from |
| // non-idle state to idle state. This is so that the client that consumes |
| // `disconnect_ctx` does not log a disconnect event when it's effectively no-op. |
| StateChangeContext::Disconnect { msg, reason_code, locally_initiated } |
| if start_state != IDLE_STATE => |
| { |
| info!( |
| "{} => {}, ctx: `{}`, locally_initiated: {}", |
| start_state, |
| new_state.state_name(), |
| msg, |
| locally_initiated |
| ); |
| |
| inspect_log!(context.inspect.state_events.lock(), { |
| from: start_state, |
| to: new_state.state_name(), |
| ctx: msg, |
| disconnect_ctx: { |
| reason_code: reason_code, |
| locally_initiated: locally_initiated, |
| } |
| }); |
| } |
| StateChangeContext::Disconnect { msg, .. } | StateChangeContext::Msg(msg) => { |
| inspect_log!(context.inspect.state_events.lock(), { |
| from: start_state, |
| to: new_state.state_name(), |
| ctx: msg, |
| }); |
| } |
| }, |
| None => { |
| inspect_log!(context.inspect.state_events.lock(), { |
| from: start_state, |
| to: new_state.state_name(), |
| }); |
| } |
| } |
| } |
| |
| fn install_wep_key(context: &mut Context, bssid: [u8; 6], key: &wep_deprecated::Key) { |
| let cipher_suite = match key { |
| wep_deprecated::Key::Bits40(_) => cipher::WEP_40, |
| wep_deprecated::Key::Bits104(_) => cipher::WEP_104, |
| }; |
| // unwrap() is safe, OUI is defined in RSN and always compatible with ciphers. |
| let cipher = cipher::Cipher::new_dot11(cipher_suite); |
| inspect_log!(context.inspect.rsn_events.lock(), { |
| derived_key: "WEP", |
| cipher: format!("{:?}", cipher), |
| key_index: 0, |
| }); |
| context |
| .mlme_sink |
| .send(MlmeRequest::SetKeys(wep_deprecated::make_mlme_set_keys_request(bssid, key))); |
| } |
| |
| /// Custom logging for ConnectCommand because its normal full debug string is too large, and we |
| /// want to reduce how much we log in memory for Inspect. Additionally, in the future, we'd need |
| /// to anonymize information like BSSID and SSID. |
| fn connect_cmd_inspect_summary(cmd: &ConnectCommand) -> String { |
| let bss = &cmd.bss; |
| format!( |
| "ConnectCmd {{ \ |
| cap: {cap:?}, rates: {rates:?}, \ |
| protected: {protected:?}, chan: {chan:?}, \ |
| rcpi: {rcpi:?}, rsni: {rsni:?}, rssi: {rssi:?}, ht_cap: {ht_cap:?}, ht_op: {ht_op:?}, \ |
| vht_cap: {vht_cap:?}, vht_op: {vht_op:?} }}", |
| cap = bss.cap, |
| rates = bss.rates, |
| protected = bss.rsne.is_some(), |
| chan = bss.chan, |
| rcpi = bss.rcpi_dbmh, |
| rsni = bss.rsni_dbh, |
| rssi = bss.rssi_dbm, |
| ht_cap = bss.ht_cap.is_some(), |
| ht_op = bss.ht_op.is_some(), |
| vht_cap = bss.vht_cap.is_some(), |
| vht_op = bss.vht_op.is_some() |
| ) |
| } |
| |
| fn send_deauthenticate_request(current_bss: &BssDescription, mlme_sink: &MlmeSink) { |
| mlme_sink.send(MlmeRequest::Deauthenticate(fidl_mlme::DeauthenticateRequest { |
| peer_sta_address: current_bss.bssid.clone(), |
| reason_code: fidl_mlme::ReasonCode::StaLeaving, |
| })); |
| } |
| |
| fn send_mlme_assoc_req( |
| bssid: Bssid, |
| capabilities: Option<&ClientCapabilities>, |
| protection_ie: &Option<ProtectionIe>, |
| mlme_sink: &MlmeSink, |
| ) { |
| assert_eq_size!(ie::HtCapabilities, [u8; fidl_mlme::HT_CAP_LEN as usize]); |
| let ht_cap = capabilities.map_or(None, |c| { |
| c.0.ht_cap.map(|h| fidl_mlme::HtCapabilities { bytes: h.as_bytes().try_into().unwrap() }) |
| }); |
| |
| assert_eq_size!(ie::VhtCapabilities, [u8; fidl_mlme::VHT_CAP_LEN as usize]); |
| let vht_cap = capabilities.map_or(None, |c| { |
| c.0.vht_cap.map(|v| fidl_mlme::VhtCapabilities { bytes: v.as_bytes().try_into().unwrap() }) |
| }); |
| let (rsne, vendor_ies) = match protection_ie.as_ref() { |
| Some(ProtectionIe::Rsne(vec)) => (Some(vec.to_vec()), None), |
| Some(ProtectionIe::VendorIes(vec)) => (None, Some(vec.to_vec())), |
| None => (None, None), |
| }; |
| let req = fidl_mlme::AssociateRequest { |
| peer_sta_address: bssid.0, |
| cap_info: capabilities.map_or(0, |c| c.0.cap_info.raw()), |
| rates: capabilities.map_or_else(|| vec![], |c| c.0.rates.as_bytes().to_vec()), |
| // TODO(fxbug.dev/43938): populate `qos_capable` field from device info |
| qos_capable: ht_cap.is_some(), |
| qos_info: 0, |
| ht_cap: ht_cap.map(Box::new), |
| vht_cap: vht_cap.map(Box::new), |
| rsne, |
| vendor_ies, |
| }; |
| mlme_sink.send(MlmeRequest::Associate(req)) |
| } |
| |
| fn now() -> zx::Time { |
| zx::Time::get_monotonic() |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use anyhow::format_err; |
| use fuchsia_inspect::Inspector; |
| use futures::channel::{mpsc, oneshot}; |
| use link_state::{EstablishingRsna, LinkUp}; |
| use std::sync::Arc; |
| use wlan_common::{assert_variant, ie::rsn::rsne::Rsne, RadioConfig}; |
| use wlan_rsn::{key::exchange::Key, rsna::SecAssocStatus}; |
| use wlan_rsn::{ |
| rsna::{SecAssocUpdate, UpdateSink}, |
| NegotiatedProtection, |
| }; |
| |
| use crate::client::test_utils::{ |
| create_assoc_conf, create_auth_conf, create_join_conf, expect_stream_empty, |
| fake_negotiated_channel_and_capabilities, fake_protected_bss_description, |
| fake_unprotected_bss_description, fake_wep_bss_description, fake_wmm_param, |
| fake_wpa1_bss_description, mock_psk_supplicant, MockSupplicant, MockSupplicantController, |
| }; |
| use crate::client::{info::InfoReporter, inspect, rsn::Rsna, InfoEvent, InfoSink, TimeStream}; |
| use crate::test_utils::make_wpa1_ie; |
| |
| use crate::{test_utils, timer, InfoStream, MlmeStream, Ssid}; |
| |
| #[test] |
| fn associate_happy_path_unprotected() { |
| let mut h = TestHelper::new(); |
| |
| let state = idle_state(); |
| let (command, receiver) = connect_command_one(); |
| let bss_ssid = command.bss.ssid.clone(); |
| let bssid = command.bss.bssid.clone(); |
| |
| // Issue a "connect" command |
| let state = state.connect(command, &mut h.context); |
| |
| expect_join_request(&mut h.mlme_stream, &bss_ssid); |
| |
| // (mlme->sme) Send a JoinConf as a response |
| let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success); |
| let state = state.on_mlme_event(join_conf, &mut h.context); |
| |
| expect_auth_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AuthenticateConf as a response |
| let auth_conf = |
| create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success); |
| let state = state.on_mlme_event(auth_conf, &mut h.context); |
| |
| expect_assoc_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AssociateConf |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let _state = state.on_mlme_event(assoc_conf, &mut h.context); |
| |
| // User should be notified that we are connected |
| expect_result(receiver, ConnectResult::Success); |
| |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(..)))); |
| } |
| |
| #[test] |
| fn connect_to_wep_network() { |
| let mut h = TestHelper::new(); |
| |
| let state = idle_state(); |
| let (command, receiver) = connect_command_wep(); |
| let bss_ssid = command.bss.ssid.clone(); |
| let bssid = command.bss.bssid.clone(); |
| |
| // Issue a "connect" command |
| let state = state.connect(command, &mut h.context); |
| |
| expect_join_request(&mut h.mlme_stream, &bss_ssid); |
| |
| // (mlme->sme) Send a JoinConf as a response |
| let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success); |
| let state = state.on_mlme_event(join_conf, &mut h.context); |
| |
| // (sme->mlme) Expect an SetKeysRequest |
| expect_set_wep_key(&mut h.mlme_stream, bssid, vec![3; 5]); |
| // (sme->mlme) Expect an AuthenticateRequest |
| assert_variant!(&mut h.mlme_stream.try_next(), |
| Ok(Some(MlmeRequest::Authenticate(req))) => { |
| assert_eq!(fidl_mlme::AuthenticationTypes::SharedKey, req.auth_type); |
| assert_eq!(bssid, req.peer_sta_address); |
| } |
| ); |
| |
| // (mlme->sme) Send an AuthenticateConf as a response |
| let auth_conf = |
| create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success); |
| let state = state.on_mlme_event(auth_conf, &mut h.context); |
| |
| expect_assoc_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AssociateConf |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let _state = state.on_mlme_event(assoc_conf, &mut h.context); |
| |
| // User should be notified that we are connected |
| expect_result(receiver, ConnectResult::Success); |
| |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(..)))); |
| } |
| |
| #[test] |
| fn connect_to_wpa1_network() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| |
| let state = idle_state(); |
| let (command, receiver) = connect_command_wpa1(supplicant); |
| let bss_ssid = command.bss.ssid.clone(); |
| let bssid = command.bss.bssid.clone(); |
| |
| // Issue a "connect" command |
| let state = state.connect(command, &mut h.context); |
| |
| expect_join_request(&mut h.mlme_stream, &bss_ssid); |
| |
| // (mlme->sme) Send a JoinConf as a response |
| let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success); |
| let state = state.on_mlme_event(join_conf, &mut h.context); |
| |
| expect_auth_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AuthenticateConf as a response |
| let auth_conf = |
| create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success); |
| let state = state.on_mlme_event(auth_conf, &mut h.context); |
| |
| expect_assoc_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AssociateConf |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let state = state.on_mlme_event(assoc_conf, &mut h.context); |
| expect_finalize_association_req( |
| &mut h.mlme_stream, |
| fake_negotiated_channel_and_capabilities(), |
| ); |
| |
| assert!(suppl_mock.is_supplicant_started()); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with key frame |
| let update = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame()); |
| let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| expect_eapol_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with keys |
| let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::wpa1_ptk())); |
| let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::wpa1_gtk())); |
| let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![ptk, gtk]); |
| |
| expect_set_wpa1_ptk(&mut h.mlme_stream, bssid); |
| expect_set_wpa1_gtk(&mut h.mlme_stream); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with completion status |
| let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished); |
| let _state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| expect_set_ctrl_port(&mut h.mlme_stream, bssid, fidl_mlme::ControlledPortState::Open); |
| expect_result(receiver, ConnectResult::Success); |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(..)))); |
| } |
| |
| #[test] |
| fn associate_happy_path_protected() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| |
| let state = idle_state(); |
| let (command, receiver) = connect_command_wpa2(supplicant); |
| let bss_ssid = command.bss.ssid.clone(); |
| let bssid = command.bss.bssid.clone(); |
| |
| // Issue a "connect" command |
| let state = state.connect(command, &mut h.context); |
| |
| expect_join_request(&mut h.mlme_stream, &bss_ssid); |
| |
| // (mlme->sme) Send a JoinConf as a response |
| let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success); |
| let state = state.on_mlme_event(join_conf, &mut h.context); |
| |
| expect_auth_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AuthenticateConf as a response |
| let auth_conf = |
| create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success); |
| let state = state.on_mlme_event(auth_conf, &mut h.context); |
| |
| expect_assoc_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an AssociateConf |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let state = state.on_mlme_event(assoc_conf, &mut h.context); |
| expect_finalize_association_req( |
| &mut h.mlme_stream, |
| fake_negotiated_channel_and_capabilities(), |
| ); |
| |
| assert!(suppl_mock.is_supplicant_started()); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with key frame |
| let update = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame()); |
| let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| expect_eapol_req(&mut h.mlme_stream, bssid); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with keys |
| let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::ptk())); |
| let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk())); |
| let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![ptk, gtk]); |
| |
| expect_set_ptk(&mut h.mlme_stream, bssid); |
| expect_set_gtk(&mut h.mlme_stream); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with completion status |
| let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished); |
| let _state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| expect_set_ctrl_port(&mut h.mlme_stream, bssid, fidl_mlme::ControlledPortState::Open); |
| expect_result(receiver, ConnectResult::Success); |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(..)))); |
| } |
| |
| #[test] |
| fn join_failure() { |
| let mut h = TestHelper::new(); |
| |
| let (cmd, receiver) = connect_command_one(); |
| // Start in a "Joining" state |
| let state = ClientState::from(testing::new_state(Joining { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| })); |
| |
| // (mlme->sme) Send an unsuccessful JoinConf |
| let join_conf = MlmeEvent::JoinConf { |
| resp: fidl_mlme::JoinConfirm { |
| result_code: fidl_mlme::JoinResultCodes::JoinFailureTimeout, |
| }, |
| }; |
| let state = state.on_mlme_event(join_conf, &mut h.context); |
| assert_idle(state); |
| |
| let result = ConnectResult::Failed(ConnectFailure::JoinFailure( |
| fidl_mlme::JoinResultCodes::JoinFailureTimeout, |
| )); |
| // User should be notified that connection attempt failed |
| expect_result(receiver, result.clone()); |
| } |
| |
| #[test] |
| fn authenticate_failure() { |
| let mut h = TestHelper::new(); |
| |
| let (cmd, receiver) = connect_command_one(); |
| |
| // Start in an "Authenticating" state |
| let state = ClientState::from(testing::new_state(Authenticating { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| })); |
| |
| // (mlme->sme) Send an unsuccessful AuthenticateConf |
| let auth_conf = MlmeEvent::AuthenticateConf { |
| resp: fidl_mlme::AuthenticateConfirm { |
| peer_sta_address: connect_command_one().0.bss.bssid, |
| auth_type: fidl_mlme::AuthenticationTypes::OpenSystem, |
| result_code: fidl_mlme::AuthenticateResultCodes::Refused, |
| }, |
| }; |
| let state = state.on_mlme_event(auth_conf, &mut h.context); |
| assert_idle(state); |
| |
| let result = ConnectResult::Failed(ConnectFailure::AuthenticationFailure( |
| fidl_mlme::AuthenticateResultCodes::Refused, |
| )); |
| // User should be notified that connection attempt failed |
| expect_result(receiver, result.clone()); |
| } |
| |
| #[test] |
| fn associate_failure() { |
| let mut h = TestHelper::new(); |
| |
| let (cmd, receiver) = connect_command_one(); |
| let bss_protection = cmd.bss.get_protection(); |
| |
| // Start in an "Associating" state |
| let state = ClientState::from(testing::new_state(Associating { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| })); |
| |
| // (mlme->sme) Send an unsuccessful AssociateConf |
| let assoc_conf = |
| create_assoc_conf(fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified); |
| let state = state.on_mlme_event(assoc_conf, &mut h.context); |
| assert_idle(state); |
| |
| let result = ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection, |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| } |
| .into(), |
| ); |
| // User should be notified that connection attempt failed |
| expect_result(receiver, result.clone()); |
| } |
| |
| #[test] |
| fn connect_while_joining() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let state = joining_state(cmd_one); |
| let (cmd_two, _receiver_two) = connect_command_two(); |
| let state = state.connect(cmd_two, &mut h.context); |
| expect_result(receiver_one, ConnectResult::Canceled); |
| expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid); |
| assert_joining(state, &connect_command_two().0.bss); |
| } |
| |
| #[test] |
| fn connect_while_authenticating() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let state = authenticating_state(cmd_one); |
| let (cmd_two, _receiver_two) = connect_command_two(); |
| let state = state.connect(cmd_two, &mut h.context); |
| expect_result(receiver_one, ConnectResult::Canceled); |
| expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid); |
| assert_joining(state, &connect_command_two().0.bss); |
| } |
| |
| #[test] |
| fn connect_while_associating() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let state = associating_state(cmd_one); |
| let (cmd_two, _receiver_two) = connect_command_two(); |
| let state = state.connect(cmd_two, &mut h.context); |
| let state = exchange_deauth(state, &mut h); |
| expect_result(receiver_one, ConnectResult::Canceled); |
| expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid); |
| assert_joining(state, &connect_command_two().0.bss); |
| } |
| |
| #[test] |
| fn deauth_while_authing() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let state = authenticating_state(cmd_one); |
| let deauth_ind = MlmeEvent::DeauthenticateInd { |
| ind: fidl_mlme::DeauthenticateIndication { |
| peer_sta_address: [7, 7, 7, 7, 7, 7], |
| reason_code: fidl_mlme::ReasonCode::UnspecifiedReason, |
| locally_initiated: false, |
| }, |
| }; |
| let state = state.on_mlme_event(deauth_ind, &mut h.context); |
| expect_result( |
| receiver_one, |
| ConnectResult::Failed(ConnectFailure::AuthenticationFailure( |
| fidl_mlme::AuthenticateResultCodes::Refused, |
| )), |
| ); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn deauth_while_associating() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let bss_protection = cmd_one.bss.get_protection(); |
| let state = associating_state(cmd_one); |
| let deauth_ind = MlmeEvent::DeauthenticateInd { |
| ind: fidl_mlme::DeauthenticateIndication { |
| peer_sta_address: [7, 7, 7, 7, 7, 7], |
| reason_code: fidl_mlme::ReasonCode::UnspecifiedReason, |
| locally_initiated: false, |
| }, |
| }; |
| let state = state.on_mlme_event(deauth_ind, &mut h.context); |
| expect_result( |
| receiver_one, |
| ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection, |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| } |
| .into(), |
| ), |
| ); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn disassoc_while_associating() { |
| let mut h = TestHelper::new(); |
| let (cmd_one, receiver_one) = connect_command_one(); |
| let bss_protection = cmd_one.bss.get_protection(); |
| let state = associating_state(cmd_one); |
| let disassoc_ind = MlmeEvent::DisassociateInd { |
| ind: fidl_mlme::DisassociateIndication { |
| peer_sta_address: [7, 7, 7, 7, 7, 7], |
| reason_code: 42, |
| locally_initiated: false, |
| }, |
| }; |
| let state = state.on_mlme_event(disassoc_ind, &mut h.context); |
| expect_result( |
| receiver_one, |
| ConnectResult::Failed( |
| AssociationFailure { |
| bss_protection, |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| } |
| .into(), |
| ), |
| ); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn supplicant_fails_to_start_while_associating() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let (command, receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| let state = associating_state(command); |
| |
| suppl_mock.set_start_failure(format_err!("failed to start supplicant")); |
| |
| // (mlme->sme) Send an AssociateConf |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let _state = state.on_mlme_event(assoc_conf, &mut h.context); |
| |
| expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving); |
| let result: ConnectResult = EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::StartSupplicantFailed, |
| } |
| .into(); |
| expect_result(receiver, result.clone()); |
| } |
| |
| #[test] |
| fn bad_eapol_frame_while_establishing_rsna() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let (command, mut receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| let state = establishing_rsna_state(command); |
| |
| // doesn't matter what we mock here |
| let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished); |
| suppl_mock.set_on_eapol_frame_results(vec![update]); |
| |
| // (mlme->sme) Send an EapolInd with bad eapol data |
| let eapol_ind = create_eapol_ind(bssid.clone(), vec![1, 2, 3, 4]); |
| let s = state.on_mlme_event(eapol_ind, &mut h.context); |
| |
| assert_eq!(Ok(None), receiver.try_recv()); |
| assert_variant!(s, ClientState::Associated(state) => { |
| assert_variant!(&state.link_state, LinkState::EstablishingRsna { .. })}); |
| |
| expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream"); |
| expect_stream_empty(&mut h.info_stream, "unexpected event in info stream"); |
| } |
| |
| #[test] |
| fn supplicant_fails_to_process_eapol_while_establishing_rsna() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let (command, mut receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| let state = establishing_rsna_state(command); |
| |
| suppl_mock.set_on_eapol_frame_failure(format_err!("supplicant::on_eapol_frame fails")); |
| |
| // (mlme->sme) Send an EapolInd |
| let eapol_ind = create_eapol_ind(bssid.clone(), test_utils::eapol_key_frame().into()); |
| let s = state.on_mlme_event(eapol_ind, &mut h.context); |
| |
| assert_eq!(Ok(None), receiver.try_recv()); |
| assert_variant!(s, ClientState::Associated(state) => { |
| assert_variant!(&state.link_state, LinkState::EstablishingRsna { .. })}); |
| |
| expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream"); |
| expect_stream_empty(&mut h.info_stream, "unexpected event in info stream"); |
| } |
| |
| #[test] |
| fn reject_foreign_eapol_frames() { |
| let mut h = TestHelper::new(); |
| let (supplicant, mock) = mock_psk_supplicant(); |
| let state = link_up_state_protected(supplicant, [7; 6]); |
| mock.set_on_eapol_frame_callback(|| { |
| panic!("eapol frame should not have been processed"); |
| }); |
| |
| // Send an EapolInd from foreign BSS. |
| let eapol_ind = create_eapol_ind([1; 6], test_utils::eapol_key_frame().into()); |
| let state = state.on_mlme_event(eapol_ind, &mut h.context); |
| |
| // Verify state did not change. |
| assert_variant!(state, ClientState::Associated(state) => { |
| assert_variant!( |
| &state.link_state, |
| LinkState::LinkUp(state) => assert_variant!(&state.protection, Protection::Rsna(_)) |
| ) |
| }); |
| } |
| |
| #[test] |
| fn wrong_password_while_establishing_rsna() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let (command, receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| let state = establishing_rsna_state(command); |
| |
| // (mlme->sme) Send an EapolInd, mock supplicant with wrong password status |
| let update = SecAssocUpdate::Status(SecAssocStatus::WrongPassword); |
| let _state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving); |
| let result: ConnectResult = EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::InternalError, |
| } |
| .into(); |
| expect_result(receiver, result.clone()); |
| } |
| |
| #[test] |
| fn overall_timeout_while_establishing_rsna() { |
| let mut h = TestHelper::new(); |
| let (supplicant, _suppl_mock) = mock_psk_supplicant(); |
| let (command, receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| |
| // Start in an "Associating" state |
| let state = ClientState::from(testing::new_state(Associating { |
| cfg: ClientConfig::default(), |
| cmd: command, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| })); |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let state = state.on_mlme_event(assoc_conf, &mut h.context); |
| |
| let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event"); |
| assert_variant!(timed_event.event, Event::EstablishingRsnaTimeout(..)); |
| |
| expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream"); |
| |
| let _state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context); |
| |
| expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving); |
| expect_result( |
| receiver, |
| EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::OverallTimeout, |
| } |
| .into(), |
| ); |
| } |
| |
| #[test] |
| fn key_frame_exchange_timeout_while_establishing_rsna() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let (command, receiver) = connect_command_wpa2(supplicant); |
| let bssid = command.bss.bssid.clone(); |
| let state = establishing_rsna_state(command); |
| |
| // (mlme->sme) Send an EapolInd, mock supplication with key frame |
| let update = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame()); |
| let mut state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]); |
| |
| for i in 1..=3 { |
| println!("send eapol attempt: {}", i); |
| expect_eapol_req(&mut h.mlme_stream, bssid); |
| expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream"); |
| |
| let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event"); |
| assert_variant!(timed_event.event, Event::KeyFrameExchangeTimeout(ref event) => { |
| assert_eq!(event.attempt, i) |
| }); |
| state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context); |
| } |
| |
| expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving); |
| expect_result( |
| receiver, |
| EstablishRsnaFailure { |
| auth_method: Some(auth::MethodName::Psk), |
| reason: EstablishRsnaFailureReason::KeyFrameExchangeTimeout, |
| } |
| .into(), |
| ); |
| } |
| |
| #[test] |
| fn gtk_rotation_during_link_up() { |
| let mut h = TestHelper::new(); |
| let (supplicant, suppl_mock) = mock_psk_supplicant(); |
| let bssid = [7; 6]; |
| let state = link_up_state_protected(supplicant, bssid); |
| |
| // (mlme->sme) Send an EapolInd, mock supplication with key frame and GTK |
| let key_frame = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame()); |
| let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk())); |
| let mut state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![key_frame, gtk]); |
| |
| // EAPoL frame is sent out, but state still remains the same |
| expect_eapol_req(&mut h.mlme_stream, bssid); |
| expect_set_gtk(&mut h.mlme_stream); |
| expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream"); |
| assert_variant!(&state, ClientState::Associated(state) => { |
| assert_variant!(&state.link_state, LinkState::LinkUp { .. }); |
| }); |
| |
| // Any timeout is ignored |
| let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event"); |
| state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context); |
| assert_variant!(&state, ClientState::Associated(state) => { |
| assert_variant!(&state.link_state, LinkState::LinkUp { .. }); |
| }); |
| } |
| |
| #[test] |
| fn connect_while_link_up() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(connect_command_one().0.bss); |
| let state = state.connect(connect_command_two().0, &mut h.context); |
| let state = exchange_deauth(state, &mut h); |
| expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid); |
| assert_joining(state, &connect_command_two().0.bss); |
| } |
| |
| #[test] |
| fn disconnect_while_idle() { |
| let mut h = TestHelper::new(); |
| let new_state = idle_state().disconnect(&mut h.context); |
| assert_idle(new_state); |
| // Expect no messages to the MLME |
| assert!(h.mlme_stream.try_next().is_err()); |
| } |
| |
| #[test] |
| fn disconnect_while_joining() { |
| let mut h = TestHelper::new(); |
| let (cmd, receiver) = connect_command_one(); |
| let state = joining_state(cmd); |
| let state = state.disconnect(&mut h.context); |
| expect_result(receiver, ConnectResult::Canceled); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn disconnect_while_authenticating() { |
| let mut h = TestHelper::new(); |
| let (cmd, receiver) = connect_command_one(); |
| let state = authenticating_state(cmd); |
| let state = state.disconnect(&mut h.context); |
| expect_result(receiver, ConnectResult::Canceled); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn disconnect_while_associating() { |
| let mut h = TestHelper::new(); |
| let (cmd, receiver) = connect_command_one(); |
| let state = associating_state(cmd); |
| let state = state.disconnect(&mut h.context); |
| let state = exchange_deauth(state, &mut h); |
| expect_result(receiver, ConnectResult::Canceled); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn disconnect_while_link_up() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(connect_command_one().0.bss); |
| let state = state.disconnect(&mut h.context); |
| let state = exchange_deauth(state, &mut h); |
| assert_idle(state); |
| } |
| |
| #[test] |
| fn increment_att_id_on_connect() { |
| let mut h = TestHelper::new(); |
| let state = idle_state(); |
| assert_eq!(h.context.att_id, 0); |
| |
| let state = state.connect(connect_command_one().0, &mut h.context); |
| assert_eq!(h.context.att_id, 1); |
| |
| let state = state.disconnect(&mut h.context); |
| assert_eq!(h.context.att_id, 1); |
| |
| let state = state.connect(connect_command_two().0, &mut h.context); |
| assert_eq!(h.context.att_id, 2); |
| |
| let _state = state.connect(connect_command_one().0, &mut h.context); |
| assert_eq!(h.context.att_id, 3); |
| } |
| |
| #[test] |
| fn increment_att_id_on_disassociate_ind() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]))); |
| assert_eq!(h.context.att_id, 0); |
| |
| let disassociate_ind = MlmeEvent::DisassociateInd { |
| ind: fidl_mlme::DisassociateIndication { |
| peer_sta_address: [0, 0, 0, 0, 0, 0], |
| reason_code: 0, |
| locally_initiated: false, |
| }, |
| }; |
| |
| let state = state.on_mlme_event(disassociate_ind, &mut h.context); |
| assert_associating(state, &unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8])); |
| assert_eq!(h.context.att_id, 1); |
| } |
| |
| #[test] |
| fn connection_ping() { |
| let mut h = TestHelper::new(); |
| |
| let (cmd, _receiver) = connect_command_one(); |
| |
| // Start in an "Associating" state |
| let state = ClientState::from(testing::new_state(Associating { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| })); |
| let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success); |
| let state = state.on_mlme_event(assoc_conf, &mut h.context); |
| |
| // Verify ping timeout is scheduled |
| let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event"); |
| let first_ping = assert_variant!(timed_event.event.clone(), Event::ConnectionPing(info) => { |
| assert_eq!(info.connected_since, info.now); |
| assert!(info.last_reported.is_none()); |
| info |
| }); |
| // Verify that ping is reported |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(ref info))) => { |
| assert_eq!(info.connected_since, info.now); |
| assert!(info.last_reported.is_none()); |
| }); |
| |
| // Trigger the above timeout |
| let _state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context); |
| |
| // Verify ping timeout is scheduled again |
| let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event"); |
| assert_variant!(timed_event.event, Event::ConnectionPing(ref info) => { |
| assert_variant!(info.last_reported, Some(time) => assert_eq!(time, first_ping.now)); |
| }); |
| // Verify that ping is reported |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::ConnectionPing(ref info))) => { |
| assert_variant!(info.last_reported, Some(time) => assert_eq!(time, first_ping.now)); |
| }); |
| } |
| |
| #[test] |
| fn disconnect_reported_on_deauth_ind() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]))); |
| |
| let deauth_ind = MlmeEvent::DeauthenticateInd { |
| ind: fidl_mlme::DeauthenticateIndication { |
| peer_sta_address: [0, 0, 0, 0, 0, 0], |
| reason_code: fidl_mlme::ReasonCode::LeavingNetworkDeauth, |
| locally_initiated: true, |
| }, |
| }; |
| |
| let _state = state.on_mlme_event(deauth_ind, &mut h.context); |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::DisconnectInfo(info))) => { |
| assert_eq!(info.last_rssi, 60); |
| assert_eq!(info.last_snr, 30); |
| assert_eq!(info.ssid, b"bar"); |
| assert_eq!(info.bssid, [8; 6]); |
| assert_eq!(info.reason_code, fidl_mlme::ReasonCode::LeavingNetworkDeauth.into_primitive()); |
| assert_variant!(info.disconnect_source, DisconnectSource::Mlme); |
| }); |
| } |
| |
| #[test] |
| fn disconnect_reported_on_disassoc_ind() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]))); |
| |
| let deauth_ind = MlmeEvent::DisassociateInd { |
| ind: fidl_mlme::DisassociateIndication { |
| peer_sta_address: [0, 0, 0, 0, 0, 0], |
| reason_code: 4, |
| locally_initiated: true, |
| }, |
| }; |
| |
| let _state = state.on_mlme_event(deauth_ind, &mut h.context); |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::DisconnectInfo(info))) => { |
| assert_eq!(info.last_rssi, 60); |
| assert_eq!(info.last_snr, 30); |
| assert_eq!(info.ssid, b"bar"); |
| assert_eq!(info.bssid, [8; 6]); |
| assert_eq!(info.reason_code, 4); |
| assert_variant!(info.disconnect_source, DisconnectSource::Mlme); |
| }); |
| } |
| |
| #[test] |
| fn disconnect_reported_on_manual_disconnect() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]))); |
| |
| let _state = state.disconnect(&mut h.context); |
| assert_variant!(h.info_stream.try_next(), Ok(Some(InfoEvent::DisconnectInfo(info))) => { |
| assert_eq!(info.last_rssi, 60); |
| assert_eq!(info.last_snr, 30); |
| assert_eq!(info.ssid, b"bar"); |
| assert_eq!(info.bssid, [8; 6]); |
| assert_eq!(info.reason_code, fidl_mlme::ReasonCode::LeavingNetworkDeauth.into_primitive()); |
| assert_eq!(info.disconnect_source, DisconnectSource::User); |
| }); |
| } |
| |
| #[test] |
| fn bss_channel_switch_ind() { |
| let mut h = TestHelper::new(); |
| let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]))); |
| |
| let switch_ind = |
| MlmeEvent::OnChannelSwitched { info: fidl_mlme::ChannelSwitchInfo { new_channel: 36 } }; |
| |
| assert_variant!(&state, ClientState::Associated(state) => { |
| assert_eq!(state.bss.chan.primary, 1); |
| }); |
| let state = state.on_mlme_event(switch_ind, &mut h.context); |
| assert_variant!(state, ClientState::Associated(state) => { |
| assert_eq!(state.bss.chan.primary, 36); |
| }); |
| } |
| |
| #[test] |
| fn join_failure_capabilities_incompatible_softmac() { |
| let (mut command, _receiver) = connect_command_one(); |
| // empty rates will cause build_join_capabilities to fail, which in turn fails the join. |
| command.bss.rates = vec![]; |
| |
| let mut h = TestHelper::new(); |
| let state = idle_state().connect(command, &mut h.context); |
| |
| // State did not change to Joining because the command was ignored due to incompatibility. |
| assert_variant!(state, ClientState::Idle(_)); |
| } |
| |
| #[test] |
| fn join_failure_capabilities_incompatible_fullmac() { |
| let (mut command, _receiver) = connect_command_one(); |
| // empty rates will cause build_join_capabilities to fail, which in turn fails the join. |
| command.bss.rates = vec![]; |
| |
| let mut h = TestHelper::new(); |
| // set as full mac |
| h.context.is_softmac = false; |
| |
| let state = idle_state().connect(command, &mut h.context); |
| |
| // State did not change to Joining because the command was ignored due to incompatibility. |
| assert_variant!(state, ClientState::Idle(_)); |
| } |
| |
| #[test] |
| fn join_success_softmac() { |
| let (command, _receiver) = connect_command_one(); |
| let mut h = TestHelper::new(); |
| let state = idle_state().connect(command, &mut h.context); |
| |
| // State changed to Joining, capabilities preserved. |
| let cap = assert_variant!(&state, ClientState::Joining(state) => &state.cap); |
| assert!(cap.is_some()); |
| } |
| |
| #[test] |
| fn join_success_fullmac() { |
| let (command, _receiver) = connect_command_one(); |
| let mut h = TestHelper::new(); |
| // set full mac |
| h.context.is_softmac = false; |
| let state = idle_state().connect(command, &mut h.context); |
| |
| // State changed to Joining, capabilities discarded as FullMAC ignore them anyway. |
| let cap = assert_variant!(&state, ClientState::Joining(state) => &state.cap); |
| assert!(cap.is_none()); |
| } |
| |
| #[test] |
| fn join_failure_rsne_wrapped_in_legacy_wpa() { |
| let (supplicant, _suppl_mock) = mock_psk_supplicant(); |
| |
| let (mut command, _receiver) = connect_command_wpa2(supplicant); |
| // Take the RSNA and wrap it in LegacyWpa to make it invalid. |
| if let Protection::Rsna(rsna) = command.protection { |
| command.protection = Protection::LegacyWpa(rsna); |
| } else { |
| panic!("command is guaranteed to be contain legacy wpa"); |
| }; |
| |
| let mut h = TestHelper::new(); |
| let state = idle_state().connect(command, &mut h.context); |
| |
| // State did not change to Joining because command is invalid, thus ignored. |
| assert_variant!(state, ClientState::Idle(_)); |
| } |
| |
| #[test] |
| fn join_failure_legacy_wpa_wrapped_in_rsna() { |
| let (supplicant, _suppl_mock) = mock_psk_supplicant(); |
| |
| let (mut command, _receiver) = connect_command_wpa1(supplicant); |
| // Take the LegacyWpa RSNA and wrap it in Rsna to make it invalid. |
| if let Protection::LegacyWpa(rsna) = command.protection { |
| command.protection = Protection::Rsna(rsna); |
| } else { |
| panic!("command is guaranteed to be contain legacy wpa"); |
| }; |
| |
| let mut h = TestHelper::new(); |
| let state = idle_state(); |
| let state = state.connect(command, &mut h.context); |
| |
| // State did not change to Joining because command is invalid, thus ignored. |
| assert_variant!(state, ClientState::Idle(_)); |
| } |
| |
| #[test] |
| fn fill_wmm_ie_associating() { |
| let mut h = TestHelper::new(); |
| let (cmd, _receiver) = connect_command_one(); |
| let resp = fidl_mlme::AssociateConfirm { |
| result_code: fidl_mlme::AssociateResultCodes::Success, |
| association_id: 1, |
| cap_info: 0, |
| rates: vec![0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c], |
| ht_cap: cmd.bss.ht_cap.clone(), |
| vht_cap: cmd.bss.vht_cap.clone(), |
| wmm_param: Some(Box::new(fake_wmm_param())), |
| }; |
| |
| let state = associating_state(cmd); |
| let state = state.on_mlme_event(MlmeEvent::AssociateConf { resp }, &mut h.context); |
| assert_variant!(state, ClientState::Associated(state) => { |
| assert!(state.wmm_param.is_some()); |
| }); |
| } |
| |
| #[test] |
| fn status_returns_last_rssi_snr() { |
| let mut h = TestHelper::new(); |
| |
| let state = link_up_state(Box::new(unprotected_bss(b"RSSI".to_vec(), [42; 6]))); |
| let state = state.on_mlme_event(signal_report_with_rssi_snr(-42, 20), &mut h.context); |
| assert_eq!(state.status().connected_to.unwrap().rx_dbm, -42); |
| assert_eq!(state.status().connected_to.unwrap().snr_db, 20); |
| |
| let state = state.on_mlme_event(signal_report_with_rssi_snr(-24, 10), &mut h.context); |
| assert_eq!(state.status().connected_to.unwrap().rx_dbm, -24); |
| assert_eq!(state.status().connected_to.unwrap().snr_db, 10); |
| } |
| |
| // Helper functions and data structures for tests |
| struct TestHelper { |
| mlme_stream: MlmeStream, |
| info_stream: InfoStream, |
| time_stream: TimeStream, |
| context: Context, |
| // Inspector is kept so that root node doesn't automatically get removed from VMO |
| _inspector: Inspector, |
| } |
| |
| impl TestHelper { |
| fn new() -> Self { |
| let (mlme_sink, mlme_stream) = mpsc::unbounded(); |
| let (info_sink, info_stream) = mpsc::unbounded(); |
| let (timer, time_stream) = timer::create_timer(); |
| let inspector = Inspector::new(); |
| let inspect_hasher = wlan_inspect::InspectHasher::new([88, 77, 66, 55, 44, 33, 22, 11]); |
| let context = Context { |
| device_info: Arc::new(fake_device_info()), |
| mlme_sink: MlmeSink::new(mlme_sink), |
| timer, |
| att_id: 0, |
| inspect: Arc::new(inspect::SmeTree::new(inspector.root(), inspect_hasher)), |
| info: InfoReporter::new(InfoSink::new(info_sink)), |
| is_softmac: true, |
| }; |
| TestHelper { mlme_stream, info_stream, time_stream, context, _inspector: inspector } |
| } |
| } |
| |
| fn on_eapol_ind( |
| state: ClientState, |
| helper: &mut TestHelper, |
| bssid: [u8; 6], |
| suppl_mock: &MockSupplicantController, |
| update_sink: UpdateSink, |
| ) -> ClientState { |
| suppl_mock.set_on_eapol_frame_results(update_sink); |
| // (mlme->sme) Send an EapolInd |
| let eapol_ind = create_eapol_ind(bssid.clone(), test_utils::eapol_key_frame().into()); |
| state.on_mlme_event(eapol_ind, &mut helper.context) |
| } |
| |
| fn create_eapol_ind(bssid: [u8; 6], data: Vec<u8>) -> MlmeEvent { |
| MlmeEvent::EapolInd { |
| ind: fidl_mlme::EapolIndication { |
| src_addr: bssid, |
| dst_addr: fake_device_info().mac_addr, |
| data, |
| }, |
| } |
| } |
| |
| fn exchange_deauth(state: ClientState, h: &mut TestHelper) -> ClientState { |
| // (sme->mlme) Expect a DeauthenticateRequest |
| assert_variant!(h.mlme_stream.try_next(), Ok(Some(MlmeRequest::Deauthenticate(req))) => { |
| assert_eq!(connect_command_one().0.bss.bssid, req.peer_sta_address); |
| }); |
| |
| // (mlme->sme) Send a DeauthenticateConf as a response |
| let deauth_conf = MlmeEvent::DeauthenticateConf { |
| resp: fidl_mlme::DeauthenticateConfirm { |
| peer_sta_address: connect_command_one().0.bss.bssid, |
| }, |
| }; |
| state.on_mlme_event(deauth_conf, &mut h.context) |
| } |
| |
| fn expect_join_request(mlme_stream: &mut MlmeStream, ssid: &[u8]) { |
| // (sme->mlme) Expect a JoinRequest |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Join(req))) => { |
| assert_eq!(ssid, &req.selected_bss.ssid[..]) |
| }); |
| } |
| |
| fn expect_set_ctrl_port( |
| mlme_stream: &mut MlmeStream, |
| bssid: [u8; 6], |
| state: fidl_mlme::ControlledPortState, |
| ) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetCtrlPort(req))) => { |
| assert_eq!(req.peer_sta_address, bssid); |
| assert_eq!(req.state, state); |
| }); |
| } |
| |
| fn expect_auth_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) { |
| // (sme->mlme) Expect an AuthenticateRequest |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Authenticate(req))) => { |
| assert_eq!(bssid, req.peer_sta_address) |
| }); |
| } |
| |
| fn expect_deauth_req( |
| mlme_stream: &mut MlmeStream, |
| bssid: [u8; 6], |
| reason_code: fidl_mlme::ReasonCode, |
| ) { |
| // (sme->mlme) Expect a DeauthenticateRequest |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Deauthenticate(req))) => { |
| assert_eq!(bssid, req.peer_sta_address); |
| assert_eq!(reason_code, req.reason_code); |
| }); |
| } |
| |
| fn expect_assoc_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Associate(req))) => { |
| assert_eq!(bssid, req.peer_sta_address); |
| }); |
| } |
| |
| fn expect_finalize_association_req( |
| mlme_stream: &mut MlmeStream, |
| chan_and_cap: (Channel, ClientCapabilities), |
| ) { |
| let (chan, join_cap) = chan_and_cap; |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::FinalizeAssociation(cap))) => { |
| assert_eq!(cap, join_cap.0.to_fidl_negotiated_capabilities(&chan)); |
| }); |
| } |
| |
| fn expect_eapol_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::Eapol(req))) => { |
| assert_eq!(req.src_addr, fake_device_info().mac_addr); |
| assert_eq!(req.dst_addr, bssid); |
| assert_eq!(req.data, Vec::<u8>::from(test_utils::eapol_key_frame())); |
| }); |
| } |
| |
| fn expect_set_ptk(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => { |
| assert_eq!(set_keys_req.keylist.len(), 1); |
| let k = set_keys_req.keylist.get(0).expect("expect key descriptor"); |
| assert_eq!(k.key, vec![0xCCu8; test_utils::cipher().tk_bytes().unwrap()]); |
| assert_eq!(k.key_id, 0); |
| assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise); |
| assert_eq!(k.address, bssid); |
| assert_eq!(k.rsc, 0); |
| assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]); |
| assert_eq!(k.cipher_suite_type, 4); |
| }); |
| } |
| |
| fn expect_set_gtk(mlme_stream: &mut MlmeStream) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => { |
| assert_eq!(set_keys_req.keylist.len(), 1); |
| let k = set_keys_req.keylist.get(0).expect("expect key descriptor"); |
| assert_eq!(k.key, test_utils::gtk_bytes()); |
| assert_eq!(k.key_id, 2); |
| assert_eq!(k.key_type, fidl_mlme::KeyType::Group); |
| assert_eq!(k.address, [0xFFu8; 6]); |
| assert_eq!(k.rsc, 0); |
| assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]); |
| assert_eq!(k.cipher_suite_type, 4); |
| }); |
| } |
| |
| fn expect_set_wpa1_ptk(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => { |
| assert_eq!(set_keys_req.keylist.len(), 1); |
| let k = set_keys_req.keylist.get(0).expect("expect key descriptor"); |
| assert_eq!(k.key, vec![0xCCu8; test_utils::wpa1_cipher().tk_bytes().unwrap()]); |
| assert_eq!(k.key_id, 0); |
| assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise); |
| assert_eq!(k.address, bssid); |
| assert_eq!(k.rsc, 0); |
| assert_eq!(k.cipher_suite_oui, [0x00, 0x50, 0xF2]); |
| assert_eq!(k.cipher_suite_type, 2); |
| }); |
| } |
| |
| fn expect_set_wpa1_gtk(mlme_stream: &mut MlmeStream) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => { |
| assert_eq!(set_keys_req.keylist.len(), 1); |
| let k = set_keys_req.keylist.get(0).expect("expect key descriptor"); |
| assert_eq!(k.key, test_utils::wpa1_gtk_bytes()); |
| assert_eq!(k.key_id, 2); |
| assert_eq!(k.key_type, fidl_mlme::KeyType::Group); |
| assert_eq!(k.address, [0xFFu8; 6]); |
| assert_eq!(k.rsc, 0); |
| assert_eq!(k.cipher_suite_oui, [0x00, 0x50, 0xF2]); |
| assert_eq!(k.cipher_suite_type, 2); |
| }); |
| } |
| |
| fn expect_set_wep_key(mlme_stream: &mut MlmeStream, bssid: [u8; 6], key_bytes: Vec<u8>) { |
| assert_variant!(mlme_stream.try_next(), Ok(Some(MlmeRequest::SetKeys(set_keys_req))) => { |
| assert_eq!(set_keys_req.keylist.len(), 1); |
| let k = set_keys_req.keylist.get(0).expect("expect key descriptor"); |
| assert_eq!(k.key, &key_bytes[..]); |
| assert_eq!(k.key_id, 0); |
| assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise); |
| assert_eq!(k.address, bssid); |
| assert_eq!(k.rsc, 0); |
| assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]); |
| assert_eq!(k.cipher_suite_type, 1); |
| }); |
| } |
| |
| fn expect_result<T>(mut receiver: oneshot::Receiver<T>, expected_result: T) |
| where |
| T: PartialEq + ::std::fmt::Debug, |
| { |
| assert_eq!(Ok(Some(expected_result)), receiver.try_recv()); |
| } |
| |
| fn connect_command_one() -> (ConnectCommand, oneshot::Receiver<ConnectResult>) { |
| let (responder, receiver) = Responder::new(); |
| let cmd = ConnectCommand { |
| bss: Box::new(unprotected_bss(b"foo".to_vec(), [7, 7, 7, 7, 7, 7])), |
| responder: Some(responder), |
| protection: Protection::Open, |
| radio_cfg: RadioConfig::default(), |
| }; |
| (cmd, receiver) |
| } |
| |
| fn connect_command_two() -> (ConnectCommand, oneshot::Receiver<ConnectResult>) { |
| let (responder, receiver) = Responder::new(); |
| let cmd = ConnectCommand { |
| bss: Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8])), |
| responder: Some(responder), |
| protection: Protection::Open, |
| radio_cfg: RadioConfig::default(), |
| }; |
| (cmd, receiver) |
| } |
| |
| fn connect_command_wep() -> (ConnectCommand, oneshot::Receiver<ConnectResult>) { |
| let (responder, receiver) = Responder::new(); |
| let cmd = ConnectCommand { |
| bss: Box::new(fake_wep_bss_description(b"wep".to_vec())), |
| responder: Some(responder), |
| protection: Protection::Wep(wep_deprecated::Key::Bits40([3; 5])), |
| radio_cfg: RadioConfig::default(), |
| }; |
| (cmd, receiver) |
| } |
| |
| fn connect_command_wpa1( |
| supplicant: MockSupplicant, |
| ) -> (ConnectCommand, oneshot::Receiver<ConnectResult>) { |
| let (responder, receiver) = Responder::new(); |
| let wpa_ie = make_wpa1_ie(); |
| let cmd = ConnectCommand { |
| bss: Box::new(fake_wpa1_bss_description(b"wpa1".to_vec())), |
| responder: Some(responder), |
| protection: Protection::LegacyWpa(Rsna { |
| negotiated_protection: NegotiatedProtection::from_legacy_wpa(&wpa_ie) |
| .expect("invalid NegotiatedProtection"), |
| supplicant: Box::new(supplicant), |
| }), |
| radio_cfg: RadioConfig::default(), |
| }; |
| (cmd, receiver) |
| } |
| |
| fn connect_command_wpa2( |
| supplicant: MockSupplicant, |
| ) -> (ConnectCommand, oneshot::Receiver<ConnectResult>) { |
| let (responder, receiver) = Responder::new(); |
| let bss = protected_bss(b"foo".to_vec(), [7, 7, 7, 7, 7, 7]); |
| let rsne = Rsne::wpa2_psk_ccmp_rsne(); |
| let cmd = ConnectCommand { |
| bss: Box::new(bss), |
| responder: Some(responder), |
| protection: Protection::Rsna(Rsna { |
| negotiated_protection: NegotiatedProtection::from_rsne(&rsne) |
| .expect("invalid NegotiatedProtection"), |
| supplicant: Box::new(supplicant), |
| }), |
| radio_cfg: RadioConfig::default(), |
| }; |
| (cmd, receiver) |
| } |
| |
| fn idle_state() -> ClientState { |
| testing::new_state(Idle { cfg: ClientConfig::default() }).into() |
| } |
| |
| fn assert_idle(state: ClientState) { |
| assert_variant!(&state, ClientState::Idle(_)); |
| } |
| |
| fn joining_state(cmd: ConnectCommand) -> ClientState { |
| testing::new_state(Joining { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| }) |
| .into() |
| } |
| |
| fn assert_joining(state: ClientState, bss: &BssDescription) { |
| assert_variant!(&state, ClientState::Joining(joining) => { |
| assert_eq!(joining.cmd.bss.as_ref(), bss); |
| }); |
| } |
| |
| fn authenticating_state(cmd: ConnectCommand) -> ClientState { |
| testing::new_state(Authenticating { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| }) |
| .into() |
| } |
| |
| fn associating_state(cmd: ConnectCommand) -> ClientState { |
| testing::new_state(Associating { |
| cfg: ClientConfig::default(), |
| cmd, |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| }) |
| .into() |
| } |
| |
| fn assert_associating(state: ClientState, bss: &BssDescription) { |
| assert_variant!(&state, ClientState::Associating(associating) => { |
| assert_eq!(associating.cmd.bss.as_ref(), bss); |
| }); |
| } |
| |
| fn establishing_rsna_state(cmd: ConnectCommand) -> ClientState { |
| let auth_method = cmd.protection.get_rsn_auth_method(); |
| let rsna = assert_variant!(cmd.protection, Protection::Rsna(rsna) => rsna); |
| let link_state = |
| testing::new_state(EstablishingRsna { rsna, rsna_timeout: None, resp_timeout: None }) |
| .into(); |
| testing::new_state(Associated { |
| cfg: ClientConfig::default(), |
| bss: cmd.bss, |
| auth_method, |
| responder: cmd.responder, |
| last_rssi: 60, |
| last_snr: 0, |
| link_state, |
| radio_cfg: RadioConfig::default(), |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| wmm_param: None, |
| last_channel_switch_time: None, |
| }) |
| .into() |
| } |
| |
| fn link_up_state(bss: Box<fidl_mlme::BssDescription>) -> ClientState { |
| let link_state = testing::new_state(LinkUp { |
| protection: Protection::Open, |
| since: now(), |
| ping_event: None, |
| }) |
| .into(); |
| testing::new_state(Associated { |
| cfg: ClientConfig::default(), |
| responder: None, |
| bss, |
| auth_method: None, |
| last_rssi: 60, |
| last_snr: 30, |
| link_state, |
| radio_cfg: RadioConfig::default(), |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| wmm_param: None, |
| last_channel_switch_time: None, |
| }) |
| .into() |
| } |
| |
| fn link_up_state_protected(supplicant: MockSupplicant, bssid: [u8; 6]) -> ClientState { |
| let bss = protected_bss(b"foo".to_vec(), bssid); |
| let rsne = Rsne::wpa2_psk_ccmp_rsne(); |
| let rsna = Rsna { |
| negotiated_protection: NegotiatedProtection::from_rsne(&rsne) |
| .expect("invalid NegotiatedProtection"), |
| supplicant: Box::new(supplicant), |
| }; |
| let protection = Protection::Rsna(rsna); |
| let auth_method = protection.get_rsn_auth_method(); |
| let link_state = |
| testing::new_state(LinkUp { protection, since: now(), ping_event: None }).into(); |
| testing::new_state(Associated { |
| cfg: ClientConfig::default(), |
| bss: Box::new(bss), |
| responder: None, |
| auth_method, |
| last_rssi: 60, |
| last_snr: 30, |
| link_state, |
| radio_cfg: RadioConfig::default(), |
| chan: fake_channel(), |
| cap: None, |
| protection_ie: None, |
| wmm_param: None, |
| last_channel_switch_time: None, |
| }) |
| .into() |
| } |
| |
| fn protected_bss(ssid: Ssid, bssid: [u8; 6]) -> fidl_mlme::BssDescription { |
| fidl_mlme::BssDescription { bssid, ..fake_protected_bss_description(ssid) } |
| } |
| |
| fn unprotected_bss(ssid: Ssid, bssid: [u8; 6]) -> fidl_mlme::BssDescription { |
| fidl_mlme::BssDescription { bssid, ..fake_unprotected_bss_description(ssid) } |
| } |
| |
| fn fake_device_info() -> fidl_mlme::DeviceInfo { |
| test_utils::fake_device_info([0, 1, 2, 3, 4, 5]) |
| } |
| |
| fn fake_channel() -> Channel { |
| Channel { primary: 153, cbw: wlan_common::channel::Cbw::Cbw20 } |
| } |
| |
| fn signal_report_with_rssi_snr(rssi_dbm: i8, snr_db: i8) -> MlmeEvent { |
| MlmeEvent::SignalReport { ind: fidl_mlme::SignalReportIndication { rssi_dbm, snr_db } } |
| } |
| } |