blob: d540fc693f7f03d12b1517a8064f7e2c0905625a [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use fidl_fuchsia_wlan_mlme::{self as fidl_mlme, BssDescription, MlmeEvent};
use log::{error, warn};
use wlan_common::RadioConfig;
use wlan_rsn::key::exchange::Key;
use wlan_rsn::rsna::{self, SecAssocStatus, SecAssocUpdate};
use super::bss::convert_bss_description;
use super::rsn::Rsna;
use super::{ConnectFailure, ConnectResult, InfoEvent, Status};
use crate::client::{
event::{self, Event},
report_connect_finished, Context,
};
use crate::clone_utils::clone_bss_desc;
use crate::phy_selection::derive_phy_cbw;
use crate::responder::Responder;
use crate::sink::MlmeSink;
use crate::timer::EventId;
use crate::MlmeRequest;
const DEFAULT_JOIN_FAILURE_TIMEOUT: u32 = 20; // beacon intervals
const DEFAULT_AUTH_FAILURE_TIMEOUT: u32 = 20; // beacon intervals
#[derive(Debug)]
pub enum LinkState {
EstablishingRsna {
responder: Option<Responder<ConnectResult>>,
rsna: Rsna,
// Timeout for the total duration RSNA may take to complete.
rsna_timeout: Option<EventId>,
// Timeout waiting to receive a key frame from the Authenticator. This timeout is None at
// the beginning of the RSNA when no frame has been exchanged yet, or at the end of the
// RSNA when all the key frames have finished exchanging.
resp_timeout: Option<EventId>,
},
LinkUp(Option<Rsna>),
}
#[derive(Debug)]
pub struct ConnectCommand {
pub bss: Box<BssDescription>,
pub responder: Option<Responder<ConnectResult>>,
pub rsna: Option<Rsna>,
pub radio_cfg: RadioConfig,
}
#[derive(Debug)]
pub enum RsnaStatus {
Established,
Failed(ConnectResult),
Unchanged,
Progressed { new_resp_timeout: Option<EventId> },
}
#[derive(Debug)]
pub enum State {
Idle,
Joining {
cmd: ConnectCommand,
},
Authenticating {
cmd: ConnectCommand,
},
Associating {
cmd: ConnectCommand,
},
Associated {
bss: Box<BssDescription>,
last_rssi: Option<i8>,
link_state: LinkState,
radio_cfg: RadioConfig,
},
}
impl State {
pub fn on_mlme_event(self, event: MlmeEvent, context: &mut Context) -> Self {
match self {
State::Idle => {
warn!("Unexpected MLME message while Idle: {:?}", event);
State::Idle
}
State::Joining { cmd } => match event {
MlmeEvent::JoinConf { resp } => match resp.result_code {
fidl_mlme::JoinResultCodes::Success => {
context.mlme_sink.send(MlmeRequest::Authenticate(
fidl_mlme::AuthenticateRequest {
peer_sta_address: cmd.bss.bssid.clone(),
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
auth_failure_timeout: DEFAULT_AUTH_FAILURE_TIMEOUT,
},
));
State::Authenticating { cmd }
}
other => {
error!("Join request failed with result code {:?}", other);
report_connect_finished(
cmd.responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::JoinFailure(other)),
);
State::Idle
}
},
_ => State::Joining { cmd },
},
State::Authenticating { cmd } => match event {
MlmeEvent::AuthenticateConf { resp } => match resp.result_code {
fidl_mlme::AuthenticateResultCodes::Success => {
to_associating_state(cmd, &context.mlme_sink)
}
other => {
error!("Authenticate request failed with result code {:?}", other);
report_connect_finished(
cmd.responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::AuthenticationFailure(other)),
);
State::Idle
}
},
_ => State::Authenticating { cmd },
},
State::Associating { cmd } => match event {
MlmeEvent::AssociateConf { resp } => match resp.result_code {
fidl_mlme::AssociateResultCodes::Success => {
context
.info_sink
.send(InfoEvent::AssociationSuccess { att_id: context.att_id });
match cmd.rsna {
Some(mut rsna) => match rsna.supplicant.start() {
Err(e) => {
handle_supplicant_start_failure(
cmd.responder,
cmd.bss,
&context,
e,
);
State::Idle
}
Ok(_) => {
context
.info_sink
.send(InfoEvent::RsnaStarted { att_id: context.att_id });
let rsna_timeout = Some(
context.timer.schedule(Event::EstablishingRsnaTimeout),
);
State::Associated {
bss: cmd.bss,
last_rssi: None,
link_state: LinkState::EstablishingRsna {
responder: cmd.responder,
rsna,
rsna_timeout,
resp_timeout: None,
},
radio_cfg: cmd.radio_cfg,
}
}
},
None => {
report_connect_finished(
cmd.responder,
&context,
ConnectResult::Success,
None,
);
State::Associated {
bss: cmd.bss,
last_rssi: None,
link_state: LinkState::LinkUp(None),
radio_cfg: cmd.radio_cfg,
}
}
}
}
other => {
error!("Associate request failed with result code {:?}", other);
report_connect_finished(
cmd.responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::AssociationFailure(other)),
);
State::Idle
}
},
_ => State::Associating { cmd },
},
State::Associated { bss, last_rssi, link_state, radio_cfg } => match event {
MlmeEvent::DisassociateInd { .. } => {
let (responder, mut rsna) = match link_state {
LinkState::LinkUp(rsna) => (None, rsna),
LinkState::EstablishingRsna { responder, rsna, .. } => {
(responder, Some(rsna))
}
};
// Client is disassociating. The ESS-SA must be kept alive but reset.
if let Some(rsna) = &mut rsna {
rsna.supplicant.reset();
}
let cmd = ConnectCommand { bss, responder, rsna, radio_cfg };
context.att_id += 1;
to_associating_state(cmd, &context.mlme_sink)
}
MlmeEvent::DeauthenticateInd { ind } => {
if let LinkState::EstablishingRsna { responder, .. } = link_state {
let connect_result = deauth_code_to_connect_result(ind.reason_code);
report_connect_finished(responder, &context, connect_result, None);
}
State::Idle
}
MlmeEvent::SignalReport { ind } => {
State::Associated { bss, last_rssi: Some(ind.rssi_dbm), link_state, radio_cfg }
}
MlmeEvent::EapolInd { ref ind } if bss.rsn.is_some() => match link_state {
LinkState::EstablishingRsna {
responder,
mut rsna,
rsna_timeout,
mut resp_timeout,
} => match process_eapol_ind(context, &mut rsna, &ind) {
RsnaStatus::Established => {
context.mlme_sink.send(MlmeRequest::SetCtrlPort(
fidl_mlme::SetControlledPortRequest {
peer_sta_address: bss.bssid.clone(),
state: fidl_mlme::ControlledPortState::Open,
},
));
context
.info_sink
.send(InfoEvent::RsnaEstablished { att_id: context.att_id });
report_connect_finished(
responder,
&context,
ConnectResult::Success,
None,
);
let link_state = LinkState::LinkUp(Some(rsna));
State::Associated { bss, last_rssi, link_state, radio_cfg }
}
RsnaStatus::Failed(result) => {
report_connect_finished(responder, &context, result, None);
send_deauthenticate_request(bss, &context.mlme_sink);
State::Idle
}
RsnaStatus::Unchanged => {
let link_state = LinkState::EstablishingRsna {
responder,
rsna,
rsna_timeout,
resp_timeout,
};
State::Associated { bss, last_rssi, link_state, radio_cfg }
}
RsnaStatus::Progressed { new_resp_timeout } => {
cancel(&mut resp_timeout);
if let Some(id) = new_resp_timeout {
resp_timeout.replace(id);
}
let link_state = LinkState::EstablishingRsna {
responder,
rsna,
rsna_timeout,
resp_timeout,
};
State::Associated { bss, last_rssi, link_state, radio_cfg }
}
},
LinkState::LinkUp(Some(mut rsna)) => {
match process_eapol_ind(context, &mut rsna, &ind) {
RsnaStatus::Unchanged => {}
// Once re-keying is supported, the RSNA can fail in LinkUp as well
// and cause deauthentication.
s => error!("unexpected RsnaStatus in LinkUp state: {:?}", s),
};
let link_state = LinkState::LinkUp(Some(rsna));
State::Associated { bss, last_rssi, link_state, radio_cfg }
}
_ => panic!("expected Link to carry RSNA because bss.rsn is present"),
},
_ => State::Associated { bss, last_rssi, link_state, radio_cfg },
},
}
}
pub fn handle_timeout(self, event_id: EventId, event: Event, context: &mut Context) -> Self {
match self {
State::Associated { bss, last_rssi, link_state, radio_cfg } => match link_state {
LinkState::EstablishingRsna {
responder,
rsna,
mut rsna_timeout,
mut resp_timeout,
} => match event {
Event::EstablishingRsnaTimeout if triggered(&rsna_timeout, event_id) => {
error!("timeout establishing RSNA; deauthenticating");
cancel(&mut rsna_timeout);
report_connect_finished(
responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::RsnaTimeout),
);
send_deauthenticate_request(bss, &context.mlme_sink);
State::Idle
}
Event::KeyFrameExchangeTimeout { bssid, sta_addr, frame, attempt } => {
if !triggered(&resp_timeout, event_id) {
let link_state = LinkState::EstablishingRsna {
responder,
rsna,
rsna_timeout,
resp_timeout,
};
return State::Associated { bss, last_rssi, link_state, radio_cfg };
}
if attempt < event::KEY_FRAME_EXCHANGE_MAX_ATTEMPTS {
warn!(
"timeout waiting for key frame for attempt {}; retrying",
attempt
);
let id = send_eapol_frame(context, bssid, sta_addr, frame, attempt + 1);
resp_timeout.replace(id);
let link_state = LinkState::EstablishingRsna {
responder,
rsna,
rsna_timeout,
resp_timeout,
};
State::Associated { bss, last_rssi, link_state, radio_cfg }
} else {
error!("timeout waiting for key frame for last attempt; deauth");
cancel(&mut resp_timeout);
report_connect_finished(
responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::RsnaTimeout),
);
send_deauthenticate_request(bss, &context.mlme_sink);
State::Idle
}
}
_ => {
let link_state = LinkState::EstablishingRsna {
responder,
rsna,
rsna_timeout,
resp_timeout,
};
State::Associated { bss, last_rssi, link_state, radio_cfg }
}
},
_ => State::Associated { bss, last_rssi, link_state, radio_cfg },
},
_ => self,
}
}
pub fn connect(self, cmd: ConnectCommand, context: &mut Context) -> Self {
self.disconnect_internal(context);
let mut selected_bss = clone_bss_desc(&cmd.bss);
let (phy_to_use, cbw_to_use) =
derive_phy_cbw(&selected_bss, &context.device_info, &cmd.radio_cfg);
selected_bss.chan.cbw = cbw_to_use;
context.mlme_sink.send(MlmeRequest::Join(fidl_mlme::JoinRequest {
selected_bss,
join_failure_timeout: DEFAULT_JOIN_FAILURE_TIMEOUT,
nav_sync_delay: 0,
op_rate_set: vec![],
phy: phy_to_use,
cbw: cbw_to_use,
}));
context.att_id += 1;
context.info_sink.send(InfoEvent::AssociationStarted { att_id: context.att_id });
State::Joining { cmd }
}
pub fn disconnect(self, context: &Context) -> Self {
self.disconnect_internal(context);
State::Idle
}
fn disconnect_internal(self, context: &Context) {
match self {
State::Idle => {}
State::Joining { cmd } | State::Authenticating { cmd } => {
report_connect_finished(cmd.responder, &context, ConnectResult::Canceled, None);
}
State::Associating { cmd, .. } => {
report_connect_finished(cmd.responder, &context, ConnectResult::Canceled, None);
send_deauthenticate_request(cmd.bss, &context.mlme_sink);
}
State::Associated { bss, .. } => {
send_deauthenticate_request(bss, &context.mlme_sink);
}
}
}
pub fn status(&self) -> Status {
match self {
State::Idle => Status { connected_to: None, connecting_to: None },
State::Joining { cmd }
| State::Authenticating { cmd }
| State::Associating { cmd, .. } => {
Status { connected_to: None, connecting_to: Some(cmd.bss.ssid.clone()) }
}
State::Associated { bss, link_state: LinkState::EstablishingRsna { .. }, .. } => {
Status { connected_to: None, connecting_to: Some(bss.ssid.clone()) }
}
State::Associated { bss, link_state: LinkState::LinkUp(..), .. } => {
Status { connected_to: Some(convert_bss_description(bss)), connecting_to: None }
}
}
}
}
fn triggered(id: &Option<EventId>, received_id: EventId) -> bool {
id.map_or(false, |id| id == received_id)
}
fn cancel(event_id: &mut Option<EventId>) {
let _ = event_id.take();
}
fn deauth_code_to_connect_result(reason_code: fidl_mlme::ReasonCode) -> ConnectResult {
match reason_code {
fidl_mlme::ReasonCode::InvalidAuthentication
| fidl_mlme::ReasonCode::Ieee8021XAuthFailed => ConnectResult::BadCredentials,
_ => ConnectResult::Failed,
}
}
fn process_eapol_ind(
context: &mut Context,
rsna: &mut Rsna,
ind: &fidl_mlme::EapolIndication,
) -> RsnaStatus {
let mic_size = rsna.negotiated_rsne.mic_size;
let eapol_pdu = &ind.data[..];
let eapol_frame = match eapol::key_frame_from_bytes(eapol_pdu, mic_size).to_full_result() {
Ok(key_frame) => eapol::Frame::Key(key_frame),
Err(e) => {
error!("received invalid EAPOL Key frame: {:?}", e);
return RsnaStatus::Unchanged;
}
};
let mut update_sink = rsna::UpdateSink::default();
match rsna.supplicant.on_eapol_frame(&mut update_sink, &eapol_frame) {
Err(e) => {
error!("error processing EAPOL key frame: {}", e);
return RsnaStatus::Unchanged;
}
Ok(_) if update_sink.is_empty() => return RsnaStatus::Unchanged,
_ => (),
}
let bssid = ind.src_addr;
let sta_addr = ind.dst_addr;
let mut new_resp_timeout = None;
for update in update_sink {
match update {
// ESS Security Association requests to send an EAPOL frame.
// Forward EAPOL frame to MLME.
SecAssocUpdate::TxEapolKeyFrame(frame) => {
new_resp_timeout.replace(send_eapol_frame(context, bssid, sta_addr, frame, 1));
}
// ESS Security Association derived a new key.
// Configure key in MLME.
SecAssocUpdate::Key(key) => send_keys(&context.mlme_sink, bssid, key),
// Received a status update.
// TODO(hahnr): Rework this part.
// As of now, we depend on the fact that the status is always the last update.
// However, this fact is not clear from the API.
// We should fix the API and make this more explicit.
// Then we should rework this part.
SecAssocUpdate::Status(status) => match status {
// ESS Security Association was successfully established. Link is now up.
SecAssocStatus::EssSaEstablished => return RsnaStatus::Established,
// TODO(hahnr): The API should not expose whether or not the connection failed
// because of bad credentials as it allows callers to reason about location
// information since the network was apparently found.
SecAssocStatus::WrongPassword => {
return RsnaStatus::Failed(ConnectResult::BadCredentials);
}
},
}
}
RsnaStatus::Progressed { new_resp_timeout }
}
fn send_eapol_frame(
context: &mut Context,
bssid: [u8; 6],
sta_addr: [u8; 6],
frame: eapol::KeyFrame,
attempt: u32,
) -> EventId {
let resp_timeout_id = context.timer.schedule(Event::KeyFrameExchangeTimeout {
bssid,
sta_addr,
frame: frame.clone(),
attempt,
});
let mut buf = Vec::with_capacity(frame.len());
frame.as_bytes(false, &mut buf);
context.mlme_sink.send(MlmeRequest::Eapol(fidl_mlme::EapolRequest {
src_addr: sta_addr,
dst_addr: bssid,
data: buf,
}));
resp_timeout_id
}
fn send_keys(mlme_sink: &MlmeSink, bssid: [u8; 6], key: Key) {
match key {
Key::Ptk(ptk) => {
mlme_sink.send(MlmeRequest::SetKeys(fidl_mlme::SetKeysRequest {
keylist: vec![fidl_mlme::SetKeyDescriptor {
key_type: fidl_mlme::KeyType::Pairwise,
key: ptk.tk().to_vec(),
key_id: 0,
address: bssid,
cipher_suite_oui: eapol::to_array(&ptk.cipher.oui[..]),
cipher_suite_type: ptk.cipher.suite_type,
rsc: [0u8; 8],
}],
}));
}
Key::Gtk(gtk) => {
mlme_sink.send(MlmeRequest::SetKeys(fidl_mlme::SetKeysRequest {
keylist: vec![fidl_mlme::SetKeyDescriptor {
key_type: fidl_mlme::KeyType::Group,
key: gtk.tk().to_vec(),
key_id: gtk.key_id() as u16,
address: [0xFFu8; 6],
cipher_suite_oui: eapol::to_array(&gtk.cipher.oui[..]),
cipher_suite_type: gtk.cipher.suite_type,
rsc: [0u8; 8],
}],
}));
}
_ => error!("derived unexpected key"),
};
}
fn send_deauthenticate_request(current_bss: Box<BssDescription>, mlme_sink: &MlmeSink) {
mlme_sink.send(MlmeRequest::Deauthenticate(fidl_mlme::DeauthenticateRequest {
peer_sta_address: current_bss.bssid.clone(),
reason_code: fidl_mlme::ReasonCode::StaLeaving,
}));
}
fn to_associating_state(cmd: ConnectCommand, mlme_sink: &MlmeSink) -> State {
let s_rsne_data = cmd.rsna.as_ref().map(|rsna| {
let s_rsne = rsna.negotiated_rsne.to_full_rsne();
let mut buf = Vec::with_capacity(s_rsne.len());
s_rsne.as_bytes(&mut buf);
buf
});
mlme_sink.send(MlmeRequest::Associate(fidl_mlme::AssociateRequest {
peer_sta_address: cmd.bss.bssid.clone(),
rsn: s_rsne_data,
}));
State::Associating { cmd }
}
fn handle_supplicant_start_failure(
responder: Option<Responder<ConnectResult>>,
bss: Box<BssDescription>,
context: &Context,
e: failure::Error,
) {
error!("deauthenticating; could not start Supplicant: {}", e);
send_deauthenticate_request(bss, &context.mlme_sink);
// TODO(hahnr): Report RSNA specific failure instead.
let reason = fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified;
report_connect_finished(
responder,
&context,
ConnectResult::Failed,
Some(ConnectFailure::AssociationFailure(reason)),
);
}
#[cfg(test)]
mod tests {
use super::*;
use failure::format_err;
use futures::channel::{mpsc, oneshot};
use std::error::Error;
use std::sync::Arc;
use wlan_common::RadioConfig;
use wlan_rsn::{rsna::UpdateSink, rsne::RsnCapabilities, NegotiatedRsne};
use crate::client::test_utils::{
expect_info_event, fake_protected_bss_description, fake_unprotected_bss_description,
mock_supplicant, MockSupplicant, MockSupplicantController,
};
use crate::client::{InfoSink, TimeStream};
use crate::{test_utils, timer, DeviceInfo, InfoStream, MlmeStream, Ssid};
#[test]
fn associate_happy_path_unprotected() {
let mut h = TestHelper::new();
let state = idle_state();
let (command, receiver) = connect_command_one();
let bss_ssid = command.bss.ssid.clone();
let bssid = command.bss.bssid.clone();
// Issue a "connect" command
let state = state.connect(command, &mut h.context);
expect_info_event(&mut h.info_stream, InfoEvent::AssociationStarted { att_id: 1 });
expect_join_request(&mut h.mlme_stream, &bss_ssid);
// (mlme->sme) Send a JoinConf as a response
let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success);
let state = state.on_mlme_event(join_conf, &mut h.context);
expect_auth_req(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an AuthenticateConf as a response
let auth_conf =
create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success);
let state = state.on_mlme_event(auth_conf, &mut h.context);
expect_assoc_req(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an AssociateConf
let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success);
let _state = state.on_mlme_event(assoc_conf, &mut h.context);
// User should be notified that we are connected
expect_result(receiver, ConnectResult::Success);
expect_info_event(&mut h.info_stream, InfoEvent::AssociationSuccess { att_id: 1 });
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished { result: ConnectResult::Success, failure: None },
);
}
#[test]
fn associate_happy_path_protected() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let state = idle_state();
let (command, receiver) = connect_command_rsna(supplicant);
let bss_ssid = command.bss.ssid.clone();
let bssid = command.bss.bssid.clone();
// Issue a "connect" command
let state = state.connect(command, &mut h.context);
expect_info_event(&mut h.info_stream, InfoEvent::AssociationStarted { att_id: 1 });
expect_join_request(&mut h.mlme_stream, &bss_ssid);
// (mlme->sme) Send a JoinConf as a response
let join_conf = create_join_conf(fidl_mlme::JoinResultCodes::Success);
let state = state.on_mlme_event(join_conf, &mut h.context);
expect_auth_req(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an AuthenticateConf as a response
let auth_conf =
create_auth_conf(bssid.clone(), fidl_mlme::AuthenticateResultCodes::Success);
let state = state.on_mlme_event(auth_conf, &mut h.context);
expect_assoc_req(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an AssociateConf
let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success);
let state = state.on_mlme_event(assoc_conf, &mut h.context);
assert!(suppl_mock.is_supplicant_started());
expect_info_event(&mut h.info_stream, InfoEvent::AssociationSuccess { att_id: 1 });
expect_info_event(&mut h.info_stream, InfoEvent::RsnaStarted { att_id: 1 });
// (mlme->sme) Send an EapolInd, mock supplicant with key frame
let update = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame());
let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]);
expect_eapol_req(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an EapolInd, mock supplicant with keys
let ptk = SecAssocUpdate::Key(Key::Ptk(test_utils::ptk()));
let gtk = SecAssocUpdate::Key(Key::Gtk(test_utils::gtk()));
let state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![ptk, gtk]);
expect_set_keys(&mut h.mlme_stream, bssid);
// (mlme->sme) Send an EapolInd, mock supplicant with completion status
let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
let _state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]);
expect_set_ctrl_port(&mut h.mlme_stream, bssid, fidl_mlme::ControlledPortState::Open);
expect_result(receiver, ConnectResult::Success);
expect_info_event(&mut h.info_stream, InfoEvent::RsnaEstablished { att_id: 1 });
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished { result: ConnectResult::Success, failure: None },
);
}
#[test]
fn join_failure() {
let mut h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
// Start in a "Joining" state
let state = State::Joining { cmd };
// (mlme->sme) Send an unsuccessful JoinConf
let join_conf = MlmeEvent::JoinConf {
resp: fidl_mlme::JoinConfirm {
result_code: fidl_mlme::JoinResultCodes::JoinFailureTimeout,
},
};
let state = state.on_mlme_event(join_conf, &mut h.context);
assert_idle(state);
// User should be notified that connection attempt failed
expect_result(receiver, ConnectResult::Failed);
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished {
result: ConnectResult::Failed,
failure: Some(ConnectFailure::JoinFailure(
fidl_mlme::JoinResultCodes::JoinFailureTimeout,
)),
},
);
}
#[test]
fn authenticate_failure() {
let mut h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
// Start in an "Authenticating" state
let state = State::Authenticating { cmd };
// (mlme->sme) Send an unsuccessful AuthenticateConf
let auth_conf = MlmeEvent::AuthenticateConf {
resp: fidl_mlme::AuthenticateConfirm {
peer_sta_address: connect_command_one().0.bss.bssid,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code: fidl_mlme::AuthenticateResultCodes::Refused,
},
};
let state = state.on_mlme_event(auth_conf, &mut h.context);
assert_idle(state);
// User should be notified that connection attempt failed
expect_result(receiver, ConnectResult::Failed);
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished {
result: ConnectResult::Failed,
failure: Some(ConnectFailure::AuthenticationFailure(
fidl_mlme::AuthenticateResultCodes::Refused,
)),
},
);
}
#[test]
fn associate_failure() {
let mut h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
// Start in an "Associating" state
let state = State::Associating { cmd };
// (mlme->sme) Send an unsuccessful AssociateConf
let assoc_conf =
create_assoc_conf(fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified);
let state = state.on_mlme_event(assoc_conf, &mut h.context);
assert_idle(state);
// User should be notified that connection attempt failed
expect_result(receiver, ConnectResult::Failed);
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished {
result: ConnectResult::Failed,
failure: Some(ConnectFailure::AssociationFailure(
fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
)),
},
);
}
#[test]
fn connect_while_joining() {
let mut h = TestHelper::new();
let (cmd_one, receiver_one) = connect_command_one();
let state = joining_state(cmd_one);
let (cmd_two, _receiver_two) = connect_command_two();
let state = state.connect(cmd_two, &mut h.context);
expect_result(receiver_one, ConnectResult::Canceled);
expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid);
assert_joining(state, &connect_command_two().0.bss);
}
#[test]
fn connect_while_authenticating() {
let mut h = TestHelper::new();
let (cmd_one, receiver_one) = connect_command_one();
let state = authenticating_state(cmd_one);
let (cmd_two, _receiver_two) = connect_command_two();
let state = state.connect(cmd_two, &mut h.context);
expect_result(receiver_one, ConnectResult::Canceled);
expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid);
assert_joining(state, &connect_command_two().0.bss);
}
#[test]
fn connect_while_associating() {
let mut h = TestHelper::new();
let (cmd_one, receiver_one) = connect_command_one();
let state = associating_state(cmd_one);
let (cmd_two, _receiver_two) = connect_command_two();
let state = state.connect(cmd_two, &mut h.context);
let state = exchange_deauth(state, &mut h);
expect_result(receiver_one, ConnectResult::Canceled);
expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid);
assert_joining(state, &connect_command_two().0.bss);
}
#[test]
fn supplicant_fails_to_start_while_associating() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let (command, receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
let state = associating_state(command);
suppl_mock.set_start_failure(format_err!("failed to start supplicant"));
// (mlme->sme) Send an AssociateConf
let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success);
let _state = state.on_mlme_event(assoc_conf, &mut h.context);
expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving);
expect_result(receiver, ConnectResult::Failed);
expect_info_event(&mut h.info_stream, InfoEvent::AssociationSuccess { att_id: 0 });
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished {
result: ConnectResult::Failed,
failure: Some(ConnectFailure::AssociationFailure(
fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified,
)),
},
);
}
#[test]
fn bad_eapol_frame_while_establishing_rsna() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let (command, mut receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
let state = establishing_rsna_state(command);
// doesn't matter what we mock here
let update = SecAssocUpdate::Status(SecAssocStatus::EssSaEstablished);
suppl_mock.set_on_eapol_frame_results(vec![update]);
// (mlme->sme) Send an EapolInd with bad eapol data
let eapol_ind = create_eapol_ind(bssid.clone(), vec![1, 2, 3, 4]);
let state = state.on_mlme_event(eapol_ind, &mut h.context);
assert_eq!(Ok(None), receiver.try_recv());
match state {
State::Associated { link_state, .. } => match link_state {
LinkState::EstablishingRsna { .. } => (), // expected path
_ => panic!("expect link state to still be establishing RSNA"),
},
_ => panic!("expect state to still be associated"),
}
expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
expect_stream_empty(&mut h.info_stream, "unexpected event in info stream");
}
#[test]
fn supplicant_fails_to_process_eapol_while_establishing_rsna() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let (command, mut receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
let state = establishing_rsna_state(command);
suppl_mock.set_on_eapol_frame_failure(format_err!("supplicant::on_eapol_frame fails"));
// (mlme->sme) Send an EapolInd
let eapol_ind = create_eapol_ind(bssid.clone(), test_utils::eapol_key_frame_bytes());
let state = state.on_mlme_event(eapol_ind, &mut h.context);
assert_eq!(Ok(None), receiver.try_recv());
match state {
State::Associated { link_state, .. } => match link_state {
LinkState::EstablishingRsna { .. } => (), // expected path
_ => panic!("expect link state to still be establishing RSNA"),
},
_ => panic!("expect state to still be associated"),
}
expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
expect_stream_empty(&mut h.info_stream, "unexpected event in info stream");
}
#[test]
fn wrong_password_while_establishing_rsna() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let (command, receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
let state = establishing_rsna_state(command);
// (mlme->sme) Send an EapolInd, mock supplicant with wrong password status
let update = SecAssocUpdate::Status(SecAssocStatus::WrongPassword);
let _state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]);
expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving);
expect_result(receiver, ConnectResult::BadCredentials);
expect_info_event(
&mut h.info_stream,
InfoEvent::ConnectFinished { result: ConnectResult::BadCredentials, failure: None },
);
}
#[test]
fn overall_timeout_while_establishing_rsna() {
let mut h = TestHelper::new();
let (supplicant, _suppl_mock) = mock_supplicant();
let (command, receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
// Start in an "Associating" state
let state = State::Associating { cmd: command };
let assoc_conf = create_assoc_conf(fidl_mlme::AssociateResultCodes::Success);
let state = state.on_mlme_event(assoc_conf, &mut h.context);
let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event");
match timed_event.event {
Event::EstablishingRsnaTimeout => (), // expected path
_ => panic!("expect EstablishingRsnaTimeout timeout event"),
}
expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
let _state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context);
expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving);
expect_result(receiver, ConnectResult::Failed);
}
#[test]
fn key_frame_exchange_timeout_while_establishing_rsna() {
let mut h = TestHelper::new();
let (supplicant, suppl_mock) = mock_supplicant();
let (command, receiver) = connect_command_rsna(supplicant);
let bssid = command.bss.bssid.clone();
let state = establishing_rsna_state(command);
// (mlme->sme) Send an EapolInd, mock supplication with key frame
let update = SecAssocUpdate::TxEapolKeyFrame(test_utils::eapol_key_frame());
let mut state = on_eapol_ind(state, &mut h, bssid, &suppl_mock, vec![update]);
for i in 1..=3 {
println!("send eapol attempt: {}", i);
expect_eapol_req(&mut h.mlme_stream, bssid);
expect_stream_empty(&mut h.mlme_stream, "unexpected event in mlme stream");
let (_, timed_event) = h.time_stream.try_next().unwrap().expect("expect timed event");
match timed_event.event {
Event::KeyFrameExchangeTimeout { attempt, .. } => assert_eq!(attempt, i),
_ => panic!("expect EstablishingRsnaTimeout timeout event"),
}
state = state.handle_timeout(timed_event.id, timed_event.event, &mut h.context);
}
expect_deauth_req(&mut h.mlme_stream, bssid, fidl_mlme::ReasonCode::StaLeaving);
expect_result(receiver, ConnectResult::Failed);
}
#[test]
fn connect_while_link_up() {
let mut h = TestHelper::new();
let state = link_up_state(connect_command_one().0.bss);
let state = state.connect(connect_command_two().0, &mut h.context);
let state = exchange_deauth(state, &mut h);
expect_join_request(&mut h.mlme_stream, &connect_command_two().0.bss.ssid);
assert_joining(state, &connect_command_two().0.bss);
}
#[test]
fn disconnect_while_idle() {
let mut h = TestHelper::new();
let new_state = idle_state().disconnect(&h.context);
assert_idle(new_state);
// Expect no messages to the MLME
assert!(h.mlme_stream.try_next().is_err());
}
#[test]
fn disconnect_while_joining() {
let h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
let state = joining_state(cmd);
let state = state.disconnect(&h.context);
expect_result(receiver, ConnectResult::Canceled);
assert_idle(state);
}
#[test]
fn disconnect_while_authenticating() {
let h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
let state = authenticating_state(cmd);
let state = state.disconnect(&h.context);
expect_result(receiver, ConnectResult::Canceled);
assert_idle(state);
}
#[test]
fn disconnect_while_associating() {
let mut h = TestHelper::new();
let (cmd, receiver) = connect_command_one();
let state = associating_state(cmd);
let state = state.disconnect(&h.context);
let state = exchange_deauth(state, &mut h);
expect_result(receiver, ConnectResult::Canceled);
assert_idle(state);
}
#[test]
fn disconnect_while_link_up() {
let mut h = TestHelper::new();
let state = link_up_state(connect_command_one().0.bss);
let state = state.disconnect(&h.context);
let state = exchange_deauth(state, &mut h);
assert_idle(state);
}
#[test]
fn increment_att_id_on_connect() {
let mut h = TestHelper::new();
let state = idle_state();
assert_eq!(h.context.att_id, 0);
let state = state.connect(connect_command_one().0, &mut h.context);
assert_eq!(h.context.att_id, 1);
let state = state.disconnect(&h.context);
assert_eq!(h.context.att_id, 1);
let state = state.connect(connect_command_two().0, &mut h.context);
assert_eq!(h.context.att_id, 2);
let _state = state.connect(connect_command_one().0, &mut h.context);
assert_eq!(h.context.att_id, 3);
}
#[test]
fn increment_att_id_on_disassociate_ind() {
let mut h = TestHelper::new();
let state = link_up_state(Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8])));
assert_eq!(h.context.att_id, 0);
let disassociate_ind = MlmeEvent::DisassociateInd {
ind: fidl_mlme::DisassociateIndication {
peer_sta_address: [0, 0, 0, 0, 0, 0],
reason_code: 0,
},
};
let state = state.on_mlme_event(disassociate_ind, &mut h.context);
assert_associating(state, &unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8]));
assert_eq!(h.context.att_id, 1);
}
struct TestHelper {
mlme_stream: MlmeStream,
info_stream: InfoStream,
time_stream: TimeStream,
context: Context,
}
impl TestHelper {
fn new() -> Self {
let (mlme_sink, mlme_stream) = mpsc::unbounded();
let (info_sink, info_stream) = mpsc::unbounded();
let (timer, time_stream) = timer::create_timer();
let context = Context {
device_info: Arc::new(fake_device_info()),
mlme_sink: MlmeSink::new(mlme_sink),
info_sink: InfoSink::new(info_sink),
timer,
att_id: 0,
};
TestHelper { mlme_stream, info_stream, time_stream, context }
}
}
fn on_eapol_ind(
state: State,
helper: &mut TestHelper,
bssid: [u8; 6],
suppl_mock: &MockSupplicantController,
update_sink: UpdateSink,
) -> State {
suppl_mock.set_on_eapol_frame_results(update_sink);
// (mlme->sme) Send an EapolInd
let eapol_ind = create_eapol_ind(bssid.clone(), test_utils::eapol_key_frame_bytes());
state.on_mlme_event(eapol_ind, &mut helper.context)
}
fn create_join_conf(result_code: fidl_mlme::JoinResultCodes) -> MlmeEvent {
MlmeEvent::JoinConf { resp: fidl_mlme::JoinConfirm { result_code } }
}
fn create_auth_conf(
bssid: [u8; 6],
result_code: fidl_mlme::AuthenticateResultCodes,
) -> MlmeEvent {
MlmeEvent::AuthenticateConf {
resp: fidl_mlme::AuthenticateConfirm {
peer_sta_address: bssid,
auth_type: fidl_mlme::AuthenticationTypes::OpenSystem,
result_code,
},
}
}
fn create_assoc_conf(result_code: fidl_mlme::AssociateResultCodes) -> MlmeEvent {
MlmeEvent::AssociateConf {
resp: fidl_mlme::AssociateConfirm { result_code, association_id: 55 },
}
}
fn create_eapol_ind(bssid: [u8; 6], data: Vec<u8>) -> MlmeEvent {
MlmeEvent::EapolInd {
ind: fidl_mlme::EapolIndication {
src_addr: bssid,
dst_addr: fake_device_info().addr,
data,
},
}
}
fn exchange_deauth(state: State, h: &mut TestHelper) -> State {
// (sme->mlme) Expect a DeauthenticateRequest
match h.mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Deauthenticate(req)) => {
assert_eq!(connect_command_one().0.bss.bssid, req.peer_sta_address);
}
other => panic!("expected a Deauthenticate request, got {:?}", other),
}
// (mlme->sme) Send a DeauthenticateConf as a response
let deauth_conf = MlmeEvent::DeauthenticateConf {
resp: fidl_mlme::DeauthenticateConfirm {
peer_sta_address: connect_command_one().0.bss.bssid,
},
};
state.on_mlme_event(deauth_conf, &mut h.context)
}
fn expect_join_request(mlme_stream: &mut MlmeStream, ssid: &[u8]) {
// (sme->mlme) Expect a JoinRequest
match mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Join(req)) => assert_eq!(ssid, &req.selected_bss.ssid[..]),
other => panic!("expect join req to MLME, got {:?}", other),
}
}
fn expect_set_ctrl_port(
mlme_stream: &mut MlmeStream,
bssid: [u8; 6],
state: fidl_mlme::ControlledPortState,
) {
match mlme_stream.try_next().unwrap().expect("expect mlme message") {
MlmeRequest::SetCtrlPort(req) => {
assert_eq!(req.peer_sta_address, bssid);
assert_eq!(req.state, state);
}
other => panic!("expected a Join request, got {:?}", other),
}
}
fn expect_auth_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) {
// (sme->mlme) Expect an AuthenticateRequest
match mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Authenticate(req)) => assert_eq!(bssid, req.peer_sta_address),
other => panic!("expected an Authenticate request, got {:?}", other),
}
}
fn expect_deauth_req(
mlme_stream: &mut MlmeStream,
bssid: [u8; 6],
reason_code: fidl_mlme::ReasonCode,
) {
// (sme->mlme) Expect a DeauthenticateRequest
match mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Deauthenticate(req)) => {
assert_eq!(bssid, req.peer_sta_address);
assert_eq!(reason_code, req.reason_code);
}
other => panic!("expected an Deauthenticate request, got {:?}", other),
}
}
fn expect_assoc_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) {
match mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Associate(req)) => assert_eq!(bssid, req.peer_sta_address),
other => panic!("expected an Associate request, got {:?}", other),
}
}
fn expect_eapol_req(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) {
match mlme_stream.try_next().unwrap() {
Some(MlmeRequest::Eapol(req)) => {
assert_eq!(req.src_addr, fake_device_info().addr);
assert_eq!(req.dst_addr, bssid);
assert_eq!(req.data, test_utils::eapol_key_frame_bytes());
}
other => panic!("expected an Eapol request, got {:?}", other),
}
}
fn expect_set_keys(mlme_stream: &mut MlmeStream, bssid: [u8; 6]) {
match mlme_stream.try_next().unwrap().expect("expect mlme message") {
MlmeRequest::SetKeys(set_keys_req) => {
assert_eq!(set_keys_req.keylist.len(), 1);
let k = set_keys_req.keylist.get(0).expect("expect key descriptor");
assert_eq!(k.key, vec![0xCCu8; test_utils::cipher().tk_bytes().unwrap()]);
assert_eq!(k.key_id, 0);
assert_eq!(k.key_type, fidl_mlme::KeyType::Pairwise);
assert_eq!(k.address, bssid);
assert_eq!(k.rsc, [0u8; 8]);
assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]);
assert_eq!(k.cipher_suite_type, 4);
}
_ => panic!("expect set keys req to MLME"),
}
match mlme_stream.try_next().unwrap().expect("expect mlme message") {
MlmeRequest::SetKeys(set_keys_req) => {
assert_eq!(set_keys_req.keylist.len(), 1);
let k = set_keys_req.keylist.get(0).expect("expect key descriptor");
assert_eq!(k.key, test_utils::gtk_bytes());
assert_eq!(k.key_id, 2);
assert_eq!(k.key_type, fidl_mlme::KeyType::Group);
assert_eq!(k.address, [0xFFu8; 6]);
assert_eq!(k.rsc, [0u8; 8]);
assert_eq!(k.cipher_suite_oui, [0x00, 0x0F, 0xAC]);
assert_eq!(k.cipher_suite_type, 4);
}
_ => panic!("expect set keys req to MLME"),
}
}
fn expect_result<T>(mut receiver: oneshot::Receiver<T>, expected_result: T)
where
T: PartialEq + ::std::fmt::Debug,
{
assert_eq!(Ok(Some(expected_result)), receiver.try_recv());
}
fn expect_stream_empty<T>(stream: &mut mpsc::UnboundedReceiver<T>, error_msg: &str) {
match stream.try_next() {
Err(e) => assert_eq!(e.description(), "receiver channel is empty"),
_ => panic!("{}", error_msg),
}
}
fn connect_command_one() -> (ConnectCommand, oneshot::Receiver<ConnectResult>) {
let (responder, receiver) = Responder::new();
let cmd = ConnectCommand {
bss: Box::new(unprotected_bss(b"foo".to_vec(), [7, 7, 7, 7, 7, 7])),
responder: Some(responder),
rsna: None,
radio_cfg: RadioConfig::default(),
};
(cmd, receiver)
}
fn connect_command_two() -> (ConnectCommand, oneshot::Receiver<ConnectResult>) {
let (responder, receiver) = Responder::new();
let cmd = ConnectCommand {
bss: Box::new(unprotected_bss(b"bar".to_vec(), [8, 8, 8, 8, 8, 8])),
responder: Some(responder),
rsna: None,
radio_cfg: RadioConfig::default(),
};
(cmd, receiver)
}
fn connect_command_rsna(
supplicant: MockSupplicant,
) -> (ConnectCommand, oneshot::Receiver<ConnectResult>) {
let (responder, receiver) = Responder::new();
let bss = protected_bss(b"foo".to_vec(), [7, 7, 7, 7, 7, 7]);
let rsne = test_utils::wpa2_psk_ccmp_rsne_with_caps(RsnCapabilities(0));
let cmd = ConnectCommand {
bss: Box::new(bss),
responder: Some(responder),
rsna: Some(Rsna {
negotiated_rsne: NegotiatedRsne::from_rsne(&rsne).expect("invalid NegotiatedRsne"),
supplicant: Box::new(supplicant),
}),
radio_cfg: RadioConfig::default(),
};
(cmd, receiver)
}
fn idle_state() -> State {
State::Idle
}
fn assert_idle(state: State) {
match state {
State::Idle => {}
other => panic!("Expected an Idle state, got {:?}", other),
}
}
fn joining_state(cmd: ConnectCommand) -> State {
State::Joining { cmd }
}
fn assert_joining(state: State, bss: &BssDescription) {
match state {
State::Joining { cmd } => {
assert_eq!(cmd.bss.as_ref(), bss);
}
other => panic!("Expected a Joining state, got {:?}", other),
}
}
fn authenticating_state(cmd: ConnectCommand) -> State {
State::Authenticating { cmd }
}
fn associating_state(cmd: ConnectCommand) -> State {
State::Associating { cmd }
}
fn assert_associating(state: State, bss: &BssDescription) {
match state {
State::Associating { cmd } => {
assert_eq!(cmd.bss.as_ref(), bss);
}
other => panic!("Expected an Associating state, got {:?}", other),
}
}
fn establishing_rsna_state(cmd: ConnectCommand) -> State {
let rsna = cmd.rsna.expect("expect rsna for establishing_rsna_state");
State::Associated {
bss: cmd.bss,
last_rssi: None,
link_state: LinkState::EstablishingRsna {
responder: cmd.responder,
rsna,
rsna_timeout: None,
resp_timeout: None,
},
radio_cfg: RadioConfig::default(),
}
}
fn link_up_state(bss: Box<fidl_mlme::BssDescription>) -> State {
State::Associated {
bss,
last_rssi: None,
link_state: LinkState::LinkUp(None),
radio_cfg: RadioConfig::default(),
}
}
fn protected_bss(ssid: Ssid, bssid: [u8; 6]) -> fidl_mlme::BssDescription {
fidl_mlme::BssDescription { bssid, ..fake_protected_bss_description(ssid) }
}
fn unprotected_bss(ssid: Ssid, bssid: [u8; 6]) -> fidl_mlme::BssDescription {
fidl_mlme::BssDescription { bssid, ..fake_unprotected_bss_description(ssid) }
}
fn fake_device_info() -> DeviceInfo {
DeviceInfo { addr: [0, 1, 2, 3, 4, 5], bands: vec![] }
}
}