blob: 5fdc490509163b90052b01f888174203799d0b1c [file] [log] [blame]
// Copyright 2019 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.
//! A state machine for associating a Client to a BSS.
//! Note: This implementation only supports simultaneous authentication with exactly one STA, the
//! AP. While 802.11 explicitly allows - and sometime requires - authentication with more than one
//! STA, Fuchsia does intentionally not yet support this use-case.
use {
crate::{
auth,
block_ack::{BlockAckState, Closed},
client::{lost_bss::LostBssCounter, BoundClient, Client, Context, TimedEvent},
ddk_converter as ddk,
error::Error,
key::KeyConfig,
timer::*,
},
banjo_ddk_protocol_wlan_mac as banjo_wlan_mac, fidl_fuchsia_wlan_mlme as fidl_mlme,
fuchsia_zircon as zx,
log::{error, info, warn},
static_assertions::assert_eq_size,
std::convert::TryInto,
wlan_common::{
buffer_reader::BufferReader,
energy::DecibelMilliWatt,
ie,
mac::{self, MacAddr, PowerState},
stats::SignalStrengthAverage,
tim,
time::TimeUnit,
},
wlan_statemachine::*,
zerocopy::{AsBytes, ByteSlice},
};
/// Association timeout in Beacon periods.
/// If no association response was received from he BSS within this time window, an association is
/// considered to have failed.
// TODO(41609): Let upper layers set this value.
const ASSOC_TIMEOUT_BCN_PERIODS: u16 = 10;
/// Number of beacon intervals which beacon is not seen before we declare BSS as lost
pub const DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT: u32 = 100;
/// Number of beacon intervals between association status check (signal report or auto-deatuh).
pub const ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT: u32 = 10;
type HtOpByteArray = [u8; fidl_mlme::HT_OP_LEN as usize];
type VhtOpByteArray = [u8; fidl_mlme::VHT_OP_LEN as usize];
/// Client joined a BSS (synchronized timers and prepared its underlying hardware).
/// At this point the Client is able to listen to frames on the BSS' channel.
pub struct Joined;
impl Joined {
/// Initiates an open authentication with the currently joined BSS.
/// The returned state is unchanged in an error case. Otherwise, the state transitions into
/// "Authenticating".
/// Returns Ok(timeout) if authentication request was sent successfully, Err(()) otherwise.
fn on_sme_authenticate(
&self,
sta: &mut BoundClient<'_>,
timeout_bcn_count: u16,
) -> Result<EventId, ()> {
match sta.send_open_auth_frame() {
Ok(()) => {
let duration_tus = TimeUnit::DEFAULT_BEACON_INTERVAL * timeout_bcn_count;
let deadline = sta.ctx.timer.now() + duration_tus.into();
let event = TimedEvent::Authenticating;
let event_id = sta.ctx.timer.schedule_event(deadline, event);
Ok(event_id)
}
Err(e) => {
error!("{}", e);
sta.send_authenticate_conf(fidl_mlme::AuthenticateResultCodes::Refused);
Err(())
}
}
}
}
/// Client issued an authentication request frame to its joined BSS prior to joining this state.
/// At this point the client is waiting for an authentication response frame from the client.
/// Note: This assumes Open System authentication.
pub struct Authenticating {
timeout: EventId,
}
impl Authenticating {
/// Processes an inbound authentication frame.
/// SME will be notified via an MLME-AUTHENTICATE.confirm message whether the authentication
/// with the BSS was successful.
/// Returns Ok(()) if the authentication was successful, otherwise Err(()).
/// Note: The pending authentication timeout will be canceled in any case.
fn on_auth_frame(&self, sta: &mut BoundClient<'_>, auth_hdr: &mac::AuthHdr) -> Result<(), ()> {
sta.ctx.timer.cancel_event(self.timeout);
let frame_type = auth::validate_ap_resp(auth_hdr).map_err(|e| {
error!("authentication with BSS failed: {}", e);
sta.send_authenticate_conf(fidl_mlme::AuthenticateResultCodes::AuthenticationRejected);
})?;
match frame_type {
auth::ValidFrame::Open => {
sta.send_authenticate_conf(fidl_mlme::AuthenticateResultCodes::Success);
Ok(())
}
_ => {
error!("authentication with BSS failed: unhandled auth type {:?}", frame_type);
sta.send_authenticate_conf(
fidl_mlme::AuthenticateResultCodes::AuthenticationRejected,
);
Err(())
}
}
}
/// Processes an inbound deauthentication frame.
/// This always results in an MLME-AUTHENTICATE.confirm message to MLME's SME peer.
/// The pending authentication timeout will be canceled in this process.
fn on_deauth_frame(&self, sta: &mut BoundClient<'_>, deauth_hdr: &mac::DeauthHdr) {
sta.ctx.timer.cancel_event(self.timeout);
info!(
"received spurious deauthentication frame while authenticating with BSS (unusual); \
authentication failed: {:?}",
{ deauth_hdr.reason_code }
);
sta.send_authenticate_conf(fidl_mlme::AuthenticateResultCodes::Refused);
}
/// Invoked when the pending timeout fired. The original authentication request is now
/// considered to be expired and invalid - the authentication failed. As a consequence,
/// an MLME-AUTHENTICATION.confirm message is reported to MLME's SME peer indicating the
/// timeout.
fn on_timeout(&self, sta: &mut BoundClient<'_>) {
// At this point, the event should already be canceled by the state's owner. However,
// ensure the timeout is canceled in any case.
sta.ctx.timer.cancel_event(self.timeout);
sta.send_authenticate_conf(fidl_mlme::AuthenticateResultCodes::AuthFailureTimeout);
}
fn on_sme_deauthenticate(&self, sta: &mut BoundClient<'_>) {
sta.ctx.timer.cancel_event(self.timeout);
}
}
/// Client received a "successful" authentication response from the BSS.
pub struct Authenticated;
impl Authenticated {
/// Initiates an association with the currently joined BSS.
/// Returns Ok(timeout) if association request was sent successfully.
/// Otherwise an Err(()) is returned and an ASSOCIATE.confirm message to its SME peer.
fn on_sme_associate(
&self,
sta: &mut BoundClient<'_>,
req: fidl_mlme::AssociateRequest,
) -> Result<EventId, ()> {
// clone ssid here because we will mutably borrow sta.
let cap_info = req.cap_info;
let rsne = req.rsne.as_ref().map(|rsne| &rsne[..]).unwrap_or(&[]);
let ht_cap = req.ht_cap.as_ref().map(|h| &h.bytes[..]).unwrap_or(&[]);
let vht_cap = req.vht_cap.as_ref().map(|v| &v.bytes[..]).unwrap_or(&[]);
match sta.send_assoc_req_frame(cap_info, &req.rates[..], rsne, ht_cap, vht_cap) {
Ok(()) => {
let duration_tus = TimeUnit::DEFAULT_BEACON_INTERVAL * ASSOC_TIMEOUT_BCN_PERIODS;
let deadline = sta.ctx.timer.now() + duration_tus.into();
let event = TimedEvent::Associating;
let event_id = sta.ctx.timer.schedule_event(deadline, event);
Ok(event_id)
}
Err(e) => {
error!("Error sending association request frame: {}", e);
sta.send_associate_conf_failure(
fidl_mlme::AssociateResultCodes::RefusedTemporarily,
);
Err(())
}
}
}
/// Sends an MLME-DEAUTHENTICATE.indication message to MLME's SME peer.
fn on_deauth_frame(&self, sta: &mut BoundClient<'_>, deauth_hdr: &mac::DeauthHdr) {
let reason_code = fidl_mlme::ReasonCode::from_primitive(deauth_hdr.reason_code.0)
.unwrap_or(fidl_mlme::ReasonCode::UnspecifiedReason);
sta.send_deauthenticate_ind(reason_code);
}
fn on_sme_deauthenticate(
&self,
sta: &mut BoundClient<'_>,
req: fidl_mlme::DeauthenticateRequest,
) {
if let Err(e) = sta.send_deauth_frame(mac::ReasonCode(req.reason_code.into_primitive())) {
error!("Error sending deauthentication frame to BSS: {}", e);
}
if let Err(e) = sta.ctx.device.access_sme_sender(|sender| {
sender.send_deauthenticate_conf(&mut fidl_mlme::DeauthenticateConfirm {
peer_sta_address: sta.sta.bssid.0,
})
}) {
error!("Error sending MLME-DEAUTHENTICATE.confirm: {}", e)
}
}
}
/// Client received an MLME-ASSOCIATE.request message from SME.
pub struct Associating {
timeout: EventId,
}
impl Associating {
/// Processes an inbound association response frame.
/// SME will be notified via an MLME-ASSOCIATE.confirm message whether the association
/// with the BSS was successful.
/// Returns Ok(()) if the association was successful, otherwise Err(()).
/// Note: The pending authentication timeout will be canceled in any case.
fn on_assoc_resp_frame<B: ByteSlice>(
&self,
sta: &mut BoundClient<'_>,
assoc_resp_hdr: &mac::AssocRespHdr,
elements: B,
) -> Result<Association, ()> {
sta.ctx.timer.cancel_event(self.timeout);
match assoc_resp_hdr.status_code {
mac::StatusCode::SUCCESS => {
sta.send_associate_conf_success(
assoc_resp_hdr.aid,
assoc_resp_hdr.capabilities,
&elements[..],
);
let (ap_ht_op, ap_vht_op) = extract_ht_vht_op(elements);
let controlled_port_open = !sta.sta.is_rsn;
if controlled_port_open {
if let Err(e) = sta.ctx.device.set_eth_link_up() {
error!("Cannot set ethernet to UP. Status: {}", e);
}
}
let lost_bss_counter = LostBssCounter::start(
sta.sta.beacon_period,
DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT,
);
let status_check_timeout =
schedule_association_status_timeout(sta.sta.beacon_period, &mut sta.ctx.timer);
Ok(Association {
aid: assoc_resp_hdr.aid,
controlled_port_open,
ap_ht_op,
ap_vht_op,
qos: Qos::PendingNegotiation,
lost_bss_counter,
status_check_timeout,
signal_strength_average: SignalStrengthAverage::new(),
block_ack_state: StateMachine::new(BlockAckState::from(State::new(Closed))),
})
}
status_code => {
error!("association with BSS failed: {:?}", status_code);
sta.send_associate_conf_failure(
fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
);
Err(())
}
}
}
/// Processes an inbound disassociation frame.
/// Note: APs should never send disassociation frames without having established a valid
/// association with the Client. However, to maximize interoperability disassociation frames
/// are handled in this state as well and treated similar to unsuccessful association responses.
/// This always results in an MLME-ASSOCIATE.confirm message to MLME's SME peer.
fn on_disassoc_frame(&self, sta: &mut BoundClient<'_>, _disassoc_hdr: &mac::DisassocHdr) {
sta.ctx.timer.cancel_event(self.timeout);
warn!("received unexpected disassociation frame while associating");
sta.send_associate_conf_failure(fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified);
}
/// Processes an inbound deauthentication frame.
/// This always results in an MLME-ASSOCIATE.confirm message to MLME's SME peer.
/// The pending association timeout will be canceled in this process.
fn on_deauth_frame(&self, sta: &mut BoundClient<'_>, deauth_hdr: &mac::DeauthHdr) {
sta.ctx.timer.cancel_event(self.timeout);
info!(
"received spurious deauthentication frame while associating with BSS (unusual); \
association failed: {:?}",
{ deauth_hdr.reason_code }
);
sta.send_associate_conf_failure(fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified);
}
/// Invoked when the pending timeout fired. The original association request is now
/// considered to be expired and invalid - the association failed. As a consequence,
/// an MLME-ASSOCIATE.confirm message is reported to MLME's SME peer indicating the
/// timeout.
fn on_timeout(&self, sta: &mut BoundClient<'_>) {
// At this point, the event should already be canceled by the state's owner. However,
// ensure the timeout is canceled in any case.
sta.ctx.timer.cancel_event(self.timeout);
sta.send_associate_conf_failure(fidl_mlme::AssociateResultCodes::RefusedTemporarily);
}
fn on_sme_deauthenticate(&self, sta: &mut BoundClient<'_>) {
sta.ctx.timer.cancel_event(self.timeout);
}
}
/// Extract HT Operation and VHT Operation IEs from the association response frame.
/// If either IE is of an incorrect length, it will be ignored.
fn extract_ht_vht_op<B: ByteSlice>(elements: B) -> (Option<HtOpByteArray>, Option<VhtOpByteArray>) {
// Note the assert_eq_size!() that guarantees these structs match at compile time.
let mut ht_op: Option<HtOpByteArray> = None;
let mut vht_op: Option<VhtOpByteArray> = None;
for (id, body) in ie::Reader::new(elements) {
match id {
ie::Id::HT_OPERATION => match ie::parse_ht_operation(body) {
Ok(h) => {
assert_eq_size!(ie::HtOperation, HtOpByteArray);
ht_op = Some(h.as_bytes().try_into().unwrap());
}
Err(e) => {
error!("Invalid HT Operation: {}", e);
continue;
}
},
ie::Id::VHT_OPERATION => match ie::parse_vht_operation(body) {
Ok(v) => {
assert_eq_size!(ie::VhtOperation, VhtOpByteArray);
vht_op = Some(v.as_bytes().try_into().unwrap());
}
Err(e) => {
error!("Invalid VHT Operation: {}", e);
continue;
}
},
_ => (),
}
}
(ht_op, vht_op)
}
pub fn schedule_association_status_timeout(
beacon_period: u16,
timer: &mut Timer<TimedEvent>,
) -> StatusCheckTimeout {
let last_fired = timer.now();
let deadline = last_fired
+ zx::Duration::from(TimeUnit(beacon_period)) * ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT;
StatusCheckTimeout {
last_fired,
next_id: timer.schedule_event(deadline, TimedEvent::AssociationStatusCheck),
}
}
#[derive(Debug, PartialEq)]
pub enum Qos {
Enabled,
Disabled,
// Intermediate state between when an association response frame is received from AP and
// when a finalize_association is received from SME.
PendingNegotiation,
}
impl From<bool> for Qos {
fn from(b: bool) -> Self {
if b {
Self::Enabled
} else {
Self::Disabled
}
}
}
impl Qos {
fn is_enabled(&self) -> bool {
*self == Self::Enabled
}
}
#[derive(Debug)]
pub struct StatusCheckTimeout {
last_fired: zx::Time,
next_id: EventId,
}
#[derive(Debug)]
pub struct Association {
pub aid: mac::Aid,
/// Represents an 802.1X controlled port.
/// A closed controlled port only processes EAP frames while an open one processes any frames.
pub controlled_port_open: bool,
pub ap_ht_op: Option<HtOpByteArray>,
pub ap_vht_op: Option<VhtOpByteArray>,
/// Whether to set QoS bit when MLME constructs an outgoing WLAN data frame.
/// Currently, QoS is enabled if the associated PHY is HT or VHT.
pub qos: Qos,
/// `lost_bss_counter` is used to determine if the BSS is still alive nearby. It is started
/// when the client is associated.
pub lost_bss_counter: LostBssCounter,
/// |timeout| is the timeout that is scheduled for the association status check, which includes
/// a) sending signal strength report to SME and b) triggering auto-deauth if necessary.
/// It will be cancelled when the client go off-channel for scanning and scheduled again when
/// back on channel.
pub status_check_timeout: StatusCheckTimeout,
pub signal_strength_average: SignalStrengthAverage,
pub block_ack_state: StateMachine<BlockAckState>,
}
/// Client received a "successful" association response from the BSS.
pub struct Associated(pub Association);
impl Associated {
/// Processes an inbound diassociation frame.
/// This always results in an MLME-DISASSOCIATE.indication message to MLME's SME peer.
fn on_disassoc_frame(&mut self, sta: &mut BoundClient<'_>, disassoc_hdr: &mac::DisassocHdr) {
self.pre_leaving_associated_state(sta);
let reason_code = fidl_mlme::ReasonCode::from_primitive(disassoc_hdr.reason_code.0)
.unwrap_or(fidl_mlme::ReasonCode::UnspecifiedReason);
sta.send_disassoc_ind(reason_code);
}
/// Sends an MLME-DEAUTHENTICATE.indication message to MLME's SME peer.
fn on_deauth_frame(&mut self, sta: &mut BoundClient<'_>, deauth_hdr: &mac::DeauthHdr) {
self.pre_leaving_associated_state(sta);
let reason_code = fidl_mlme::ReasonCode::from_primitive(deauth_hdr.reason_code.0)
.unwrap_or(fidl_mlme::ReasonCode::UnspecifiedReason);
sta.send_deauthenticate_ind(reason_code);
}
/// Process every inbound management frame before its being handed off to a more specific
/// handler.
fn on_any_mgmt_frame(&self, sta: &mut BoundClient<'_>, mgmt_hdr: &mac::MgmtHdr) {
self.request_bu_if_available(sta, mgmt_hdr.frame_ctrl, mgmt_hdr.addr1);
}
/// Sends PS-POLL requests if the FrameControl's more_data bit is set, and the received frame
/// was addressed for this STA. No-op if the controlled port is closed.
fn request_bu_if_available(
&self,
sta: &mut BoundClient<'_>,
fc: mac::FrameControl,
dst_addr: MacAddr,
) {
if !self.0.controlled_port_open {
return;
}
// IEEE Std. 802.11-2016, 9.2.4.1.8
if fc.more_data() && dst_addr == sta.sta.iface_mac {
let _result = sta.send_ps_poll_frame(self.0.aid);
}
}
fn extract_and_record_signal_dbm(&mut self, rx_info: Option<banjo_wlan_mac::WlanRxInfo>) {
let rssi_dbm = match rx_info.and_then(|rx_info| ddk::get_rssi_dbm(rx_info)) {
Some(dbm) => dbm,
None => return,
};
self.0.signal_strength_average.add(DecibelMilliWatt(rssi_dbm));
}
/// Process and inbound beacon frame.
/// Resets LostBssCounter, check buffered frame if available.
fn on_beacon_frame<B: ByteSlice>(&mut self, sta: &mut BoundClient<'_>, elements: B) {
self.0.lost_bss_counter.reset();
for (id, body) in ie::Reader::new(elements) {
match id {
ie::Id::TIM => match ie::parse_tim(body) {
Ok(ie::TimView { header, bitmap }) => {
if tim::is_traffic_buffered(header.bmp_ctrl.offset(), &bitmap, self.0.aid) {
let _result = sta.send_ps_poll_frame(self.0.aid);
}
}
_ => (),
},
_ => (),
}
}
}
/// Extracts aggregated and non-aggregated MSDUs from the data frame.
/// Handles all data subtypes.
/// EAPoL MSDUs are forwarded to SME via an MLME-EAPOL.indication message independent of the
/// STA's current controlled port status.
/// All other MSDUs are converted into Ethernet II frames and forwarded via the device to
/// Fuchsia's Netstack if the STA's controlled port is open.
/// NULL-Data frames are interpreted as "Keep Alive" requests and responded with NULL data
/// frames if the STA's controlled port is open.
fn on_data_frame<B: ByteSlice>(
&self,
sta: &mut BoundClient<'_>,
fixed_data_fields: &mac::FixedDataHdrFields,
addr4: Option<mac::Addr4>,
qos_ctrl: Option<mac::QosControl>,
body: B,
) {
self.request_bu_if_available(
sta,
fixed_data_fields.frame_ctrl,
mac::data_dst_addr(&fixed_data_fields),
);
let msdus =
mac::MsduIterator::from_data_frame_parts(*fixed_data_fields, addr4, qos_ctrl, body);
// Handle NULL data frames independent of the controlled port's status.
if let mac::MsduIterator::Null = msdus {
if let Err(e) = sta.send_keep_alive_resp_frame() {
error!("error sending keep alive frame: {}", e);
}
}
// Handle aggregated and non-aggregated MSDUs.
for msdu in msdus {
let mac::Msdu { dst_addr, src_addr, llc_frame } = &msdu;
match llc_frame.hdr.protocol_id.to_native() {
// Forward EAPoL frames to SME independent of the controlled port's
// status.
mac::ETHER_TYPE_EAPOL => {
if let Err(e) =
sta.send_eapol_indication(*src_addr, *dst_addr, &llc_frame.body[..])
{
error!("error sending MLME-EAPOL.indication: {}", e);
}
}
// Deliver non-EAPoL MSDUs only if the controlled port is open.
_ if self.0.controlled_port_open => {
if let Err(e) = sta.deliver_msdu(msdu) {
error!("error while handling data frame: {}", e);
}
}
// Drop all non-EAPoL MSDUs if the controlled port is closed.
_ => (),
}
}
}
fn on_eth_frame<B: ByteSlice>(&self, sta: &mut BoundClient<'_>, frame: B) -> Result<(), Error> {
let mac::EthernetFrame { hdr, body } = match mac::EthernetFrame::parse(frame) {
Some(eth_frame) => eth_frame,
None => {
return Err(Error::Status(
format!("Ethernet frame too short"),
zx::Status::IO_DATA_INTEGRITY,
));
}
};
if !self.0.controlled_port_open {
return Err(Error::Status(
format!("Ethernet dropped. RSN not established"),
zx::Status::BAD_STATE,
));
}
sta.send_data_frame(
hdr.sa,
hdr.da,
sta.sta.is_rsn,
self.0.qos.is_enabled(),
hdr.ether_type.to_native(),
&body,
)
}
fn on_block_ack_frame<B: ByteSlice>(
&mut self,
sta: &mut BoundClient<'_>,
action: mac::BlockAckAction,
body: B,
) {
self.0.block_ack_state.replace_state(|state| state.on_block_ack_frame(sta, action, body));
}
/// Process an inbound MLME-FinalizeAssociation.request.
/// Derive an `AssociationContext` fromthe negotiated capabilities and
/// install the context in the underlying driver.
fn on_sme_finalize_association(
&mut self,
sta: &mut BoundClient<'_>,
cap: fidl_mlme::NegotiatedCapabilities,
) {
let Association { aid, ap_ht_op, ap_vht_op, .. } = self.0;
let assoc_ctx = ddk::build_ddk_assoc_ctx(sta.sta.bssid, aid, cap, ap_ht_op, ap_vht_op);
// TODO(29325): Determine for each outbound data frame,
// given the result of the dynamic capability negotiation, data frame
// classification, and QoS policy.
//
// Aruba / Ubiquiti are confirmed to be compatible with QoS field for the
// BlockAck session, independently of 40MHz operation.
self.0.qos = assoc_ctx.qos.into();
if let Err(status) = sta.ctx.device.configure_assoc(assoc_ctx) {
// Device cannot handle this association. Something is seriously wrong.
// TODO(eyw): Is this allowed? Should we deauthenticate or panic instead?
error!("device failed to configure association: {}", status);
}
}
fn on_sme_eapol(&self, sta: &mut BoundClient<'_>, req: fidl_mlme::EapolRequest) {
// Drop EAPoL frame if it is not a protected network.
if !sta.sta.is_rsn {
error!("Unexpected MLME-EAPOL.request message: BSS not protected");
return;
}
// There may be more EAPoL frames (such as key rotation) coming after EAPoL established.
// They need to be protected.
let protected = sta.sta.is_rsn && self.0.controlled_port_open;
sta.send_eapol_frame(req.src_addr, req.dst_addr, protected, &req.data);
}
fn on_sme_set_keys(&self, sta: &BoundClient<'_>, req: fidl_mlme::SetKeysRequest) {
if !sta.sta.is_rsn {
error!("Unexpected MLME-SetKeys.request message: BSS not protected");
return;
}
for key_desc in req.keylist {
if let Err(e) = sta.ctx.device.set_key(KeyConfig::from(&key_desc)) {
error!("failed to set keys in driver: {}", e);
}
}
}
fn on_sme_set_controlled_port(
&mut self,
sta: &BoundClient<'_>,
req: fidl_mlme::SetControlledPortRequest,
) {
if !sta.sta.is_rsn {
error!("Unexpected MLME-SetControlledPort.request message: BSS not protected.");
return;
}
let should_open_controlled_port = req.state == fidl_mlme::ControlledPortState::Open;
if should_open_controlled_port == self.0.controlled_port_open {
return;
}
self.0.controlled_port_open = should_open_controlled_port;
if let Err(e) = sta.ctx.device.set_eth_link(req.state.into()) {
error!(
"Error settting Ethernet port to {}: {}",
if should_open_controlled_port { "OPEN" } else { "CLOSED" },
e
);
}
}
fn on_sme_deauthenticate(
&mut self,
sta: &mut BoundClient<'_>,
req: fidl_mlme::DeauthenticateRequest,
) {
if let Err(e) = sta.send_deauth_frame(mac::ReasonCode(req.reason_code.into_primitive())) {
error!("Error sending deauthentication frame to BSS: {}", e);
}
self.pre_leaving_associated_state(sta);
if let Err(e) = sta.ctx.device.access_sme_sender(|sender| {
sender.send_deauthenticate_conf(&mut fidl_mlme::DeauthenticateConfirm {
peer_sta_address: sta.sta.bssid.0,
})
}) {
error!("Error sending MLME-DEAUTHENTICATE.confirm: {}", e)
}
}
fn pre_leaving_associated_state(&mut self, sta: &mut BoundClient<'_>) {
sta.ctx.timer.cancel_event(self.0.status_check_timeout.next_id);
self.0.controlled_port_open = false;
if let Err(e) = sta.ctx.device.set_eth_link_down() {
error!("Error disabling ethernet device offline: {}", e);
}
if let Err(e) = sta.ctx.device.clear_assoc(&sta.sta.bssid.0) {
error!("Error clearing association in vendor drvier: {}", e);
}
}
#[must_use]
/// Reports average signal strength to SME and check if auto deauthentication is due.
/// Returns true if there auto deauthentication is triggered by lack of beacon frames.
fn on_timeout(&mut self, sta: &mut BoundClient<'_>) -> bool {
// timeout should have been cancelled at this point, this is almost always a no-op.
sta.ctx.timer.cancel_event(self.0.status_check_timeout.next_id);
if let Err(e) = sta.ctx.device.access_sme_sender(|sender| {
sender.send_signal_report(&mut fidl_mlme::SignalReportIndication {
rssi_dbm: self.0.signal_strength_average.avg_dbm().0,
snr_db: 0,
})
}) {
error!("Error sending MLME-SignalReport: {}", e)
}
let auto_deauth = self.0.lost_bss_counter.should_deauthenticate();
if auto_deauth {
sta.send_deauthenticate_ind(fidl_mlme::ReasonCode::LeavingNetworkDeauth);
if let Err(e) = sta.send_deauth_frame(mac::ReasonCode::LEAVING_NETWORK_DEAUTH) {
warn!("Failed sending deauth frame {:?}", e);
}
self.pre_leaving_associated_state(sta);
} else {
// Always check should_deauthenticate() first since even if Client receives a beacon,
// it would still add a full association status check interval to the lost BSS counter.
self.0.lost_bss_counter.add_beacon_interval(ASSOCIATION_STATUS_TIMEOUT_BEACON_COUNT);
self.0.status_check_timeout =
schedule_association_status_timeout(sta.sta.beacon_period, &mut sta.ctx.timer);
}
auto_deauth
}
fn off_channel(&mut self, sta: &mut Client, ctx: &mut Context) {
if let Err(e) = sta.send_power_state_frame(ctx, PowerState::DOZE) {
warn!("unable to send doze frame: {:?}", e);
}
self.0.lost_bss_counter.add_time(ctx.timer.now() - self.0.status_check_timeout.last_fired);
ctx.timer.cancel_event(self.0.status_check_timeout.next_id);
}
fn on_channel(&mut self, sta: &mut Client, ctx: &mut Context) {
if let Err(e) = sta.send_power_state_frame(ctx, PowerState::AWAKE) {
warn!("unable to send awake frame: {:?}", e);
}
self.0.status_check_timeout =
schedule_association_status_timeout(sta.beacon_period, &mut ctx.timer);
}
}
statemachine!(
/// Client state machine.
pub enum States,
// Regular successful flow:
() => Joined,
Joined => Authenticating,
Authenticating => Authenticated,
Authenticated => Associating,
Associating => Associated,
// Multi-step authentication (SAE):
Authenticating => Authenticating,
// Timeout:
Authenticating => Joined,
Associating => Authenticated,
// Deauthentication:
Authenticating => Joined,
Authenticated => Joined,
Associating => Joined,
Associated => Joined,
// Disassociation:
Associating => Authenticated,
Associated => Authenticated,
);
impl States {
/// Returns the STA's initial state.
pub fn new_initial() -> States {
States::from(State::new(Joined))
}
/// Callback to process arbitrary IEEE 802.11 frames.
/// Frames are dropped if:
/// - frames are corrupted (too short)
/// - frames' frame class is not yet permitted
/// - frames are from a foreign BSS
/// - frames are unicast but destined for a MAC address that is different from this STA.
// TODO(43456): Implement a packet counter and add tests to verify frames are dropped correctly.
pub fn on_mac_frame<B: ByteSlice>(
mut self,
sta: &mut BoundClient<'_>,
bytes: B,
rx_info: Option<banjo_wlan_mac::WlanRxInfo>,
) -> States {
// For now, silently drop all frames when we are off channel.
if !sta.is_on_channel() {
return self;
}
let body_aligned = rx_info.map_or(false, |ri| {
(ri.rx_flags & banjo_wlan_mac::WlanRxInfoFlags::FRAME_BODY_PADDING_4.0) != 0
});
// Parse mac frame. Drop corrupted ones.
let mac_frame = match mac::MacFrame::parse(bytes, body_aligned) {
Some(mac_frame) => mac_frame,
None => return self,
};
if !sta.sta.should_handle_frame(&mac_frame) {
// While scanning, it is normal to receive mac frames from other BSS. Off-channel frames
// are already dropped at this point. No need to print error messages in this case.
if !sta.scanner.is_scanning() {
warn!("Mac frame is either from a foreign BSS or not destined for us. Dropped.");
}
return self;
}
// Drop frames which are not permitted in the STA's current state.
let frame_class = mac::FrameClass::from(&mac_frame);
if !self.is_frame_class_permitted(frame_class) {
return self;
}
match mac_frame {
mac::MacFrame::Mgmt { mgmt_hdr, body, .. } => {
self.on_mgmt_frame(sta, &mgmt_hdr, body, rx_info)
}
mac::MacFrame::Data { fixed_fields, addr4, qos_ctrl, body, .. } => {
if let States::Associated(state) = &mut self {
state.on_data_frame(
sta,
&fixed_fields,
addr4.map(|x| *x),
qos_ctrl.map(|x| x.get()),
body,
);
state.extract_and_record_signal_dbm(rx_info);
}
// Drop data frames in all other states
self
}
// Control frames are not yet supported. Drop them.
_ => self,
}
}
/// Processes inbound management frames.
/// Only frames from the joined BSS are processed. Frames from other STAs are dropped.
fn on_mgmt_frame<B: ByteSlice>(
self,
sta: &mut BoundClient<'_>,
mgmt_hdr: &mac::MgmtHdr,
body: B,
rx_info: Option<banjo_wlan_mac::WlanRxInfo>,
) -> States {
// Parse management frame. Drop corrupted ones.
let mgmt_body = match mac::MgmtBody::parse({ mgmt_hdr.frame_ctrl }.mgmt_subtype(), body) {
Some(x) => x,
None => return self,
};
match self {
States::Authenticating(state) => match mgmt_body {
mac::MgmtBody::Authentication { auth_hdr, .. } => {
match state.on_auth_frame(sta, &auth_hdr) {
Ok(()) => state.transition_to(Authenticated).into(),
Err(()) => state.transition_to(Joined).into(),
}
}
mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
state.on_deauth_frame(sta, &deauth_hdr);
state.transition_to(Joined).into()
}
_ => state.into(),
},
States::Authenticated(state) => match mgmt_body {
mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
state.on_deauth_frame(sta, &deauth_hdr);
state.transition_to(Joined).into()
}
_ => state.into(),
},
States::Associating(state) => match mgmt_body {
mac::MgmtBody::AssociationResp { assoc_resp_hdr, elements } => {
match state.on_assoc_resp_frame(sta, &assoc_resp_hdr, elements) {
Ok(association) => state.transition_to(Associated(association)).into(),
Err(()) => state.transition_to(Authenticated).into(),
}
}
mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
state.on_deauth_frame(sta, &deauth_hdr);
state.transition_to(Joined).into()
}
// This case is highly unlikely and only added to improve interoperability with
// buggy Access Points.
mac::MgmtBody::Disassociation { disassoc_hdr, .. } => {
state.on_disassoc_frame(sta, &disassoc_hdr);
state.transition_to(Authenticated).into()
}
_ => state.into(),
},
States::Associated(mut state) => {
state.extract_and_record_signal_dbm(rx_info);
state.on_any_mgmt_frame(sta, mgmt_hdr);
match mgmt_body {
mac::MgmtBody::Beacon { bcn_hdr: _, elements } => {
state.on_beacon_frame(sta, elements);
state.into()
}
mac::MgmtBody::Deauthentication { deauth_hdr, .. } => {
state.on_deauth_frame(sta, &deauth_hdr);
state.transition_to(Joined).into()
}
mac::MgmtBody::Disassociation { disassoc_hdr, .. } => {
state.on_disassoc_frame(sta, &disassoc_hdr);
state.transition_to(Authenticated).into()
}
mac::MgmtBody::Action { action_hdr, elements, .. } => match action_hdr.action {
mac::ActionCategory::BLOCK_ACK => {
let reader = BufferReader::new(elements);
if let Some(action) = reader.peek_unaligned::<mac::BlockAckAction>() {
state.on_block_ack_frame(
sta,
action.get(),
reader.into_remaining(),
);
}
state.into()
}
_ => state.into(),
},
_ => state.into(),
}
}
_ => self,
}
}
pub fn on_eth_frame<B: ByteSlice>(
&self,
sta: &mut BoundClient<'_>,
frame: B,
) -> Result<(), Error> {
match self {
States::Associated(state) if sta.is_on_channel() => state.on_eth_frame(sta, frame),
States::Associated(_state) => Err(Error::Status(
format!("Associated but not on main channel. Ethernet dropped"),
zx::Status::BAD_STATE,
)),
_ => Err(Error::Status(
format!("Not associated. Ethernet dropped"),
zx::Status::BAD_STATE,
)),
}
}
/// Callback when a previously scheduled event fired.
pub fn on_timed_event(self, sta: &mut BoundClient<'_>, event: TimedEvent) -> States {
match event {
TimedEvent::Authenticating => match self {
States::Authenticating(state) => {
state.on_timeout(sta);
state.transition_to(Joined).into()
}
_ => {
error!("received Authenticating timeout in unexpected state; ignoring timeout");
self
}
},
TimedEvent::Associating => match self {
States::Associating(state) => {
state.on_timeout(sta);
state.transition_to(Authenticated).into()
}
_ => {
error!("received Associating timeout in unexpected state; ignoring timeout");
self
}
},
TimedEvent::AssociationStatusCheck => {
match self {
States::Associated(mut state) => {
let should_auto_deauth = state.on_timeout(sta);
match should_auto_deauth {
true => state.transition_to(Joined).into(),
false => state.into(),
}
}
_ => {
error!("received association status update timeout in unexpected state; ignoring");
self
}
}
}
event => {
error!("unsupported event, {:?}, this should NOT happen", event);
self
}
}
}
#[allow(deprecated)] // Allow until main message loop is in Rust.
pub fn handle_mlme_msg(
self,
sta: &mut BoundClient<'_>,
msg: fidl_mlme::MlmeRequestMessage,
) -> States {
use fidl_mlme::MlmeRequestMessage as MlmeMsg;
match self {
States::Joined(state) => match msg {
MlmeMsg::AuthenticateReq { req } => {
match state.on_sme_authenticate(sta, req.auth_failure_timeout as u16) {
Ok(timeout) => state.transition_to(Authenticating { timeout }).into(),
Err(()) => state.into(),
}
}
_ => state.into(),
},
States::Authenticating(state) => match msg {
MlmeMsg::DeauthenticateReq { req: _ } => {
state.on_sme_deauthenticate(sta);
state.transition_to(Joined).into()
}
_ => state.into(),
},
States::Authenticated(state) => match msg {
MlmeMsg::AssociateReq { req } => match state.on_sme_associate(sta, req) {
Ok(timeout) => state.transition_to(Associating { timeout }).into(),
Err(()) => state.into(),
},
MlmeMsg::DeauthenticateReq { req } => {
state.on_sme_deauthenticate(sta, req);
state.transition_to(Joined).into()
}
_ => state.into(),
},
States::Associating(state) => match msg {
MlmeMsg::DeauthenticateReq { req: _ } => {
state.on_sme_deauthenticate(sta);
state.transition_to(Joined).into()
}
_ => state.into(),
},
States::Associated(mut state) => match msg {
MlmeMsg::FinalizeAssociationReq { cap } => {
state.on_sme_finalize_association(sta, cap);
state.into()
}
MlmeMsg::EapolReq { req } => {
state.on_sme_eapol(sta, req);
state.into()
}
MlmeMsg::SetKeysReq { req } => {
state.on_sme_set_keys(sta, req);
state.into()
}
MlmeMsg::SetControlledPort { req } => {
state.on_sme_set_controlled_port(sta, req);
state.into()
}
MlmeMsg::DeauthenticateReq { req } => {
state.on_sme_deauthenticate(sta, req);
state.transition_to(Joined).into()
}
_ => state.into(),
},
}
}
/// Returns |true| iff a given FrameClass is permitted to be processed in the current state.
fn is_frame_class_permitted(&self, class: mac::FrameClass) -> bool {
match self {
States::Joined(_) | States::Authenticating(_) => class == mac::FrameClass::Class1,
States::Authenticated(_) | States::Associating(_) => class <= mac::FrameClass::Class2,
States::Associated(_) => class <= mac::FrameClass::Class3,
}
}
pub fn pre_switch_off_channel(&mut self, sta: &mut Client, ctx: &mut Context) {
match self {
States::Associated(state) => {
state.off_channel(sta, ctx);
}
_ => (),
}
}
pub fn handle_back_on_channel(&mut self, sta: &mut Client, ctx: &mut Context) {
match self {
States::Associated(state) => {
state.on_channel(sta, ctx);
}
_ => (),
}
}
}
#[cfg(test)]
mod free_function_tests {
use super::*;
#[test]
fn test_extract_ht_vht_op_success() {
let mut buf = Vec::<u8>::new();
ie::write_ht_operation(&mut buf, &ie::fake_ht_operation()).expect("valid HT Op");
ie::write_vht_operation(&mut buf, &ie::fake_vht_operation()).expect("valid VHT Op");
let (ht_bytes, vht_bytes) = extract_ht_vht_op(&buf[..]);
assert_eq!(ht_bytes.unwrap(), ie::fake_ht_op_bytes());
assert_eq!(vht_bytes.unwrap(), ie::fake_vht_op_bytes());
}
#[test]
fn test_extract_ht_op_too_short() {
let mut buf = Vec::<u8>::new();
ie::write_ht_operation(&mut buf, &ie::fake_ht_operation()).expect("valid HT Op");
buf[1] -= 1; // Make length shorter
buf.truncate(buf.len() - 1);
ie::write_vht_operation(&mut buf, &ie::fake_vht_operation()).expect("valid VHT Op");
let (ht_bytes, vht_bytes) = extract_ht_vht_op(&buf[..]);
assert_eq!(ht_bytes, None);
assert_eq!(vht_bytes.unwrap(), ie::fake_vht_op_bytes());
}
#[test]
fn test_extract_vht_op_too_short() {
let mut buf = Vec::<u8>::new();
ie::write_ht_operation(&mut buf, &ie::fake_ht_operation()).expect("valid HT Op");
let ht_end = buf.len();
ie::write_vht_operation(&mut buf, &ie::fake_vht_operation()).expect("valid VHT Op");
buf[ht_end + 1] -= 1; // Make VHT operation shorter.
buf.truncate(buf.len() - 1);
let (ht_bytes, vht_bytes) = extract_ht_vht_op(&buf[..]);
assert_eq!(ht_bytes.unwrap(), ie::fake_ht_op_bytes());
assert_eq!(vht_bytes, None);
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
block_ack::{write_addba_req_body, ADDBA_REQ_FRAME_LEN},
buffer::FakeBufferProvider,
client::{
channel_listener::ChannelListenerState, channel_scheduler::ChannelScheduler,
scanner::Scanner, Client, ClientConfig, Context,
},
device::{Device, FakeDevice},
},
banjo_ddk_protocol_wlan_info as banjo_wlan_info, fidl_fuchsia_wlan_common as fidl_common,
fuchsia_zircon::{self as zx, DurationNum},
wlan_common::{
assert_variant,
buffer_writer::BufferWriter,
mac::{Bssid, MacAddr},
mgmt_writer,
sequence::SequenceManager,
test_utils::fake_frames::*,
},
wlan_frame_writer::write_frame_with_dynamic_buf,
wlan_statemachine as statemachine,
};
const BSSID: Bssid = Bssid([6u8; 6]);
const IFACE_MAC: MacAddr = [3u8; 6];
struct MockObjects {
fake_device: FakeDevice,
fake_scheduler: FakeScheduler,
scanner: Scanner,
chan_sched: ChannelScheduler,
channel_state: ChannelListenerState,
}
impl MockObjects {
fn new() -> Self {
Self {
fake_device: FakeDevice::new(),
fake_scheduler: FakeScheduler::new(),
scanner: Scanner::new(IFACE_MAC),
chan_sched: ChannelScheduler::new(),
channel_state: Default::default(),
}
}
fn make_ctx(&mut self) -> Context {
let device = self.fake_device.as_device();
device.set_channel(fake_wlan_channel()).expect("fake device is obedient");
self.channel_state.main_channel = Some(fake_wlan_channel());
self.make_ctx_with_device(device)
}
fn make_ctx_with_device(&mut self, device: Device) -> Context {
let timer = Timer::<TimedEvent>::new(self.fake_scheduler.as_scheduler());
Context {
config: ClientConfig { ensure_on_channel_time: 0 },
device,
buf_provider: FakeBufferProvider::new(),
timer,
seq_mgr: SequenceManager::new(),
}
}
}
fn fake_wlan_channel() -> banjo_wlan_info::WlanChannel {
banjo_wlan_info::WlanChannel {
primary: 0,
cbw: banjo_wlan_info::WlanChannelBandwidth(0),
secondary80: 0,
}
}
fn make_client_station() -> Client {
Client::new(vec![], BSSID, IFACE_MAC, TimeUnit::DEFAULT_BEACON_INTERVAL.into(), false)
}
fn make_protected_client_station() -> Client {
Client::new(vec![], BSSID, IFACE_MAC, TimeUnit::DEFAULT_BEACON_INTERVAL.into(), true)
}
fn empty_associate_conf() -> fidl_mlme::AssociateConfirm {
fidl_mlme::AssociateConfirm {
association_id: 0,
result_code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
cap_info: 0,
rates: vec![],
ht_cap: None,
vht_cap: None,
}
}
fn empty_association(sta: &mut BoundClient<'_>) -> Association {
let status_check_timeout =
schedule_association_status_timeout(sta.sta.beacon_period, &mut sta.ctx.timer);
Association {
controlled_port_open: false,
aid: 0,
ap_ht_op: None,
ap_vht_op: None,
lost_bss_counter: LostBssCounter::start(
sta.sta.beacon_period,
DEFAULT_AUTO_DEAUTH_TIMEOUT_BEACON_COUNT,
),
qos: Qos::Disabled,
status_check_timeout,
signal_strength_average: SignalStrengthAverage::new(),
block_ack_state: StateMachine::new(BlockAckState::from(State::new(Closed))),
}
}
fn fake_ddk_assoc_ctx() -> banjo_wlan_info::WlanAssocCtx {
ddk::build_ddk_assoc_ctx(
BSSID,
42,
fidl_mlme::NegotiatedCapabilities {
channel: fidl_common::WlanChan {
primary: 149,
cbw: fidl_common::Cbw::Cbw40,
secondary80: 42,
},
cap_info: 0,
rates: vec![],
ht_cap: None,
vht_cap: None,
},
None,
None,
)
}
#[allow(deprecated)] // Raw MLME messages are deprecated.
fn fake_mlme_deauth_req() -> fidl_mlme::MlmeRequestMessage {
fidl_mlme::MlmeRequestMessage::DeauthenticateReq {
req: fidl_mlme::DeauthenticateRequest {
peer_sta_address: BSSID.0,
reason_code: fidl_mlme::ReasonCode::LeavingNetworkDeauth,
},
}
}
fn empty_authenticate_request() -> fidl_mlme::AuthenticateRequest {
fidl_mlme::AuthenticateRequest {
peer_sta_address: [0; 6],
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
auth_failure_timeout: 0,
auth_content: None,
}
}
fn empty_associate_request() -> fidl_mlme::AssociateRequest {
fidl_mlme::AssociateRequest {
peer_sta_address: [1, 2, 3, 4, 5, 6],
cap_info: 0,
rates: vec![1, 2, 3],
ht_cap: None,
vht_cap: None,
rsne: None,
vendor_ies: None,
}
}
#[test]
fn join_state_authenticate_success() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Joined;
let timeout_id = state.on_sme_authenticate(&mut sta, 10).expect("failed authenticating");
// Verify an event was queued up in the timer.
assert_variant!(sta.ctx.timer.triggered(&timeout_id), Some(TimedEvent::Authenticating));
// Verify authentication frame was sent to AP.
assert_eq!(m.fake_device.wlan_queue.len(), 1);
let (frame, _txflags) = m.fake_device.wlan_queue.remove(0);
#[rustfmt::skip]
let expected = vec![
// Mgmt Header:
0b1011_00_00, 0b00000000, // Frame Control
0, 0, // Duration
6, 6, 6, 6, 6, 6, // Addr1
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Auth Header:
0, 0, // Algorithm Number (Open)
1, 0, // Txn Sequence Number
0, 0, // Status Code
];
assert_eq!(&frame[..], &expected[..]);
// Verify no MLME message was sent yet.
m.fake_device
.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>()
.expect_err("unexpected message");
}
#[test]
fn join_state_authenticate_tx_failure() {
let mut m = MockObjects::new();
let device = m.fake_device.as_device_fail_wlan_tx();
let mut ctx = m.make_ctx_with_device(device);
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Joined;
state.on_sme_authenticate(&mut sta, 10).expect_err("should fail authenticating");
// Verify no event was queued up in the timer.
assert_eq!(sta.ctx.timer.scheduled_event_count(), 0);
// Verify MLME-AUTHENTICATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AuthenticateConfirm {
peer_sta_address: BSSID.0,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::Refused,
auth_content: None,
}
);
}
#[test]
fn authenticating_state_auth_success() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Authenticating);
let state = Authenticating { timeout };
// Verify authentication was considered successful.
state
.on_auth_frame(
&mut sta,
&mac::AuthHdr {
auth_alg_num: mac::AuthAlgorithmNumber::OPEN,
auth_txn_seq_num: 2,
status_code: mac::StatusCode::SUCCESS,
},
)
.expect("failed processing auth frame");
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-AUTHENTICATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AuthenticateConfirm {
peer_sta_address: BSSID.0,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::Success,
auth_content: None,
}
);
}
#[test]
fn authenticating_state_auth_rejected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Authenticating);
let state = Authenticating { timeout };
// Verify authentication was considered successful.
state
.on_auth_frame(
&mut sta,
&mac::AuthHdr {
auth_alg_num: mac::AuthAlgorithmNumber::OPEN,
auth_txn_seq_num: 2,
status_code: mac::StatusCode::NOT_IN_SAME_BSS,
},
)
.expect_err("expected failure processing auth frame");
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-AUTHENTICATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AuthenticateConfirm {
peer_sta_address: BSSID.0,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::AuthenticationRejected,
auth_content: None,
}
);
}
#[test]
fn authenticating_state_timeout() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Authenticating);
let state = Authenticating { timeout };
state.on_timeout(&mut sta);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-AUTHENTICATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AuthenticateConfirm {
peer_sta_address: BSSID.0,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::AuthFailureTimeout,
auth_content: None,
}
);
}
#[test]
fn authenticating_state_deauth_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Authenticating);
let state = Authenticating { timeout };
state.on_deauth_frame(
&mut sta,
&mac::DeauthHdr { reason_code: mac::ReasonCode::NO_MORE_STAS },
);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-AUTHENTICATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::AuthenticateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AuthenticateConfirm {
peer_sta_address: BSSID.0,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::Refused,
auth_content: None,
}
);
}
#[test]
fn authenticated_state_deauth_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Authenticated;
state.on_deauth_frame(
&mut sta,
&mac::DeauthHdr { reason_code: mac::ReasonCode::NO_MORE_STAS },
);
// Verify MLME-DEAUTHENTICATE.indication message was sent.
let msg = m
.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
.expect("no message");
assert_eq!(
msg,
fidl_mlme::DeauthenticateIndication {
peer_sta_address: BSSID.0,
reason_code: fidl_mlme::ReasonCode::NoMoreStas,
}
);
}
#[test]
fn associating_success_unprotected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout = sta
.ctx
.timer
.schedule_event(sta.ctx.timer.now() + 1.seconds(), TimedEvent::Associating);
let state = Associating { timeout };
let Association { aid, controlled_port_open, .. } = state
.on_assoc_resp_frame(
&mut sta,
&mac::AssocRespHdr {
aid: 42,
capabilities: mac::CapabilityInfo(52),
status_code: mac::StatusCode::SUCCESS,
},
&[][..],
)
.expect("failed processing association response frame");
assert_eq!(aid, 42);
assert_eq!(true, controlled_port_open);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 42,
result_code: fidl_mlme::AssociateResultCodes::Success,
cap_info: 52,
..empty_associate_conf()
}
);
}
#[test]
fn associating_success_protected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = Associating { timeout };
let Association { aid, controlled_port_open, .. } = state
.on_assoc_resp_frame(
&mut sta,
&mac::AssocRespHdr {
aid: 42,
capabilities: mac::CapabilityInfo(52),
status_code: mac::StatusCode::SUCCESS,
},
&[][..],
)
.expect("failed processing association response frame");
assert_eq!(aid, 42);
assert_eq!(false, controlled_port_open);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 42,
result_code: fidl_mlme::AssociateResultCodes::Success,
cap_info: 52,
..empty_associate_conf()
}
);
}
#[test]
fn associating_failure() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = Associating { timeout };
// Verify authentication was considered successful.
state
.on_assoc_resp_frame(
&mut sta,
&mac::AssocRespHdr {
aid: 42,
capabilities: mac::CapabilityInfo(52),
status_code: mac::StatusCode::NOT_IN_SAME_BSS,
},
&[][..],
)
.expect_err("expected failure processing association response frame");
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 0,
result_code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
..empty_associate_conf()
}
);
}
#[test]
fn associating_timeout() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = Associating { timeout };
// Trigger timeout.
state.on_timeout(&mut sta);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 0,
result_code: fidl_mlme::AssociateResultCodes::RefusedTemporarily,
..empty_associate_conf()
}
);
}
#[test]
fn associating_deauth_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = Associating { timeout };
state.on_deauth_frame(
&mut sta,
&mac::DeauthHdr { reason_code: mac::ReasonCode::AP_INITIATED },
);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 0,
result_code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
..empty_associate_conf()
}
);
}
#[test]
fn associating_disassociation() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = Associating { timeout };
state.on_disassoc_frame(
&mut sta,
&mac::DisassocHdr { reason_code: mac::ReasonCode::AP_INITIATED },
);
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m.fake_device.next_mlme_msg::<fidl_mlme::AssociateConfirm>().expect("no message");
assert_eq!(
msg,
fidl_mlme::AssociateConfirm {
association_id: 0,
result_code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
..empty_associate_conf()
}
);
}
#[test]
fn associated_block_ack_frame() {
let mut mock = MockObjects::new();
let mut ctx = mock.make_ctx();
let mut station = make_client_station();
let mut client = station.bind(
&mut ctx,
&mut mock.scanner,
&mut mock.chan_sched,
&mut mock.channel_state,
);
let frame = {
let mut buffer = [0u8; ADDBA_REQ_FRAME_LEN];
let writer = BufferWriter::new(&mut buffer[..]);
let (mut writer, _) = write_frame_with_dynamic_buf!(
writer,
{
headers: {
mac::MgmtHdr: &mgmt_writer::mgmt_hdr_from_ap(
mac::FrameControl(0)
.with_frame_type(mac::FrameType::MGMT)
.with_mgmt_subtype(mac::MgmtSubtype::ACTION),
client.sta.iface_mac,
client.sta.bssid,
mac::SequenceControl(0)
.with_seq_num(client.ctx.seq_mgr.next_sns1(&client.sta.bssid.0) as u16),
),
},
}
)
.unwrap();
write_addba_req_body(&mut writer, 1).unwrap();
buffer
};
let state = States::from(statemachine::testing::new_state(Associated(empty_association(
&mut client,
))));
match state.on_mac_frame(&mut client, &frame[..], None) {
States::Associated(state) => {
let (_, associated) = state.release_data();
match *associated.0.block_ack_state.as_ref() {
BlockAckState::Established(_) => {}
_ => panic!("client has not established BlockAck"),
}
}
_ => panic!("client no longer associated"),
}
}
#[test]
fn associated_deauth_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = Associated(empty_association(&mut sta));
// ddk_assoc_ctx will be cleared when MLME receives deauth frame.
sta.ctx
.device
.configure_assoc(fake_ddk_assoc_ctx())
.expect("valid assoc_ctx should succeed");
assert_eq!(1, m.fake_device.assocs.len());
sta.ctx.device.set_eth_link_up().expect("should succeed");
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::UP);
state.on_deauth_frame(
&mut sta,
&mac::DeauthHdr { reason_code: mac::ReasonCode::AP_INITIATED },
);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg = m
.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
.expect("no message");
assert_eq!(
msg,
fidl_mlme::DeauthenticateIndication {
peer_sta_address: BSSID.0,
reason_code: fidl_mlme::ReasonCode::ApInitiated,
}
);
// Verify association context is cleared and ethernet port is shut down.
assert_eq!(0, m.fake_device.assocs.len());
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
}
#[test]
fn associated_disassociation() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = Associated(empty_association(&mut sta));
state.0.controlled_port_open = true;
// ddk_assoc_ctx must be cleared when MLME receives disassociation frame later.
sta.ctx
.device
.configure_assoc(fake_ddk_assoc_ctx())
.expect("valid assoc_ctx should succeed");
assert_eq!(1, m.fake_device.assocs.len());
sta.ctx.device.set_eth_link_up().expect("should succeed");
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::UP);
state.on_disassoc_frame(
&mut sta,
&mac::DisassocHdr { reason_code: mac::ReasonCode::AP_INITIATED },
);
// Verify MLME-ASSOCIATE.confirm message was sent.
let msg =
m.fake_device.next_mlme_msg::<fidl_mlme::DisassociateIndication>().expect("no message");
assert_eq!(
msg,
fidl_mlme::DisassociateIndication {
peer_sta_address: BSSID.0,
reason_code: mac::ReasonCode::AP_INITIATED.0,
}
);
// Verify association everything is properly cleared.
assert_eq!(0, sta.ctx.timer.scheduled_event_count());
assert_eq!(false, state.0.controlled_port_open);
assert_eq!(0, m.fake_device.assocs.len());
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
}
#[test]
fn associated_move_data_closed_controlled_port() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Associated(empty_association(&mut sta));
let data_frame = make_data_frame_single_llc(None, None);
let (fixed, addr4, qos, body) = parse_data_frame(&data_frame[..]);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
// Verify data frame was dropped.
assert_eq!(m.fake_device.eth_queue.len(), 0);
}
#[test]
fn associated_move_data_opened_controlled_port() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
let data_frame = make_data_frame_single_llc(None, None);
let (fixed, addr4, qos, body) = parse_data_frame(&data_frame[..]);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
// Verify data frame was processed.
assert_eq!(m.fake_device.eth_queue.len(), 1);
#[rustfmt::skip]
assert_eq!(m.fake_device.eth_queue[0], [
3, 3, 3, 3, 3, 3, // dst_addr
4, 4, 4, 4, 4, 4, // src_addr
9, 10, // ether_type
11, 11, 11, // payload
]);
}
#[test]
fn associated_handle_eapol_closed_controlled_port() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Associated(empty_association(&mut sta));
let (src_addr, dst_addr, eapol_frame) = make_eapol_frame(IFACE_MAC);
let (fixed, addr4, qos, body) = parse_data_frame(&eapol_frame[..]);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
// Verify EAPOL frame was not sent to netstack.
assert_eq!(m.fake_device.eth_queue.len(), 0);
// Verify EAPoL frame was sent to SME.
let eapol_ind = m
.fake_device
.next_mlme_msg::<fidl_mlme::EapolIndication>()
.expect("error reading EAPOL.indication");
assert_eq!(
eapol_ind,
fidl_mlme::EapolIndication { src_addr, dst_addr, data: EAPOL_PDU.to_vec() }
);
}
#[test]
fn associated_handle_eapol_open_controlled_port() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Associated(empty_association(&mut sta));
let (src_addr, dst_addr, eapol_frame) = make_eapol_frame(IFACE_MAC);
let (fixed, addr4, qos, body) = parse_data_frame(&eapol_frame[..]);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
// Verify EAPOL frame was not sent to netstack.
assert_eq!(m.fake_device.eth_queue.len(), 0);
// Verify EAPoL frame was sent to SME.
let eapol_ind = m
.fake_device
.next_mlme_msg::<fidl_mlme::EapolIndication>()
.expect("error reading EAPOL.indication");
assert_eq!(
eapol_ind,
fidl_mlme::EapolIndication { src_addr, dst_addr, data: EAPOL_PDU.to_vec() }
);
}
#[test]
fn associated_handle_amsdus_open_controlled_port() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
Associated(Association { controlled_port_open: true, ..empty_association(&mut sta) });
let data_frame = make_data_frame_amsdu();
let (fixed, addr4, qos, body) = parse_data_frame(&data_frame[..]);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
let queue = &m.fake_device.eth_queue;
assert_eq!(queue.len(), 2);
#[rustfmt::skip]
let mut expected_first_eth_frame = vec![
0x78, 0x8a, 0x20, 0x0d, 0x67, 0x03, // dst_addr
0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xab, // src_addr
0x08, 0x00, // ether_type
];
expected_first_eth_frame.extend_from_slice(MSDU_1_PAYLOAD);
assert_eq!(queue[0], &expected_first_eth_frame[..]);
#[rustfmt::skip]
let mut expected_second_eth_frame = vec![
0x78, 0x8a, 0x20, 0x0d, 0x67, 0x04, // dst_addr
0xb4, 0xf7, 0xa1, 0xbe, 0xb9, 0xac, // src_addr
0x08, 0x01, // ether_type
];
expected_second_eth_frame.extend_from_slice(MSDU_2_PAYLOAD);
assert_eq!(queue[1], &expected_second_eth_frame[..]);
}
#[test]
fn associated_request_bu_data_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Associated(Association {
aid: 42,
controlled_port_open: true,
..empty_association(&mut sta)
});
let data_frame = make_data_frame_single_llc(None, None);
let (mut fixed, addr4, qos, body) = parse_data_frame(&data_frame[..]);
fixed.frame_ctrl = fixed.frame_ctrl.with_more_data(true);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
assert_eq!(m.fake_device.wlan_queue.len(), 1);
#[rustfmt::skip]
assert_eq!(&m
.fake_device.wlan_queue[0].0[..], &[
// Frame Control:
0b10100100, 0b00000000, // FC
42, 0b11_000000, // Id
6, 6, 6, 6, 6, 6, // addr1
3, 3, 3, 3, 3, 3, // addr2
][..]);
}
#[test]
fn associated_request_bu_mgmt_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = Associated(Association {
aid: 42,
controlled_port_open: true,
..empty_association(&mut sta)
});
state.on_any_mgmt_frame(
&mut sta,
&mac::MgmtHdr {
frame_ctrl: mac::FrameControl(0)
.with_frame_type(mac::FrameType::MGMT)
.with_mgmt_subtype(mac::MgmtSubtype::BEACON)
.with_more_data(true),
duration: 0,
addr1: [3; 6],
addr2: BSSID.0,
addr3: BSSID.0,
seq_ctrl: mac::SequenceControl(0),
},
);
assert_eq!(m.fake_device.wlan_queue.len(), 1);
#[rustfmt::skip]
assert_eq!(&m
.fake_device.wlan_queue[0].0[..], &[
// Frame Control:
0b10100100, 0b00000000, // FC
42, 0b11_000000, // Id
6, 6, 6, 6, 6, 6, // addr1
3, 3, 3, 3, 3, 3, // addr2
][..]);
}
#[test]
fn associated_no_bu_request() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
// Closed Controlled port
let state = Associated(empty_association(&mut sta));
let data_frame = make_data_frame_single_llc(None, None);
let (mut fixed, addr4, qos, body) = parse_data_frame(&data_frame[..]);
fixed.frame_ctrl = fixed.frame_ctrl.with_more_data(true);
state.on_data_frame(&mut sta, &fixed, addr4, qos, body);
assert_eq!(m.fake_device.wlan_queue.len(), 0);
// Foreign management frame
let state = States::from(statemachine::testing::new_state(Associated(Association {
controlled_port_open: true,
..empty_association(&mut sta)
})));
#[rustfmt::skip]
let beacon = vec![
// Mgmt Header:
0b1000_00_00, 0b00100000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1
7, 7, 7, 7, 7, 7, // Addr2
5, 5, 5, 5, 5, 5, // Addr3
0x10, 0, // Sequence Control
// Omit IEs
];
state.on_mac_frame(&mut sta, &beacon[..], None);
assert_eq!(m.fake_device.wlan_queue.len(), 0);
}
#[test]
fn associated_drop_foreign_data_frames() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
// Foreign data frame
let state = States::from(statemachine::testing::new_state(Associated(Association {
aid: 42,
controlled_port_open: true,
..empty_association(&mut sta)
})));
let fc = mac::FrameControl(0)
.with_frame_type(mac::FrameType::DATA)
.with_data_subtype(mac::DataSubtype(0))
.with_from_ds(true);
let fc = fc.0.to_le_bytes();
// Send data frame from an address other than the BSSID([6u8; 6]).
let bytes = vec![
// Data Header
fc[0], fc[1], // fc
2, 2, // duration
3, 3, 3, 3, 3, 3, // addr1
4, 4, 4, 4, 4, 4, // addr2
5, 5, 5, 5, 5, 5, // addr3
6, 6, // sequence control
// LLC Header
7, 7, 7, // DSAP, SSAP & control
8, 8, 8, // OUI
9, 10, // eth type
// Trailing bytes
11, 11, 11,
];
state.on_mac_frame(&mut sta, &bytes[..], None);
assert_eq!(m.fake_device.eth_queue.len(), 0);
}
#[test]
#[allow(deprecated)] // for raw MlmeRequestMessage
fn state_transitions_joined_authing() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::new_initial();
assert_variant!(state, States::Joined(_), "not in joined state");
// Successful: Joined > Authenticating
state = state.handle_mlme_msg(
&mut sta,
fidl_mlme::MlmeRequestMessage::AuthenticateReq {
req: fidl_mlme::AuthenticateRequest {
auth_failure_timeout: 10,
..empty_authenticate_request()
},
},
);
assert_variant!(state, States::Authenticating(_), "not in auth'ing state");
}
#[test]
fn state_transitions_authing_success() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
// Successful: Joined > Authenticating > Authenticated
#[rustfmt::skip]
let auth_resp_success = vec![
// Mgmt Header:
0b1011_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Auth Header:
0, 0, // Algorithm Number (Open)
2, 0, // Txn Sequence Number
0, 0, // Status Code
];
state = state.on_mac_frame(&mut sta, &auth_resp_success[..], None);
assert_variant!(state, States::Authenticated(_), "not in auth'ed state");
}
#[test]
fn state_transitions_authing_failure() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
// Failure: Joined > Authenticating > Joined
#[rustfmt::skip]
let auth_resp_failure = vec![
// Mgmt Header:
0b1011_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Auth Header:
0, 0, // Algorithm Number (Open)
2, 0, // Txn Sequence Number
42, 0, // Status Code
];
state = state.on_mac_frame(&mut sta, &auth_resp_failure[..], None);
assert_variant!(state, States::Joined(_), "not in joined state");
}
#[test]
#[allow(deprecated)] // for raw MlmeRequestMessage
fn state_transitions_authing_timeout() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::new_initial();
assert_variant!(state, States::Joined(_), "not in joined state");
// Timeout: Joined > Authenticating > Joined
state = state.handle_mlme_msg(
&mut sta,
fidl_mlme::MlmeRequestMessage::AuthenticateReq {
req: fidl_mlme::AuthenticateRequest {
auth_failure_timeout: 10,
..empty_authenticate_request()
},
},
);
let timeout_id = assert_variant!(state, States::Authenticating(ref state) => {
state.timeout
}, "not in auth'ing state");
let event = sta.ctx.timer.triggered(&timeout_id);
assert_variant!(event, Some(TimedEvent::Authenticating));
state = state.on_timed_event(&mut sta, event.unwrap());
assert_variant!(state, States::Joined(_), "not in joined state");
}
#[test]
fn state_transitions_authing_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
// Deauthenticate: Authenticating > Joined
#[rustfmt::skip]
let deauth = vec![
// Mgmt Header:
0b1100_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Deauth Header:
5, 0, // Algorithm Number (Open)
];
state = state.on_mac_frame(&mut sta, &deauth[..], None);
assert_variant!(state, States::Joined(_), "not in joined state");
}
#[test]
fn state_transitions_authed() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticated));
// Deauthenticate: Authenticated > Joined
#[rustfmt::skip]
let deauth = vec![
// Mgmt Header:
0b1100_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Deauth Header:
5, 0, // Algorithm Number (Open)
];
state = state.on_mac_frame(&mut sta, &deauth[..], None);
assert_variant!(state, States::Joined(_), "not in joined state");
}
#[test]
fn state_transitions_foreign_auth_resp() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
// Send foreign auth response. State should not change.
#[rustfmt::skip]
let auth_resp_success = vec![
// Mgmt Header:
0b1011_00_00, 0b00000000, // Frame Control
0, 0, // Duration
5, 5, 5, 5, 5, 5, // Addr1
3, 3, 3, 3, 3, 3, // Addr2
5, 5, 5, 5, 5, 5, // Addr3
0x10, 0, // Sequence Control
// Auth Header:
0, 0, // Algorithm Number (Open)
2, 0, // Txn Sequence Number
0, 0, // Status Code
];
state = state.on_mac_frame(&mut sta, &auth_resp_success[..], None);
assert_variant!(state, States::Authenticating(_), "not in auth'ing state");
// Verify that an authentication response from the joined BSS still moves the Client
// forward.
#[rustfmt::skip]
let auth_resp_success = vec![
// Mgmt Header:
0b1011_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Auth Header:
0, 0, // Algorithm Number (Open)
2, 0, // Txn Sequence Number
0, 0, // Status Code
];
state = state.on_mac_frame(&mut sta, &auth_resp_success[..], None);
assert_variant!(state, States::Authenticated(_), "not in auth'ed state");
}
#[test]
fn state_transitions_associng_success() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
// Successful: Associating > Associated
#[rustfmt::skip]
let assoc_resp_success = vec![
// Mgmt Header:
0b0001_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Assoc Resp Header:
0, 0, // Capabilities
0, 0, // Status Code
0, 0, // AID
];
state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], None);
assert_variant!(state, States::Associated(_), "not in associated state");
}
#[test]
fn state_transitions_associng_failure() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
// Failure: Associating > Authenticated
#[rustfmt::skip]
let assoc_resp_success = vec![
// Mgmt Header:
0b0001_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Assoc Resp Header:
0, 0, // Capabilities
2, 0, // Status Code (Failed)
0, 0, // AID
];
state = state.on_mac_frame(&mut sta, &assoc_resp_success[..], None);
assert_variant!(state, States::Authenticated(_), "not in authenticated state");
}
#[test]
#[allow(deprecated)] // for raw MlmeRequestMessage
fn state_transitions_associng_timeout() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Authenticated));
state = state.handle_mlme_msg(
&mut sta,
fidl_mlme::MlmeRequestMessage::AssociateReq { req: empty_associate_request() },
);
let timeout_id = assert_variant!(state, States::Associating(ref state) => {
state.timeout.clone()
}, "not in assoc'ing state");
let event = sta.ctx.timer.triggered(&timeout_id);
assert_variant!(event, Some(TimedEvent::Associating));
state = state.on_timed_event(&mut sta, event.unwrap());
assert_variant!(state, States::Authenticated(_), "not in authenticated state");
}
#[test]
fn state_transitions_associng_deauthing() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
// Deauthentication: Associating > Joined
#[rustfmt::skip]
let deauth = vec![
// Mgmt Header:
0b1100_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Deauth Header:
4, 0, // Reason Code
];
state = state.on_mac_frame(&mut sta, &deauth[..], None);
assert_variant!(state, States::Joined(_), "not in joined state");
}
#[test]
fn state_transitions_assoced_disassoc() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
// Disassociation: Associated > Authenticated
#[rustfmt::skip]
let disassoc = vec![
// Mgmt Header:
0b1010_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Deauth Header:
4, 0, // Reason Code
];
state = state.on_mac_frame(&mut sta, &disassoc[..], None);
assert_variant!(state, States::Authenticated(_), "not in authenticated state");
}
#[test]
fn state_transitions_assoced_deauthing() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let mut state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
// Deauthentication: Associated > Joined
#[rustfmt::skip]
let deauth = vec![
// Mgmt Header:
0b1100_00_00, 0b00000000, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
3, 3, 3, 3, 3, 3, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0x10, 0, // Sequence Control
// Deauth Header:
4, 0, // Reason Code
];
state = state.on_mac_frame(&mut sta, &deauth[..], None);
assert_variant!(state, States::Joined(_), "not in joined state");
}
fn parse_data_frame(
bytes: &[u8],
) -> (mac::FixedDataHdrFields, Option<MacAddr>, Option<mac::QosControl>, &[u8]) {
let parsed_frame = mac::MacFrame::parse(bytes, false).expect("invalid frame");
match parsed_frame {
mac::MacFrame::Data { fixed_fields, addr4, qos_ctrl, body, .. } => {
(*fixed_fields, addr4.map(|x| *x), qos_ctrl.map(|x| x.get()), body)
}
_ => panic!("error parsing data frame"),
}
}
#[test]
fn assoc_send_eth_frame_becomes_data_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Associated(Association {
controlled_port_open: true,
..empty_association(&mut sta)
})));
let eth_frame = [
1, 2, 3, 4, 5, 6, // dst_addr
11, 12, 13, 14, 15, 16, // src_addr
0x0d, 0x05, // ether_type
21, 22, 23, 24, 25, 26, 27, 28, // payload
29, // more payload
];
assert_eq!((), state.on_eth_frame(&mut sta, &eth_frame[..]).expect("all good"));
assert_eq!(m.fake_device.wlan_queue.len(), 1);
let (data_frame, _tx_flags) = m.fake_device.wlan_queue.remove(0);
assert_eq!(
&data_frame[..],
&[
// Data header
0b00001000, 0b00000001, // Frame Control
0, 0, // Duration
6, 6, 6, 6, 6, 6, // addr1
11, 12, 13, 14, 15, 16, // addr2 (from src_addr above)
1, 2, 3, 4, 5, 6, // addr3 (from dst_addr above)
0x10, 0, // Sequence Control
// LLC header
0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
0, 0, 0, // OUI
0x0d, 0x05, // Protocol ID (from ether_type above)
21, 22, 23, 24, 25, 26, 27, 28, // Payload
29, // More payload
][..]
)
}
#[test]
fn eth_frame_dropped_when_off_channel() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
sta.ctx
.device
.set_channel(banjo_wlan_info::WlanChannel { primary: 42, ..fake_wlan_channel() })
.expect("fake device is obedient");
let eth_frame = &[100; 14]; // An ethernet frame must be at least 14 bytes long.
let error = state
.on_eth_frame(&mut sta, &eth_frame[..])
.expect_err("Ethernet frame is dropped when client is off channel");
assert_variant!(error, Error::Status(_str, status) =>
assert_eq!(status, zx::Status::BAD_STATE),
"error should contain a status"
);
}
#[test]
fn assoc_eth_frame_too_short_dropped() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let eth_frame = &[100; 13]; // Needs at least 14 bytes for header.
let error =
state.on_eth_frame(&mut sta, &eth_frame[..]).expect_err("Ethernet frame is too short");
assert_variant!(error, Error::Status(_str, status) =>
assert_eq!(status, zx::Status::IO_DATA_INTEGRITY),
"error should contain a status"
);
}
#[test]
fn assoc_controlled_port_closed_eth_frame_dropped() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let eth_frame = &[100; 14]; // long enough for ethernet header.
let error = state
.on_eth_frame(&mut sta, &eth_frame[..])
.expect_err("Ethernet frame canot be sent when controlled port is closed");
assert_variant!(error, Error::Status(_str, status) =>
assert_eq!(status, zx::Status::BAD_STATE),
"Error should contain status"
);
}
#[test]
fn not_assoc_eth_frame_dropped() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Joined));
let eth_frame = &[100; 14]; // long enough for ethernet header.
let error = state
.on_eth_frame(&mut sta, &eth_frame[..])
.expect_err("Ethernet frame cannot be sent in Joined state");
assert_variant !(error, Error::Status(_str, status) =>
assert_eq!(status, zx::Status::BAD_STATE),
"Error should contain status"
);
}
#[test]
#[allow(deprecated)] // For constructing MLME message
fn finalize_assoc_success() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Associated(Association {
ap_ht_op: Some(ie::fake_ht_operation().as_bytes().try_into().unwrap()),
ap_vht_op: Some(ie::fake_vht_operation().as_bytes().try_into().unwrap()),
..empty_association(&mut sta)
})));
let cap = fidl_mlme::NegotiatedCapabilities {
channel: fidl_common::WlanChan {
primary: 32,
cbw: fidl_common::Cbw::Cbw40,
secondary80: 88,
},
cap_info: 0x1234,
rates: vec![125, 126, 127, 128, 129, 130],
ht_cap: Some(Box::new(fidl_mlme::HtCapabilities {
bytes: ie::fake_ht_capabilities().as_bytes().try_into().unwrap(),
})),
vht_cap: Some(Box::new(fidl_mlme::VhtCapabilities {
bytes: ie::fake_vht_capabilities().as_bytes().try_into().unwrap(),
})),
};
let state = state.handle_mlme_msg(
&mut sta,
fidl_mlme::MlmeRequestMessage::FinalizeAssociationReq { cap },
);
assert_variant!(state, States::Associated(_), "should stay in associated");
assert_eq!(m.fake_device.assocs.keys().count(), 1);
assert_eq!(*m.fake_device.assocs.keys().next().unwrap(), BSSID.0);
let assoc_ctx = m.fake_device.assocs.get(&BSSID.0).unwrap();
assert_eq!(assoc_ctx.aid, 0);
assert_eq!(assoc_ctx.phy, banjo_ddk_protocol_wlan_info::WlanPhyType::VHT);
assert_eq!(assoc_ctx.qos, true);
assert_eq!(assoc_ctx.rates_cnt, 6);
assert_eq!(assoc_ctx.rates[..6], [125, 126, 127, 128, 129, 130]);
assert_eq!(assoc_ctx.cap_info, 0x1234);
assert!(assoc_ctx.has_ht_cap);
assert!(assoc_ctx.has_vht_cap);
assert!(assoc_ctx.has_ht_op);
assert!(assoc_ctx.has_vht_op);
}
#[test]
#[allow(deprecated)] // Raw MLME messages are deprecated.
fn joined_sme_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Joined));
let state = state.handle_mlme_msg(&mut sta, fake_mlme_deauth_req());
assert_variant!(state, States::Joined(_), "Joined should stay in Joined");
// No MLME message was sent because MLME already deauthenticated.
m.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateIndication>()
.expect_err("should be no outgoing message");
}
#[test]
fn authenticating_sme_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Authenticating);
let state = States::from(statemachine::testing::new_state(Authenticating { timeout }));
let state = state.handle_mlme_msg(&mut sta, fake_mlme_deauth_req());
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
assert_variant!(state, States::Joined(_), "should transition to Joined");
// No need to notify SME since it already deauthenticated
m.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
.expect_err("should not see more MLME messages");
}
#[test]
fn authenticated_sme_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Authenticated));
let state = state.handle_mlme_msg(&mut sta, fake_mlme_deauth_req());
assert_variant!(state, States::Joined(_), "should transition to Joined");
// Should accept the deauthentication request and send back confirm.
let deauth_conf = m
.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
.expect("should see deauth conf");
assert_eq!(deauth_conf, fidl_mlme::DeauthenticateConfirm { peer_sta_address: BSSID.0 });
m.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
.expect_err("should not see more MLME messages");
}
#[test]
fn associating_sme_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let timeout =
sta.ctx.timer.schedule_event(zx::Time::after(1.seconds()), TimedEvent::Associating);
let state = States::from(statemachine::testing::new_state(Associating { timeout }));
let state = state.handle_mlme_msg(&mut sta, fake_mlme_deauth_req());
// Verify timeout was canceled.
assert_variant!(sta.ctx.timer.triggered(&timeout), None);
assert_variant!(state, States::Joined(_), "should transition to Joined");
// No need to notify SME since it already deauthenticated
m.fake_device
.next_mlme_msg::<fidl_mlme::AssociateConfirm>()
.expect_err("should not see more MLME messages");
}
#[test]
fn associated_sme_deauth() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Associated(Association {
controlled_port_open: true,
..empty_association(&mut sta)
})));
sta.ctx
.device
.configure_assoc(fake_ddk_assoc_ctx())
.expect("valid assoc ctx should not fail");
assert_eq!(1, m.fake_device.assocs.len());
sta.ctx.device.set_eth_link_up().expect("should succeed");
assert_eq!(crate::device::LinkStatus::UP, m.fake_device.link_status);
let state = state.handle_mlme_msg(&mut sta, fake_mlme_deauth_req());
assert_variant!(state, States::Joined(_), "should transition to Joined");
// Should accept the deauthentication request and send back confirm.
let deauth_conf = m
.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
.expect("should see deauth conf");
assert_eq!(deauth_conf, fidl_mlme::DeauthenticateConfirm { peer_sta_address: BSSID.0 });
m.fake_device
.next_mlme_msg::<fidl_mlme::DeauthenticateConfirm>()
.expect_err("should not see more MLME messages");
// Verify association context cleared.
assert_eq!(0, m.fake_device.assocs.len());
// Verify ethernet link status is down.
assert_eq!(crate::device::LinkStatus::DOWN, m.fake_device.link_status);
}
#[allow(deprecated)]
fn fake_mlme_eapol_req() -> fidl_mlme::MlmeRequestMessage {
fidl_mlme::MlmeRequestMessage::EapolReq {
req: fidl_mlme::EapolRequest {
dst_addr: BSSID.0,
src_addr: IFACE_MAC,
data: vec![1, 2, 3, 4],
},
}
}
#[test]
#[allow(deprecated)]
fn mlme_eapol_not_associated() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Joined));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 0);
let state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 0);
let state = States::from(statemachine::testing::new_state(Authenticated));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 0);
let state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 0);
}
#[test]
#[allow(deprecated)]
fn mlme_eapol_associated_not_protected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 0);
}
#[test]
#[allow(deprecated)]
fn mlme_eapol_associated() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_eapol_req());
assert_eq!(m.fake_device.wlan_queue.len(), 1);
assert_eq!(
&m.fake_device.wlan_queue[0].0[..],
&[
// Data header (EAPoL frames are data frames)
0b00001000, 0b00000001, // Frame Control
0, 0, // Duration
6, 6, 6, 6, 6, 6, // addr1 - BSSID
3, 3, 3, 3, 3, 3, // addr2 - IFACE_MAC
6, 6, 6, 6, 6, 6, // addr3 - BSSID
0x10, 0, // Sequence Control
// LLC header
0xAA, 0xAA, 0x03, // DSAP, SSAP, Control, OUI
0, 0, 0, // OUI
0x88, 0x8E, // Protocol ID (EAPoL is 0x888E)
1, 2, 3, 4, // Payload
][..]
);
}
#[allow(deprecated)]
fn fake_mlme_set_keys_req() -> fidl_mlme::MlmeRequestMessage {
fidl_mlme::MlmeRequestMessage::SetKeysReq {
req: fidl_mlme::SetKeysRequest {
keylist: vec![fidl_mlme::SetKeyDescriptor {
cipher_suite_oui: [1, 2, 3],
cipher_suite_type: 4,
key_type: fidl_mlme::KeyType::Pairwise,
address: [5; 6],
key_id: 6,
key: vec![1, 2, 3, 4, 5, 6, 7],
rsc: 8,
}],
},
}
}
#[test]
#[allow(deprecated)]
fn mlme_set_keys_not_associated() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Joined));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 0);
let state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 0);
let state = States::from(statemachine::testing::new_state(Authenticated));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 0);
let state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 0);
}
#[test]
#[allow(deprecated)]
fn mlme_set_keys_associated_not_protected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 0);
}
#[test]
#[allow(deprecated)]
fn mlme_set_keys_associated() {
use crate::key::*;
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_keys_req());
assert_eq!(m.fake_device.keys.len(), 1);
assert_eq!(
m.fake_device.keys[0],
KeyConfig {
bssid: 0,
protection: Protection::RX_TX,
cipher_oui: [1, 2, 3],
cipher_type: 4,
key_type: KeyType::PAIRWISE,
peer_addr: [5; 6],
key_idx: 6,
key_len: 7,
key: [
1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
],
rsc: 8,
}
);
}
#[allow(deprecated)]
fn fake_mlme_set_ctrl_port_open(open: bool) -> fidl_mlme::MlmeRequestMessage {
fidl_mlme::MlmeRequestMessage::SetControlledPort {
req: fidl_mlme::SetControlledPortRequest {
peer_sta_address: BSSID.0,
state: match open {
true => fidl_mlme::ControlledPortState::Open,
false => fidl_mlme::ControlledPortState::Closed,
},
},
}
}
#[test]
#[allow(deprecated)]
fn mlme_set_controlled_port_not_associated() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Joined));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
let state = States::from(statemachine::testing::new_state(Authenticating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
let state = States::from(statemachine::testing::new_state(Authenticated));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
let state = States::from(statemachine::testing::new_state(Associating {
timeout: EventId::default(),
}));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
}
#[test]
#[allow(deprecated)]
fn mlme_set_controlled_port_associated_not_protected() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
}
#[test]
#[allow(deprecated)]
fn mlme_set_controlled_port_associated() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state =
States::from(statemachine::testing::new_state(Associated(empty_association(&mut sta))));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
let state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(true));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::UP);
let _state = state.handle_mlme_msg(&mut sta, fake_mlme_set_ctrl_port_open(false));
assert_eq!(m.fake_device.link_status, crate::device::LinkStatus::DOWN);
}
#[test]
fn associated_request_bu_if_tim_indicates_buffered_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Associated(Association {
aid: 1,
..empty_association(&mut sta)
})));
let beacon = [
// Mgmt header
0b10000000, 0, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
7, 7, 7, 7, 7, 7, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0, 0, // Sequence Control
// Beacon header:
0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
10, 0, // Beacon interval
33, 0, // Capabilities
5, 4, 0, 0, 0, 0b00000010, // Tim IE: bit 1 in the last octet indicates AID 1
];
state.on_mac_frame(&mut sta, &beacon[..], None);
assert_eq!(m.fake_device.wlan_queue.len(), 1);
assert_eq!(
&m.fake_device.wlan_queue[0].0[..],
&[
0b10100100, 0, // Frame control
1, 0b11000000, // ID (2 MSBs are set to 1 from the AID)
6, 6, 6, 6, 6, 6, // BSSID
3, 3, 3, 3, 3, 3, // TA
][..]
);
}
#[test]
fn associated_does_not_request_bu_if_tim_indicates_no_buffered_frame() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(statemachine::testing::new_state(Associated(Association {
aid: 1,
..empty_association(&mut sta)
})));
let beacon = [
// Mgmt header
0b10000000, 0, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
7, 7, 7, 7, 7, 7, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0, 0, // Sequence Control
// Beacon header:
0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
10, 0, // Beacon interval
33, 0, // Capabilities
5, 4, 0, 0, 0, 0, // Tim IE: No buffered frame for any client.
];
state.on_mac_frame(&mut sta, &beacon[..], None);
assert_eq!(m.fake_device.wlan_queue.len(), 0);
}
fn rx_info_with_dbm(rssi_dbm: i8) -> banjo_wlan_mac::WlanRxInfo {
banjo_wlan_mac::WlanRxInfo {
rx_flags: 0,
valid_fields: banjo_wlan_info::WlanRxInfoValid::RSSI.0,
phy: 0,
data_rate: 0,
chan: banjo_wlan_info::WlanChannel {
primary: 0,
cbw: banjo_wlan_info::WlanChannelBandwidth::_20,
secondary80: 0,
},
mcs: 0,
rssi_dbm,
rcpi_dbmh: 0,
snr_dbh: 0,
}
}
#[test]
fn signal_report() {
let mut m = MockObjects::new();
let mut ctx = m.make_ctx();
let mut sta = make_protected_client_station();
let mut sta = sta.bind(&mut ctx, &mut m.scanner, &mut m.chan_sched, &mut m.channel_state);
let state = States::from(State::from(statemachine::testing::new_state(Associated(
empty_association(&mut sta),
))));
let (id, _dealine) =
m.fake_scheduler.next_event().expect("should see a signal report timeout");
let event = sta.ctx.timer.triggered(&id).expect("event id should exist");
let state = state.on_timed_event(&mut sta, event);
let signal_ind = m
.fake_device
.next_mlme_msg::<fidl_mlme::SignalReportIndication>()
.expect("should see a signal report");
// -128 is the default value, equivalent to 0 watt.
assert_eq!(signal_ind.rssi_dbm, -128);
let beacon = [
// Mgmt header
0b10000000, 0, // Frame Control
0, 0, // Duration
3, 3, 3, 3, 3, 3, // Addr1 == IFACE_MAC
7, 7, 7, 7, 7, 7, // Addr2
6, 6, 6, 6, 6, 6, // Addr3
0, 0, // Sequence Control
// Beacon header:
0, 0, 0, 0, 0, 0, 0, 0, // Timestamp
10, 0, // Beacon interval
33, 0, // Capabilities
];
const EXPECTED_DBM: i8 = -32;
let state = state.on_mac_frame(&mut sta, &beacon[..], Some(rx_info_with_dbm(EXPECTED_DBM)));
let (id, _dealine) =
m.fake_scheduler.next_event().expect("should see a signal report timeout");
let event = sta.ctx.timer.triggered(&id).expect("event id should exist");
let _state = state.on_timed_event(&mut sta, event);
let signal_ind = m
.fake_device
.next_mlme_msg::<fidl_mlme::SignalReportIndication>()
.expect("should see a signal report");
assert_eq!(signal_ind.rssi_dbm, EXPECTED_DBM);
}
}